Última actividad 4 days ago

urocibg revisó este gist 4 days ago. Ir a la revisión

2 files changed, 519 insertions

YouTube MP3 Downloader.py (archivo creado)

@@ -0,0 +1,519 @@
1 + #!/usr/bin/env python3
2 + # -*- coding: utf-8 -*-
3 +
4 + """
5 + YouTube MP3 Downloader v2.0
6 + Изисква: yt-dlp, ffmpeg
7 + Инсталация: pip install yt-dlp
8 + За ffmpeg: https://ffmpeg.org/download.html
9 + """
10 +
11 + import os
12 + import sys
13 + import json
14 + import subprocess
15 + import platform
16 + from pathlib import Path
17 + from datetime import datetime
18 + from typing import Optional, Dict, Any
19 +
20 + # Проверка за Python версия
21 + if sys.version_info < (3, 7):
22 + print("❌ Нужна е Python 3.7 или по-нова!")
23 + sys.exit(1)
24 +
25 + # Опит за импорт на yt-dlp
26 + try:
27 + import yt_dlp
28 + except ImportError:
29 + print("❌ yt-dlp не е инсталиран!")
30 + print("📦 Изпълни: pip install yt-dlp")
31 + sys.exit(1)
32 +
33 + # ========== КЛАСОВЕ И КОНФИГУРАЦИЯ ==========
34 +
35 + class Colors:
36 + """Цветове за терминала"""
37 + HEADER = '\033[95m'
38 + BLUE = '\033[94m'
39 + CYAN = '\033[96m'
40 + GREEN = '\033[92m'
41 + YELLOW = '\033[93m'
42 + RED = '\033[91m'
43 + RESET = '\033[0m'
44 + BOLD = '\033[1m'
45 + DIM = '\033[2m'
46 +
47 + class Config:
48 + """Управление на конфигурацията"""
49 + def __init__(self):
50 + self.config_file = Path.home() / ".ytmp3downloader.json"
51 + self.default_config = {
52 + "quality": "320",
53 + "output_dir": str(Path.home() / "Desktop" / "YouTube MP3"),
54 + "embed_thumbnail": True,
55 + "add_metadata": True,
56 + "extract_audio": True,
57 + "last_updated": datetime.now().isoformat()
58 + }
59 + self.load()
60 +
61 + def load(self):
62 + """Зареждане на конфигурацията"""
63 + if self.config_file.exists():
64 + try:
65 + with open(self.config_file, 'r', encoding='utf-8') as f:
66 + saved = json.load(f)
67 + self.config = {**self.default_config, **saved}
68 + except:
69 + self.config = self.default_config.copy()
70 + else:
71 + self.config = self.default_config.copy()
72 +
73 + def save(self):
74 + """Запазване на конфигурацията"""
75 + self.config['last_updated'] = datetime.now().isoformat()
76 + with open(self.config_file, 'w', encoding='utf-8') as f:
77 + json.dump(self.config, f, indent=2, ensure_ascii=False)
78 +
79 + def get(self, key: str, default=None):
80 + return self.config.get(key, default)
81 +
82 + def set(self, key: str, value):
83 + self.config[key] = value
84 + self.save()
85 +
86 + class YouTubeDownloader:
87 + """Основен клас за сваляне"""
88 +
89 + def __init__(self):
90 + self.config = Config()
91 + self.check_dependencies()
92 +
93 + @staticmethod
94 + def clear_screen():
95 + """Изчистване на екрана"""
96 + os.system('cls' if platform.system() == 'Windows' else 'clear')
97 +
98 + @staticmethod
99 + def print_header():
100 + """Показване на хедъра"""
101 + YouTubeDownloader.clear_screen()
102 + print(f"\n{Colors.RED} ╔══════════════════════════════════════════╗{Colors.RESET}")
103 + print(f"{Colors.RED} ║ YouTube → MP3 Downloader v2.0 ║{Colors.RESET}")
104 + print(f"{Colors.DIM} ║ powered by yt-dlp ║{Colors.RESET}")
105 + print(f"{Colors.RED} ╚══════════════════════════════════════════╝{Colors.RESET}")
106 + print("")
107 +
108 + def print_ok(self, msg: str):
109 + print(f" {Colors.GREEN}✓{Colors.RESET} {msg}")
110 +
111 + def print_error(self, msg: str):
112 + print(f" {Colors.RED}✗{Colors.RESET} {msg}")
113 +
114 + def print_info(self, msg: str):
115 + print(f" {Colors.CYAN}ℹ{Colors.RESET} {msg}")
116 +
117 + def print_warning(self, msg: str):
118 + print(f" {Colors.YELLOW}⚠{Colors.RESET} {msg}")
119 +
120 + def print_step(self, msg: str):
121 + print(f" {Colors.BLUE}→{Colors.RESET} {msg}")
122 +
123 + def check_ffmpeg(self) -> bool:
124 + """Проверка за ffmpeg"""
125 + try:
126 + result = subprocess.run(
127 + ['ffmpeg', '-version'],
128 + capture_output=True,
129 + text=True,
130 + creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == 'Windows' else 0
131 + )
132 + if result.returncode == 0:
133 + version = result.stdout.split('\n')[0].replace('ffmpeg version', '').strip()
134 + self.print_ok(f"ffmpeg {version}")
135 + return True
136 + except:
137 + pass
138 +
139 + self.print_error("ffmpeg не е намерен!")
140 + self.print_warning("Изтегли от: https://ffmpeg.org/download.html")
141 + return False
142 +
143 + def check_dependencies(self):
144 + """Проверка на всички зависимости"""
145 + self.print_info("Проверка на зависимости...")
146 +
147 + # Проверка за yt-dlp (вече е импортнат)
148 + try:
149 + import yt_dlp
150 + self.print_ok(f"yt-dlp {yt_dlp.version.__version__}")
151 + except:
152 + self.print_error("yt-dlp не е намерен!")
153 + self.print_warning("Изпълни: pip install yt-dlp")
154 + sys.exit(1)
155 +
156 + # Проверка за ffmpeg
157 + if not self.check_ffmpeg():
158 + self.print_warning("Ще продължа, но някои функции няма да работят!")
159 +
160 + print("")
161 +
162 + def get_video_info(self, url: str) -> Optional[Dict[str, Any]]:
163 + """Взема информация за видеото"""
164 + try:
165 + ydl_opts = {
166 + 'quiet': True,
167 + 'no_warnings': True,
168 + 'extract_flat': False,
169 + }
170 +
171 + with yt_dlp.YoutubeDL(ydl_opts) as ydl:
172 + info = ydl.extract_info(url, download=False)
173 + return info
174 + except Exception as e:
175 + self.print_error(f"Грешка при получаване на информация: {str(e)}")
176 + return None
177 +
178 + def select_quality(self) -> str:
179 + """Избор на качество"""
180 + print(f" {Colors.BOLD}Качество на MP3:{Colors.RESET}")
181 + print(f" 1) 320 kbps {Colors.DIM}(най-добро){Colors.RESET}")
182 + print(f" 2) 192 kbps {Colors.DIM}(препоръчително){Colors.RESET}")
183 + print(f" 3) 128 kbps {Colors.DIM}(компактно){Colors.RESET}")
184 + print(f" {Colors.DIM}(Enter = {self.config.get('quality')} kbps){Colors.RESET}")
185 +
186 + choice = input("\n Избор [1-3]: ").strip()
187 +
188 + quality_map = {
189 + "1": "320",
190 + "2": "192",
191 + "3": "128"
192 + }
193 +
194 + quality = quality_map.get(choice, self.config.get('quality'))
195 + self.config.set('quality', quality)
196 + return quality
197 +
198 + def select_output_dir(self) -> str:
199 + """Избор на изходна папка"""
200 + default = self.config.get('output_dir')
201 + print(f" {Colors.BOLD}Папка за запис:{Colors.RESET}")
202 + print(f" {Colors.DIM}(Enter = {default}){Colors.RESET}")
203 +
204 + user_input = input(" Папка: ").strip().strip('"').strip("'")
205 +
206 + if not user_input:
207 + output_dir = default
208 + else:
209 + output_dir = user_input
210 +
211 + # Създаване на папката ако не съществува
212 + Path(output_dir).mkdir(parents=True, exist_ok=True)
213 +
214 + self.config.set('output_dir', output_dir)
215 + return output_dir
216 +
217 + def download_single(self, url: str, quality: str, output_dir: str):
218 + """Сваляне на единично видео"""
219 + # Валидация на URL
220 + if 'youtube.com' not in url and 'youtu.be' not in url:
221 + self.print_error("Невалиден YouTube URL!")
222 + return
223 +
224 + # Показване на информация
225 + self.print_step("Получаване на информация...")
226 + info = self.get_video_info(url)
227 +
228 + if info:
229 + duration_min = info.get('duration', 0) / 60
230 + self.print_info(f"Заглавие: {info.get('title', 'N/A')[:60]}")
231 + self.print_info(f"Продължителност: {duration_min:.1f} минути")
232 + self.print_info(f"Качване: {info.get('upload_date', 'N/A')}")
233 + print("")
234 +
235 + # Конфигурация за сваляне
236 + ydl_opts = {
237 + 'format': 'bestaudio/best',
238 + 'postprocessors': [{
239 + 'key': 'FFmpegExtractAudio',
240 + 'preferredcodec': 'mp3',
241 + 'preferredquality': quality,
242 + }],
243 + 'outtmpl': str(Path(output_dir) / '%(title)s.%(ext)s'),
244 + 'quiet': False,
245 + 'no_warnings': False,
246 + 'progress_hooks': [self.progress_hook],
247 + }
248 +
249 + # Добавяне на thumbnail и metadata ако са включени
250 + if self.config.get('embed_thumbnail'):
251 + ydl_opts['postprocessors'].append({
252 + 'key': 'EmbedThumbnail',
253 + 'already_have_thumbnail': False,
254 + })
255 +
256 + if self.config.get('add_metadata'):
257 + ydl_opts['postprocessors'].append({
258 + 'key': 'FFmpegMetadata',
259 + })
260 +
261 + try:
262 + self.print_step("Стартиране на сваляне...")
263 + self.print_step(f"Качество: {quality} kbps")
264 + print(f"\n {Colors.DIM}─" * 50 + f"{Colors.RESET}")
265 +
266 + with yt_dlp.YoutubeDL(ydl_opts) as ydl:
267 + ydl.download([url])
268 +
269 + print(f"\n {Colors.DIM}─" * 50 + f"{Colors.RESET}")
270 + self.print_ok(f"Готово! Файлът е записан в: {output_dir}")
271 +
272 + # Въпрос за отваряне на папката
273 + open_folder = input("\n Да се отвори ли папката? (д/н) [н]: ").strip().lower()
274 + if open_folder == 'д':
275 + if platform.system() == 'Windows':
276 + os.startfile(output_dir)
277 + else:
278 + subprocess.run(['open', output_dir] if platform.system() == 'Darwin' else ['xdg-open', output_dir])
279 +
280 + except Exception as e:
281 + self.print_error(f"Грешка при сваляне: {str(e)}")
282 +
283 + def download_playlist(self, url: str, quality: str, output_dir: str):
284 + """Сваляне на плейлист"""
285 + # Създаване на папка за плейлиста
286 + playlist_dir = Path(output_dir) / "Playlists"
287 + playlist_dir.mkdir(parents=True, exist_ok=True)
288 +
289 + # Показване на информация за плейлиста
290 + self.print_step("Получаване на информация за плейлиста...")
291 + info = self.get_video_info(url)
292 +
293 + if info and 'entries' in info:
294 + self.print_info(f"Намерени {len(info['entries'])} видеа в плейлиста")
295 + print("")
296 +
297 + ydl_opts = {
298 + 'format': 'bestaudio/best',
299 + 'postprocessors': [{
300 + 'key': 'FFmpegExtractAudio',
301 + 'preferredcodec': 'mp3',
302 + 'preferredquality': quality,
303 + }],
304 + 'outtmpl': str(playlist_dir / '%(playlist_index)s - %(title)s.%(ext)s'),
305 + 'quiet': False,
306 + 'no_warnings': False,
307 + 'ignoreerrors': True,
308 + 'extract_flat': False,
309 + }
310 +
311 + # Добавяне на thumbnail и metadata
312 + if self.config.get('embed_thumbnail'):
313 + ydl_opts['postprocessors'].append({
314 + 'key': 'EmbedThumbnail',
315 + 'already_have_thumbnail': False,
316 + })
317 +
318 + if self.config.get('add_metadata'):
319 + ydl_opts['postprocessors'].append({
320 + 'key': 'FFmpegMetadata',
321 + })
322 +
323 + try:
324 + self.print_step("Стартиране на сваляне на плейлиста...")
325 + print(f"\n {Colors.DIM}─" * 50 + f"{Colors.RESET}")
326 +
327 + with yt_dlp.YoutubeDL(ydl_opts) as ydl:
328 + ydl.download([url])
329 +
330 + print(f"\n {Colors.DIM}─" * 50 + f"{Colors.RESET}")
331 + self.print_ok(f"Плейлистът е свален успешно в: {playlist_dir}")
332 +
333 + except Exception as e:
334 + self.print_error(f"Грешка при сваляне на плейлист: {str(e)}")
335 +
336 + def progress_hook(self, d):
337 + """Hook за показване на прогреса"""
338 + if d['status'] == 'downloading':
339 + if 'total_bytes' in d:
340 + percent = d['downloaded_bytes'] / d['total_bytes'] * 100
341 + print(f"\r {Colors.CYAN}Сваляне: {percent:.1f}%{Colors.RESET}", end='')
342 + elif d['status'] == 'finished':
343 + print(f"\r {Colors.GREEN}Обработка на аудио...{Colors.RESET}")
344 +
345 + def update_ytdlp(self):
346 + """Обновяване на yt-dlp"""
347 + self.print_step("Обновяване на yt-dlp...")
348 + try:
349 + subprocess.run([sys.executable, '-m', 'pip', 'install', '--upgrade', 'yt-dlp'])
350 + self.print_ok("yt-dlp е обновен успешно!")
351 + except Exception as e:
352 + self.print_error(f"Грешка при обновяване: {str(e)}")
353 +
354 + def clear_cache(self):
355 + """Изчистване на кеша"""
356 + self.print_step("Изчистване на кеша...")
357 + cache_dir = Path.home() / ".cache" / "yt-dlp"
358 + if cache_dir.exists():
359 + import shutil
360 + shutil.rmtree(cache_dir)
361 + self.print_ok("Кешът е изчистен!")
362 + else:
363 + self.print_info("Няма намерен кеш")
364 +
365 + def show_stats(self):
366 + """Показване на статистика"""
367 + print(f"\n {Colors.BOLD}📊 Статистика:{Colors.RESET}")
368 + print(f" Качество: {self.config.get('quality')} kbps")
369 + print(f" Папка: {self.config.get('output_dir')}")
370 + print(f" Thumbnail: {'Да' if self.config.get('embed_thumbnail') else 'Не'}")
371 + print(f" Metadata: {'Да' if self.config.get('add_metadata') else 'Не'}")
372 + print(f" Последна промяна: {self.config.get('last_updated')}")
373 +
374 + # Броене на MP3 файловете
375 + output_dir = Path(self.config.get('output_dir'))
376 + if output_dir.exists():
377 + mp3_files = list(output_dir.rglob("*.mp3"))
378 + print(f" Общо MP3 файлове: {len(mp3_files)}")
379 +
380 + def toggle_setting(self, setting: str):
381 + """Превключване на настройка"""
382 + current = self.config.get(setting)
383 + self.config.set(setting, not current)
384 + self.print_ok(f"{setting} е {'включена' if not current else 'изключена'}")
385 +
386 + # ========== MAIN MENU ==========
387 +
388 + def main():
389 + """Основно меню"""
390 + downloader = YouTubeDownloader()
391 +
392 + while True:
393 + downloader.print_header()
394 +
395 + print(f" {Colors.DIM}═" * 50 + f"{Colors.RESET}")
396 + print(f" {Colors.BOLD}МЕНЮ{Colors.RESET}")
397 + print(f" {Colors.DIM}─" * 50 + f"{Colors.RESET}")
398 + print(f" {Colors.CYAN}1){Colors.RESET} Свали едно видео като MP3")
399 + print(f" {Colors.CYAN}2){Colors.RESET} Свали целия плейлист")
400 + print(f" {Colors.CYAN}3){Colors.RESET} Само аудио (без thumbnail)")
401 + print(f" {Colors.DIM}─" * 50 + f"{Colors.RESET}")
402 + print(f" {Colors.YELLOW}4){Colors.RESET} Обнови yt-dlp")
403 + print(f" {Colors.YELLOW}5){Colors.RESET} Настройки")
404 + print(f" {Colors.YELLOW}6){Colors.RESET} Статистика")
405 + print(f" {Colors.YELLOW}7){Colors.RESET} Изчисти кеша")
406 + print(f" {Colors.DIM}─" * 50 + f"{Colors.RESET}")
407 + print(f" {Colors.RED}0){Colors.RESET} Изход")
408 + print(f" {Colors.DIM}═" * 50 + f"{Colors.RESET}")
409 +
410 + choice = input("\n Избор [0-7]: ").strip()
411 +
412 + if choice == "1":
413 + print("")
414 + url = input(" YouTube URL: ").strip()
415 + if not url:
416 + downloader.print_warning("Не е въведен URL.")
417 + input("\n Натисни Enter за продължение...")
418 + continue
419 +
420 + quality = downloader.select_quality()
421 + output_dir = downloader.select_output_dir()
422 + downloader.download_single(url, quality, output_dir)
423 + input("\n Натисни Enter за менюто...")
424 +
425 + elif choice == "2":
426 + print("")
427 + url = input(" YouTube плейлист URL: ").strip()
428 + if not url:
429 + downloader.print_warning("Не е въведен URL.")
430 + input("\n Натисни Enter за продължение...")
431 + continue
432 +
433 + quality = downloader.select_quality()
434 + output_dir = downloader.select_output_dir()
435 + downloader.download_playlist(url, quality, output_dir)
436 + input("\n Натисни Enter за менюто...")
437 +
438 + elif choice == "3":
439 + print("")
440 + url = input(" YouTube URL: ").strip()
441 + if not url:
442 + downloader.print_warning("Не е въведен URL.")
443 + input("\n Натисни Enter за продължение...")
444 + continue
445 +
446 + quality = downloader.select_quality()
447 + output_dir = downloader.select_output_dir()
448 +
449 + # Временно изключване на thumbnail и metadata
450 + old_thumb = downloader.config.get('embed_thumbnail')
451 + old_meta = downloader.config.get('add_metadata')
452 + downloader.config.set('embed_thumbnail', False)
453 + downloader.config.set('add_metadata', False)
454 +
455 + downloader.download_single(url, quality, output_dir)
456 +
457 + # Възстановяване
458 + downloader.config.set('embed_thumbnail', old_thumb)
459 + downloader.config.set('add_metadata', old_meta)
460 + input("\n Натисни Enter за менюто...")
461 +
462 + elif choice == "4":
463 + downloader.update_ytdlp()
464 + input("\n Натисни Enter за менюто...")
465 +
466 + elif choice == "5":
467 + while True:
468 + downloader.print_header()
469 + print(f" {Colors.BOLD}⚙️ НАСТРОЙКИ{Colors.RESET}\n")
470 + print(f" 1) Качество: {downloader.config.get('quality')} kbps")
471 + print(f" 2) Папка: {downloader.config.get('output_dir')}")
472 + print(f" 3) Thumbnail: {'✓' if downloader.config.get('embed_thumbnail') else '✗'}")
473 + print(f" 4) Metadata: {'✓' if downloader.config.get('add_metadata') else '✗'}")
474 + print(f" {Colors.DIM}─" * 50 + f"{Colors.RESET}")
475 + print(f" 0) Назад")
476 +
477 + sub_choice = input("\n Избор [0-4]: ").strip()
478 +
479 + if sub_choice == "1":
480 + downloader.select_quality()
481 + elif sub_choice == "2":
482 + downloader.select_output_dir()
483 + elif sub_choice == "3":
484 + downloader.toggle_setting('embed_thumbnail')
485 + elif sub_choice == "4":
486 + downloader.toggle_setting('add_metadata')
487 + elif sub_choice == "0":
488 + break
489 + else:
490 + downloader.print_warning("Невалиден избор!")
491 +
492 + elif choice == "6":
493 + downloader.show_stats()
494 + input("\n Натисни Enter за менюто...")
495 +
496 + elif choice == "7":
497 + downloader.clear_cache()
498 + input("\n Натисни Enter за менюто...")
499 +
500 + elif choice == "0":
501 + print("")
502 + downloader.print_info("Довиждане!")
503 + print("")
504 + sys.exit(0)
505 +
506 + else:
507 + downloader.print_warning("Невалиден избор. Моля въведи 0-7.")
508 + input("\n Натисни Enter за продължение...")
509 +
510 + if __name__ == "__main__":
511 + try:
512 + main()
513 + except KeyboardInterrupt:
514 + print(f"\n\n {Colors.YELLOW}⚠ Програмата е спряна от потребителя{Colors.RESET}")
515 + sys.exit(0)
516 + except Exception as e:
517 + print(f"\n\n {Colors.RED}❌ Критична грешка: {str(e)}{Colors.RESET}")
518 + input("\n Натисни Enter за изход...")
519 + sys.exit(1)
Siguiente Anterior