Skip to content

Commit b1cc56e

Browse files
authored
Merge pull request #3 from kaifcodec/feat/playlist-support
feat: add playlist support
2 parents b8ef755 + 2b3a518 commit b1cc56e

4 files changed

Lines changed: 198 additions & 9 deletions

File tree

ytconverter/cli/banner.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
║ ♚ Email : kaifcodec@gmail.com ║
1515
╠═════════════════════════════════════════ """
1616
f3 = """ ╠═▶ [𝗦𝗲𝗹𝗲𝗰𝘁 𝗔n 𝐎𝐩𝐭𝐢𝐨𝐧] ➳
17-
╠═▶ 1. Single Music Mp3 ⏬ 🎶
18-
╠═▶ 2. Single Video ⏬ 🎥(detailed quailty & size but slow fetch)
19-
╠═▶ 3. Multiple videos ⏬ 🎥
20-
╠═▶ 4. Multiple audios ⏬ 🎶
21-
╠═▶ 5. Exit YTConverter"""
17+
╠═▶ 1. Single Music Mp3
18+
╠═▶ 2. Single Video (detailed quailty & size but slow fetch)
19+
╠═▶ 3. Multiple videos
20+
╠═▶ 4. Multiple audios
21+
╠═▶ 5. Full Playlist MP3
22+
╠═▶ 6. Full Playlist MP4
23+
╠═▶ 7. Exit YTConverter"""
2224
f4 = " ╚═:➤ "
2325

2426
des1 = apply_style(f1, "/green/bold")

ytconverter/cli/menu.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
from ytconverter.core.bootstrap import ensure_dependencies, setup_termux_storage
88
from ytconverter.utils.update import update_self
99
from ytconverter.core.version import check_version
10-
from ytconverter.downloaders import multi_mp3, multi_mp4, single_mp3, single_mp4
10+
from ytconverter.downloaders import (
11+
multi_mp3,
12+
multi_mp4,
13+
single_mp3,
14+
single_mp4,
15+
playlist_mp4,
16+
playlist_mp3,
17+
)
1118
from ytconverter.utils.styling import apply_style
1219

