urocibg zrewidował ten Gist . Przejdź do rewizji
1 file changed, 400 insertions
playlist_downloader.py(stworzono plik)
| @@ -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() | |
Nowsze
Starsze