Last active 1754707381

Това приложение ви позволява лесно да сваляте музикални плейлисти от Spotify и YouTube

playlist_downloader.py Raw
1import os
2import sys
3import tkinter as tk
4from tkinter import filedialog, messagebox, ttk
5import webbrowser
6import subprocess
7import threading
8from pathlib import Path
9
10class PlaylistDownloader:
11 def __init__(self):
12 self.root = tk.Tk()
13
14 # Инициализиране на променливите ПРЕДИ setup_ui()
15 self.progress_var = tk.DoubleVar()
16 self.status_var = tk.StringVar(value="Готов")
17 self.playlist_file_path = tk.StringVar()
18 self.spotify_link = tk.StringVar()
19 self.folder_path = tk.StringVar()
20 self.youtube_video_link = tk.StringVar()
21 self.audio_quality = tk.StringVar(value="best")
22 self.video_quality = tk.StringVar(value="best")
23
24 self.setup_ui()
25
26 def setup_ui(self):
27 self.root.title("🎵 Spotify & YouTube Playlist Downloader v2.0")
28 self.root.geometry("850x500")
29 self.root.resizable(False, False)
30
31 # Центриране на прозореца
32 self.root.eval('tk::PlaceWindow . center')
33
34 # Цветова схема
35 bg_color = "#f0f0f0"
36 button_color = "#4a90e2"
37 success_color = "#5cb85c"
38 info_color = "#5bc0de"
39 warning_color = "#f0ad4e"
40
41 self.root.configure(bg=bg_color)
42
43 # Основна рамка с padding
44 main_frame = tk.Frame(self.root, bg=bg_color, padx=20, pady=20)
45 main_frame.pack(fill=tk.BOTH, expand=True)
46
47 # Заглавие
48 title_label = tk.Label(main_frame, text="🎵 Playlist Downloader",
49 font=("Arial", 16, "bold"), bg=bg_color, fg="#333")
50 title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
51
52 row = 1
53
54 # Файл с песни
55 tk.Label(main_frame, text="📄 Текстов файл с имената на песните:",
56 bg=bg_color, font=("Arial", 10)).grid(
57 row=row, column=0, padx=10, pady=5, sticky="w")
58 tk.Entry(main_frame, textvariable=self.playlist_file_path, width=60,
59 state='readonly', font=("Arial", 9)).grid(row=row, column=1, padx=5, pady=5)
60 tk.Button(main_frame, text="Избери", command=self.select_playlist_file,
61 bg=button_color, fg="white", font=("Arial", 9),
62 relief=tk.FLAT, padx=10).grid(row=row, column=2, padx=5, pady=5)
63 row += 1
64
65 # Spotify линк
66 tk.Label(main_frame, text="🎧 Spotify плейлист линк:",
67 bg=bg_color, font=("Arial", 10)).grid(
68 row=row, column=0, padx=10, pady=5, sticky="w")
69 spotify_entry = tk.Entry(main_frame, textvariable=self.spotify_link, width=60,
70 font=("Arial", 9))
71 spotify_entry.grid(row=row, column=1, padx=5, pady=5)
72 tk.Button(main_frame, text="Постави",
73 command=lambda: self.paste_from_clipboard(self.spotify_link),
74 bg=button_color, fg="white", font=("Arial", 9),
75 relief=tk.FLAT, padx=10).grid(row=row, column=2, padx=5, pady=5)
76 row += 1
77
78 # YouTube видео линк
79 tk.Label(main_frame, text="📹 YouTube видео линк:",
80 bg=bg_color, font=("Arial", 10)).grid(
81 row=row, column=0, padx=10, pady=5, sticky="w")
82 youtube_entry = tk.Entry(main_frame, textvariable=self.youtube_video_link, width=60,
83 font=("Arial", 9))
84 youtube_entry.grid(row=row, column=1, padx=5, pady=5)
85 tk.Button(main_frame, text="Постави",
86 command=lambda: self.paste_from_clipboard(self.youtube_video_link),
87 bg=button_color, fg="white", font=("Arial", 9),
88 relief=tk.FLAT, padx=10).grid(row=row, column=2, padx=5, pady=5)
89 row += 1
90
91 # Папка за запазване
92 tk.Label(main_frame, text="📁 Папка за запазване:",
93 bg=bg_color, font=("Arial", 10)).grid(
94 row=row, column=0, padx=10, pady=5, sticky="w")
95 tk.Entry(main_frame, textvariable=self.folder_path, width=60,
96 font=("Arial", 9)).grid(row=row, column=1, padx=5, pady=5)
97 tk.Button(main_frame, text="Избери", command=self.select_folder,
98 bg=button_color, fg="white", font=("Arial", 9),
99 relief=tk.FLAT, padx=10).grid(row=row, column=2, padx=5, pady=5)
100 row += 1
101
102 # Качество на аудио
103 tk.Label(main_frame, text="🎵 Качество на аудиото:",
104 bg=bg_color, font=("Arial", 10)).grid(
105 row=row, column=0, padx=10, pady=5, sticky="w")
106 audio_quality_combo = ttk.Combobox(main_frame, textvariable=self.audio_quality,
107 values=["best", "320k", "256k", "128k"],
108 state="readonly", width=57, font=("Arial", 9))
109 audio_quality_combo.grid(row=row, column=1, padx=5, pady=5, columnspan=2, sticky="w")
110 row += 1
111
112 # Качество на видео
113 tk.Label(main_frame, text="📹 Качество на видеото:",
114 bg=bg_color, font=("Arial", 10)).grid(
115 row=row, column=0, padx=10, pady=5, sticky="w")
116 video_quality_combo = ttk.Combobox(main_frame, textvariable=self.video_quality,
117 values=["best", "1080p", "720p", "480p"],
118 state="readonly", width=57, font=("Arial", 9))
119 video_quality_combo.grid(row=row, column=1, padx=5, pady=5, columnspan=2, sticky="w")
120 row += 1
121
122 # Прогрес бар
123 tk.Label(main_frame, text="Прогрес:", bg=bg_color, font=("Arial", 10)).grid(
124 row=row, column=0, padx=10, pady=(20, 5), sticky="w")
125 self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var,
126 length=500, mode='determinate')
127 self.progress_bar.grid(row=row, column=1, columnspan=2, padx=5, pady=(20, 5), sticky="ew")
128 row += 1
129
130 # Статус
131 self.status_label = tk.Label(main_frame, textvariable=self.status_var,
132 bg=bg_color, font=("Arial", 9), fg="#666")
133 self.status_label.grid(row=row, column=0, columnspan=3, pady=5)
134 row += 1
135
136 # Бутони за сваляне
137 button_frame = tk.Frame(main_frame, bg=bg_color)
138 button_frame.grid(row=row, column=0, columnspan=3, pady=20)
139
140 tk.Button(button_frame, text="🎧 Свали от Spotify",
141 command=self.download_spotify_playlist,
142 bg=success_color, fg="white", font=("Arial", 10, "bold"),
143 relief=tk.FLAT, padx=15, pady=8, width=22).pack(side=tk.LEFT, padx=5)
144
145 tk.Button(button_frame, text="🎵 Свали MP3 от YouTube",
146 command=self.download_from_youtube,
147 bg=info_color, fg="white", font=("Arial", 10, "bold"),
148 relief=tk.FLAT, padx=15, pady=8, width=22).pack(side=tk.LEFT, padx=5)
149
150 tk.Button(button_frame, text="📹 Свали YouTube видео",
151 command=self.download_youtube_video,
152 bg=warning_color, fg="white", font=("Arial", 10, "bold"),
153 relief=tk.FLAT, padx=15, pady=8, width=22).pack(side=tk.LEFT, padx=5)
154 row += 1
155
156 # Линк към сайт
157 link_button = tk.Button(main_frame, text="🌐 Посетете сайта на приложението",
158 command=self.open_link, bg=bg_color, fg=button_color,
159 font=("Arial", 9, "underline"), relief=tk.FLAT,
160 cursor="hand2")
161 link_button.grid(row=row, column=0, columnspan=3, pady=10)
162
163 def select_playlist_file(self):
164 filename = filedialog.askopenfilename(
165 title="Изберете текстов файл",
166 filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
167 )
168 if filename:
169 self.playlist_file_path.set(filename)
170
171 def select_folder(self):
172 folder = filedialog.askdirectory(title="Изберете папка за запазване")
173 if folder:
174 self.folder_path.set(folder)
175
176 def paste_from_clipboard(self, var):
177 try:
178 clipboard_content = self.root.clipboard_get()
179 var.set(clipboard_content)
180 except tk.TkError:
181 messagebox.showwarning("Предупреждение", "Няма данни в клипборда")
182
183 def validate_url(self, url, platform):
184 if platform == "spotify":
185 return "spotify.com" in url and "playlist" in url
186 elif platform == "youtube":
187 return any(domain in url for domain in ["youtube.com", "youtu.be"])
188 return False
189
190 def check_dependencies(self):
191 """Проверява дали са инсталирани необходимите инструменти"""
192 missing = []
193
194 # Проверка за spotdl
195 try:
196 result = subprocess.run(['spotdl', '--version'],
197 capture_output=True, text=True, timeout=5)
198 if result.returncode != 0:
199 missing.append("spotdl")
200 except (subprocess.TimeoutExpired, FileNotFoundError):
201 missing.append("spotdl")
202
203 # Проверка за yt-dlp
204 try:
205 result = subprocess.run(['yt-dlp', '--version'],
206 capture_output=True, text=True, timeout=5)
207 if result.returncode != 0:
208 missing.append("yt-dlp")
209 except (subprocess.TimeoutExpired, FileNotFoundError):
210 missing.append("yt-dlp")
211
212 if missing:
213 messagebox.showerror(
214 "Грешка",
215 f"Липсват необходимите инструменти: {', '.join(missing)}\n\n"
216 f"Моля инсталирайте ги с:\n"
217 f"pip install spotdl yt-dlp"
218 )
219 return False
220 return True
221
222 def run_download(self, func, *args):
223 """Стартира сваляне в отделен thread"""
224 if not self.check_dependencies():
225 return
226
227 thread = threading.Thread(target=func, args=args)
228 thread.daemon = True
229 thread.start()
230
231 def update_progress(self, value, status):
232 """Обновява прогрес бара и статуса"""
233 self.progress_var.set(value)
234 self.status_var.set(status)
235 self.root.update_idletasks()
236
237 def download_spotify_playlist(self):
238 self.run_download(self._download_spotify_playlist)
239
240 def _download_spotify_playlist(self):
241 playlist_link = self.spotify_link.get().strip()
242 output_folder = self.folder_path.get().strip()
243
244 if not playlist_link:
245 messagebox.showerror("Грешка", "Моля, въведете линк към Spotify плейлист.")
246 return
247
248 if not self.validate_url(playlist_link, "spotify"):
249 messagebox.showerror("Грешка", "Невалиден Spotify плейлист линк.")
250 return
251
252 if not output_folder:
253 messagebox.showerror("Грешка", "Моля, изберете папка за запазване.")
254 return
255
256 try:
257 self.update_progress(10, "Започва сваляне от Spotify...")
258
259 # Създаване на папката ако не съществува
260 Path(output_folder).mkdir(parents=True, exist_ok=True)
261
262 # Команда за spotdl
263 quality_param = f"--audio-bitrate {self.audio_quality.get()}" if self.audio_quality.get() != "best" else ""
264 cmd = f'spotdl "{playlist_link}" --output "{output_folder}" {quality_param}'
265
266 self.update_progress(30, "Сваляне в процес...")
267
268 process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
269 stderr=subprocess.STDOUT, text=True, cwd=output_folder)
270
271 while True:
272 output = process.stdout.readline()
273 if output == '' and process.poll() is not None:
274 break
275 if output:
276 self.update_progress(50, f"Сваляне: {output.strip()[:50]}...")
277
278 if process.returncode == 0:
279 self.update_progress(100, "Плейлистът е успешно свален!")
280 messagebox.showinfo("Успех", "Плейлистът беше успешно свален.")
281 else:
282 raise Exception("Грешка при изпълнение на spotdl")
283
284 except Exception as e:
285 self.update_progress(0, "Грешка при свалянето")
286 messagebox.showerror("Грешка", f"Грешка при свалянето: {str(e)}")
287
288 def download_from_youtube(self):
289 self.run_download(self._download_from_youtube)
290
291 def _download_from_youtube(self):
292 playlist_file = self.playlist_file_path.get().strip()
293 output_folder = self.folder_path.get().strip()
294
295 if not playlist_file:
296 messagebox.showerror("Грешка", "Моля, изберете текстов файл с имената на песните.")
297 return
298 if not output_folder:
299 messagebox.showerror("Грешка", "Моля, изберете папка за запазване.")
300 return
301
302 try:
303 self.update_progress(5, "Четене на файла с песни...")
304
305 with open(playlist_file, 'r', encoding='utf-8') as file:
306 songs = [line.strip() for line in file.readlines() if line.strip()]
307
308 if not songs:
309 messagebox.showerror("Грешка", "Файлът е празен или не съдържа валидни песни.")
310 return
311
312 Path(output_folder).mkdir(parents=True, exist_ok=True)
313 total_songs = len(songs)
314
315 for i, song in enumerate(songs):
316 progress = (i / total_songs) * 90 + 10
317 self.update_progress(progress, f"Сваляне: {song[:40]}...")
318
319 # Качество на аудиото
320 quality_param = ""
321 if self.audio_quality.get() != "best":
322 quality_param = f"--audio-quality {self.audio_quality.get()}"
323
324 cmd = f'yt-dlp --extract-audio --audio-format mp3 {quality_param} -o "%(title)s.%(ext)s" "ytsearch:{song}"'
325
326 result = subprocess.run(cmd, shell=True, cwd=output_folder,
327 capture_output=True, text=True)
328
329 if result.returncode != 0:
330 print(f"Грешка при {song}: {result.stderr}")
331
332 self.update_progress(100, f"Свалени {total_songs} песни!")
333 messagebox.showinfo("Успех", f"Всички {total_songs} песни бяха успешно свалени.")
334
335 except Exception as e:
336 self.update_progress(0, "Грешка при свалянето")
337 messagebox.showerror("Грешка", f"Грешка при свалянето: {str(e)}")
338
339 def download_youtube_video(self):
340 self.run_download(self._download_youtube_video)
341
342 def _download_youtube_video(self):
343 video_link = self.youtube_video_link.get().strip()
344 output_folder = self.folder_path.get().strip()
345
346 if not video_link:
347 messagebox.showerror("Грешка", "Моля, въведете линк към YouTube видеото.")
348 return
349
350 if not self.validate_url(video_link, "youtube"):
351 messagebox.showerror("Грешка", "Невалиден YouTube линк.")
352 return
353
354 if not output_folder:
355 messagebox.showerror("Грешка", "Моля, изберете папка за запазване.")
356 return
357
358 try:
359 self.update_progress(10, "Започва сваляне на видеото...")
360
361 Path(output_folder).mkdir(parents=True, exist_ok=True)
362
363 # Качество на видеото
364 format_param = "best"
365 if self.video_quality.get() != "best":
366 format_param = f"best[height<={self.video_quality.get()[:-1]}]"
367
368 cmd = f'yt-dlp -f "{format_param}" -o "%(title)s.%(ext)s" "{video_link}"'
369
370 self.update_progress(30, "Сваляне в процес...")
371
372 process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
373 stderr=subprocess.STDOUT, text=True, cwd=output_folder)
374
375 while True:
376 output = process.stdout.readline()
377 if output == '' and process.poll() is not None:
378 break
379 if output:
380 self.update_progress(50, f"Сваляне: {output.strip()[:50]}...")
381
382 if process.returncode == 0:
383 self.update_progress(100, "Видеото е успешно свалено!")
384 messagebox.showinfo("Успех", "Видеото беше успешно свалено.")
385 else:
386 raise Exception("Грешка при изпълнение на yt-dlp")
387
388 except Exception as e:
389 self.update_progress(0, "Грешка при свалянето")
390 messagebox.showerror("Грешка", f"Грешка при свалянето: {str(e)}")
391
392 def open_link(self):
393 webbrowser.open("https://urocibg.eu/%f0%9f%8e%b5-spotify-%d0%b8-youtube-playlist-downloader/")
394
395 def run(self):
396 self.root.mainloop()
397
398if __name__ == "__main__":
399 app = PlaylistDownloader()
400 app.run()