Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions ytconverter/cli/banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
16 changes: 12 additions & 4 deletions ytconverter/cli/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -52,6 +63,3 @@ def main_loop():
"Press [ENTER] to continue downloading another content ", "/green/bold"
)
input(exitc)



77 changes: 77 additions & 0 deletions ytconverter/downloaders/playlist_mp3.py
Original file line number Diff line number Diff line change
@@ -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"))
102 changes: 102 additions & 0 deletions ytconverter/downloaders/playlist_mp4.py
Original file line number Diff line number Diff line change
@@ -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"))