1320
ensure_dependencies()
@@ -42,6 +49,10 @@ def main_loop():
4249
elif choice == "4":
4350
multi_mp3.run()
4451
elif choice == "5":
52+
playlist_mp3.run()
53+
elif choice == "6":
54+
playlist_mp4.run()
55+
elif choice == "7":
4556
print("Have a nice day, Bye!")
4657
sys.exit()
4758
else:
@@ -52,6 +63,3 @@ def main_loop():
5263
"Press [ENTER] to continue downloading another content ", "/green/bold"
5364
)
5465
input(exitc)
55-
56-
57-
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import subprocess as sp
2+
import time
3+
from pathlib import Path
4+
5+
import yt_dlp
6+
7+
from ytconverter.constants import URL_RE
8+
from ytconverter.utils import apply_style, get_download_path, sanitize
9+
10+
11+
def run():
12+
from ytconverter.config import load_local_version
13+
14+
print("\n" + apply_style("Enter the playlist URL you want to download as MP3 (audio):", "/cyan"))
15+
url = input(">> ").strip()
16+
if not URL_RE.match(url):
17+
print(apply_style("Invalid URL. Please enter a valid YouTube URL.", "/red/bold"))
18+
return
19+
20+
ydl_opts = {"quiet": True, "no_warnings": True}
21+
try:
22+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
23+
info = ydl.extract_info(url, download=False)
24+
except Exception as e:
25+
print(apply_style(f"Failed to fetch playlist info: {e}", "/red/bold"))
26+
return
27+
28+
playlist_title = info.get("title") or info.get("playlist_title") or "playlist"
29+
safe_playlist_title = sanitize(playlist_title)[:60]
30+
31+
destination = Path(get_download_path("mp3"))
32+
playlist_folder = destination / safe_playlist_title
33+
playlist_folder.mkdir(parents=True, exist_ok=True)
34+
35+
ask_subs = input(apply_style("\nDownload subtitles for playlist videos? (y/n): ", "/cyan")).strip().lower()
36+
sub_flags = []
37+
cookie_path = ""
38+
if ask_subs == "y":
39+
print(apply_style("Note: YouTube may block subtitle extraction with HTTP 429. If that happens provide a cookies file.", "/yellow"))
40+
cookie_path = input(apply_style("Enter path to cookies file for yt-dlp (optional, press Enter to skip): ", "/green")).strip()
41+
lang = input(apply_style("Enter subtitle language code (e.g. en) or leave blank for all: ", "/green")).strip()
42+
pref_auto = input(apply_style("Also download automatic subtitles if manual not available? (y/n) [y]: ", "/green")).strip().lower()
43+
if pref_auto == "":
44+
pref_auto = "y"
45+
sub_flags += ["--write-sub"]
46+
if pref_auto == "y":
47+
sub_flags += ["--write-auto-sub"]
48+
if lang:
49+
sub_flags += ["--sub-lang", lang]
50+
sub_flags += ["--convert-subs", "srt"]
51+
if cookie_path:
52+
sub_flags += ["--cookies", cookie_path]
53+
54+
out_template = str(playlist_folder / "%(playlist_index)03d - %(title)s.%(ext)s")
55+
cmd = [
56+
"yt-dlp",
57+
"-i",
58+
"--yes-playlist",
59+
"-x",
60+
"--audio-format", "mp3",
61+
"-o", out_template,
62+
]
63+
if sub_flags:
64+
cmd += sub_flags
65+
66+
cmd += [url]
67+
68+
print(apply_style("\nStarting playlist MP3 download. This may take a while depending on size...\n", "/yellow/bold"))
69+
start = int(time.time())
70+
try:
71+
sp.run(cmd, check=True)
72+
except Exception as e:
73+
print(apply_style(f"An error occurred while downloading the playlist: {e}", "/red/bold"))
74+
return
75+
end = int(time.time())
76+
print(apply_style(f"\nPlaylist '{playlist_title}' downloaded to: {playlist_folder}", "/green/bold"))
77+
print(apply_style(f"Time taken: {end - start} sec", "/cyan"))
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import subprocess as sp
2+
import time
3+
from pathlib import Path
4+
5+
import yt_dlp
6+
7+
from ytconverter.constants import URL_RE
8+
from ytconverter.utils import apply_style, get_download_path, sanitize
9+
10+
11+
def run():
12+
from ytconverter.config import load_local_version
13+
14+
print("\n" + apply_style("Enter the playlist URL you want to download as MP4:", "/cyan"))
15+
url = input(">> ").strip()
16+
if not URL_RE.match(url):
17+
print(apply_style("Invalid URL. Please enter a valid YouTube URL.", "/red/bold"))
18+
return
19+
20+
ydl_opts = {"quiet": True, "no_warnings": True}
21+
try:
22+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
23+
info = ydl.extract_info(url, download=False)
24+
except Exception as e:
25+
print(apply_style(f"Failed to fetch playlist info: {e}", "/red/bold"))
26+
return
27+
28+
playlist_title = info.get("title") or info.get("playlist_title") or "playlist"
29+
safe_playlist_title = sanitize(playlist_title)[:60]
30+
31+
format_map = {
32+
"1": "bestvideo[height>=1080]+bestaudio/best[height>=1080]",
33+
"2": "bestvideo[height<=1080]+bestaudio/best[height<=1080]",
34+
"3": "bestvideo[height<=720]+bestaudio/best[height<=720]",
35+
"4": "bestvideo[height<=480]+bestaudio/best[height<=480]",
36+
"5": "bestvideo[height<=360]+bestaudio/best[height<=360]",
37+
}
38+
39+
quality_title = """
40+
Select desired download quality for playlist:
41+
[1] >= 1080p Full HD+/4K
42+
[2] 1080p Full HD
43+
[3] 720p HD
44+
[4] 480p SD
45+
[5] <= 360p Low
46+
"""
47+
print(apply_style(quality_title, "/cyan/bold"))
48+
while True:
49+
qua_text = apply_style("Enter choice number (1-5): ", "/green/bold")
50+
choice = input(qua_text).strip()
51+
if choice in format_map:
52+
break
53+
print(apply_style("Invalid choice. Please select a number from 1 to 5.", "/red/bold"))
54+
55+
destination = Path(get_download_path("mp4"))
56+
playlist_folder = destination / safe_playlist_title
57+
playlist_folder.mkdir(parents=True, exist_ok=True)
58+
59+
ask_subs = input(apply_style("\nDownload subtitles for playlist videos? (y/n): ", "/cyan")).strip().lower()
60+
sub_flags = []
61+
cookie_path = ""
62+
if ask_subs == "y":
63+
print(apply_style("Note: YouTube may block subtitle extraction with HTTP 429. If that happens provide a cookies file.", "/yellow"))
64+
cookie_path = input(apply_style("Enter path to cookies file for yt-dlp (optional, press Enter to skip): ", "/green")).strip()
65+
lang = input(apply_style("Enter subtitle language code (e.g. en) or leave blank for all: ", "/green")).strip()
66+
pref_auto = input(apply_style("Also download automatic subtitles if manual not available? (y/n) [y]: ", "/green")).strip().lower()
67+
if pref_auto == "":
68+
pref_auto = "y"
69+
sub_flags += ["--write-sub"]
70+
if pref_auto == "y":
71+
sub_flags += ["--write-auto-sub"]
72+
if lang:
73+
sub_flags += ["--sub-lang", lang]
74+
sub_flags += ["--convert-subs", "srt"]
75+
if cookie_path:
76+
sub_flags += ["--cookies", cookie_path]
77+
78+
fmt = format_map[choice]
79+
out_template = str(playlist_folder / "%(playlist_index)03d - %(title)s.%(ext)s")
80+
81+
cmd = [
82+
"yt-dlp",
83+
"-i",
84+
"--yes-playlist",
85+
"-f", fmt,
86+
"-o", out_template,
87+
]
88+
if sub_flags:
89+
cmd += sub_flags
90+
91+
cmd += [url]
92+
93+
print(apply_style("\nStarting playlist download. This may take a while depending on size...\n", "/yellow/bold"))
94+
start = int(time.time())
95+
try:
96+
sp.run(cmd, check=True)
97+
except Exception as e:
98+
print(apply_style(f"An error occurred while downloading the playlist: {e}", "/red/bold"))
99+
return
100+
end = int(time.time())
101+
print(apply_style(f"\nPlaylist '{playlist_title}' downloaded to: {playlist_folder}", "/green/bold"))
102+
print(apply_style(f"Time taken: {end - start} sec", "/cyan"))

0 commit comments

Comments
 (0)