urocibg 修订了这个 Gist . 转到此修订
1 file changed, 400 insertions
playlist_downloader.py(文件已创建)
@@ -0,0 +1,400 @@ | |||
1 | + | import os | |
2 | + | import sys | |
3 | + | import tkinter as tk | |
4 | + | from tkinter import filedialog, messagebox, ttk | |
5 | + | import webbrowser | |
6 | + | import subprocess | |
7 | + | import threading | |
8 | + | from pathlib import Path | |
9 | + | ||
10 | + | class 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 | + | ||
398 | + | if __name__ == "__main__": | |
399 | + | app = PlaylistDownloader() | |
400 | + | app.run() |
上一页
下一页