From 2b3a51831045dce8fd957cf1e60acd82b2ceb47c Mon Sep 17 00:00:00 2001 From: kaifcodec Date: Mon, 29 Dec 2025 08:49:06 +0530 Subject: [PATCH] feat: add playlist support --- ytconverter/cli/banner.py | 12 +-- ytconverter/cli/menu.py | 16 +++- ytconverter/downloaders/playlist_mp3.py | 77 ++++++++++++++++++ ytconverter/downloaders/playlist_mp4.py | 102 ++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 ytconverter/downloaders/playlist_mp3.py create mode 100644 ytconverter/downloaders/playlist_mp4.py diff --git a/ytconverter/cli/banner.py b/ytconverter/cli/banner.py index bf72ea4..0fdbff5 100644 --- a/ytconverter/cli/banner.py +++ b/ytconverter/cli/banner.py @@ -14,11 +14,13 @@ ║ ♚ Email : kaifcodec@gmail.com ║ ╠═════════════════════════════════════════ """ f3 = """ ╠═▶ [𝗦𝗲𝗹𝗲𝗰𝘁 𝗔n 𝐎𝐩𝐭𝐢𝐨𝐧] ➳ - ╠═▶ 1. Single Music Mp3 ⏬ 🎶 - ╠═▶ 2. Single Video ⏬ 🎥(detailed quailty & size but slow fetch) - ╠═▶ 3. Multiple videos ⏬ 🎥 - ╠═▶ 4. Multiple audios ⏬ 🎶 - ╠═▶ 5. Exit YTConverter""" + ╠═▶ 1. Single Music Mp3 + ╠═▶ 2. Single Video (detailed quailty & size but slow fetch) + ╠═▶ 3. Multiple videos + ╠═▶ 4. Multiple audios + ╠═▶ 5. Full Playlist MP3 + ╠═▶ 6. Full Playlist MP4 + ╠═▶ 7. Exit YTConverter""" f4 = " ╚═:➤ " des1 = apply_style(f1, "/green/bold") diff --git a/ytconverter/cli/menu.py b/ytconverter/cli/menu.py index d2b281c..a6abe9c 100644 --- a/ytconverter/cli/menu.py +++ b/ytconverter/cli/menu.py @@ -7,7 +7,14 @@ from ytconverter.core.bootstrap import ensure_dependencies, setup_termux_storage from ytconverter.utils.update import update_self from ytconverter.core.version import check_version -from ytconverter.downloaders import multi_mp3, multi_mp4, single_mp3, single_mp4 +from ytconverter.downloaders import ( + multi_mp3, + multi_mp4, + single_mp3, + single_mp4, + playlist_mp4, + playlist_mp3, +) from ytconverter.utils.styling import apply_style ensure_dependencies() @@ -42,6 +49,10 @@ def main_loop(): elif choice == "4": multi_mp3.run() elif choice == "5": + playlist_mp3.run() + elif choice == "6": + playlist_mp4.run() + elif choice == "7": print("Have a nice day, Bye!") sys.exit() else: @@ -52,6 +63,3 @@ def main_loop(): "Press [ENTER] to continue downloading another content ", "/green/bold" ) input(exitc) - - - diff --git a/ytconverter/downloaders/playlist_mp3.py b/ytconverter/downloaders/playlist_mp3.py new file mode 100644 index 0000000..13876f7 --- /dev/null +++ b/ytconverter/downloaders/playlist_mp3.py @@ -0,0 +1,77 @@ +import subprocess as sp +import time +from pathlib import Path + +import yt_dlp + +from ytconverter.constants import URL_RE +from ytconverter.utils import apply_style, get_download_path, sanitize + + +def run(): + from ytconverter.config import load_local_version + + print("\n" + apply_style("Enter the playlist URL you want to download as MP3 (audio):", "/cyan")) + url = input(">> ").strip() + if not URL_RE.match(url): + print(apply_style("Invalid URL. Please enter a valid YouTube URL.", "/red/bold")) + return + + ydl_opts = {"quiet": True, "no_warnings": True} + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=False) + except Exception as e: + print(apply_style(f"Failed to fetch playlist info: {e}", "/red/bold")) + return + + playlist_title = info.get("title") or info.get("playlist_title") or "playlist" + safe_playlist_title = sanitize(playlist_title)[:60] + + destination = Path(get_download_path("mp3")) + playlist_folder = destination / safe_playlist_title + playlist_folder.mkdir(parents=True, exist_ok=True) + + ask_subs = input(apply_style("\nDownload subtitles for playlist videos? (y/n): ", "/cyan")).strip().lower() + sub_flags = [] + cookie_path = "" + if ask_subs == "y": + print(apply_style("Note: YouTube may block subtitle extraction with HTTP 429. If that happens provide a cookies file.", "/yellow")) + cookie_path = input(apply_style("Enter path to cookies file for yt-dlp (optional, press Enter to skip): ", "/green")).strip() + lang = input(apply_style("Enter subtitle language code (e.g. en) or leave blank for all: ", "/green")).strip() + pref_auto = input(apply_style("Also download automatic subtitles if manual not available? (y/n) [y]: ", "/green")).strip().lower() + if pref_auto == "": + pref_auto = "y" + sub_flags += ["--write-sub"] + if pref_auto == "y": + sub_flags += ["--write-auto-sub"] + if lang: + sub_flags += ["--sub-lang", lang] + sub_flags += ["--convert-subs", "srt"] + if cookie_path: + sub_flags += ["--cookies", cookie_path] + + out_template = str(playlist_folder / "%(playlist_index)03d - %(title)s.%(ext)s") + cmd = [ + "yt-dlp", + "-i", + "--yes-playlist", + "-x", + "--audio-format", "mp3", + "-o", out_template, + ] + if sub_flags: + cmd += sub_flags + + cmd += [url] + + print(apply_style("\nStarting playlist MP3 download. This may take a while depending on size...\n", "/yellow/bold")) + start = int(time.time()) + try: + sp.run(cmd, check=True) + except Exception as e: + print(apply_style(f"An error occurred while downloading the playlist: {e}", "/red/bold")) + return + end = int(time.time()) + print(apply_style(f"\nPlaylist '{playlist_title}' downloaded to: {playlist_folder}", "/green/bold")) + print(apply_style(f"Time taken: {end - start} sec", "/cyan")) diff --git a/ytconverter/downloaders/playlist_mp4.py b/ytconverter/downloaders/playlist_mp4.py new file mode 100644 index 0000000..a6618d6 --- /dev/null +++ b/ytconverter/downloaders/playlist_mp4.py @@ -0,0 +1,102 @@ +import subprocess as sp +import time +from pathlib import Path + +import yt_dlp + +from ytconverter.constants import URL_RE +from ytconverter.utils import apply_style, get_download_path, sanitize + + +def run(): + from ytconverter.config import load_local_version + + print("\n" + apply_style("Enter the playlist URL you want to download as MP4:", "/cyan")) + url = input(">> ").strip() + if not URL_RE.match(url): + print(apply_style("Invalid URL. Please enter a valid YouTube URL.", "/red/bold")) + return + + ydl_opts = {"quiet": True, "no_warnings": True} + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=False) + except Exception as e: + print(apply_style(f"Failed to fetch playlist info: {e}", "/red/bold")) + return + + playlist_title = info.get("title") or info.get("playlist_title") or "playlist" + safe_playlist_title = sanitize(playlist_title)[:60] + + format_map = { + "1": "bestvideo[height>=1080]+bestaudio/best[height>=1080]", + "2": "bestvideo[height<=1080]+bestaudio/best[height<=1080]", + "3": "bestvideo[height<=720]+bestaudio/best[height<=720]", + "4": "bestvideo[height<=480]+bestaudio/best[height<=480]", + "5": "bestvideo[height<=360]+bestaudio/best[height<=360]", + } + + quality_title = """ +Select desired download quality for playlist: + [1] >= 1080p Full HD+/4K + [2] 1080p Full HD + [3] 720p HD + [4] 480p SD + [5] <= 360p Low + """ + print(apply_style(quality_title, "/cyan/bold")) + while True: + qua_text = apply_style("Enter choice number (1-5): ", "/green/bold") + choice = input(qua_text).strip() + if choice in format_map: + break + print(apply_style("Invalid choice. Please select a number from 1 to 5.", "/red/bold")) + + destination = Path(get_download_path("mp4")) + playlist_folder = destination / safe_playlist_title + playlist_folder.mkdir(parents=True, exist_ok=True) + + ask_subs = input(apply_style("\nDownload subtitles for playlist videos? (y/n): ", "/cyan")).strip().lower() + sub_flags = [] + cookie_path = "" + if ask_subs == "y": + print(apply_style("Note: YouTube may block subtitle extraction with HTTP 429. If that happens provide a cookies file.", "/yellow")) + cookie_path = input(apply_style("Enter path to cookies file for yt-dlp (optional, press Enter to skip): ", "/green")).strip() + lang = input(apply_style("Enter subtitle language code (e.g. en) or leave blank for all: ", "/green")).strip() + pref_auto = input(apply_style("Also download automatic subtitles if manual not available? (y/n) [y]: ", "/green")).strip().lower() + if pref_auto == "": + pref_auto = "y" + sub_flags += ["--write-sub"] + if pref_auto == "y": + sub_flags += ["--write-auto-sub"] + if lang: + sub_flags += ["--sub-lang", lang] + sub_flags += ["--convert-subs", "srt"] + if cookie_path: + sub_flags += ["--cookies", cookie_path] + + fmt = format_map[choice] + out_template = str(playlist_folder / "%(playlist_index)03d - %(title)s.%(ext)s") + + cmd = [ + "yt-dlp", + "-i", + "--yes-playlist", + "-f", fmt, + "-o", out_template, + ] + if sub_flags: + cmd += sub_flags + + cmd += [url] + + print(apply_style("\nStarting playlist download. This may take a while depending on size...\n", "/yellow/bold")) + start = int(time.time()) + try: + sp.run(cmd, check=True) + except Exception as e: + print(apply_style(f"An error occurred while downloading the playlist: {e}", "/red/bold")) + return + end = int(time.time()) + print(apply_style(f"\nPlaylist '{playlist_title}' downloaded to: {playlist_folder}", "/green/bold")) + print(apply_style(f"Time taken: {end - start} sec", "/cyan"))