Última actividad 2 days ago

YouTube_MP3_Downloader.html Sin formato
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4"""
5YouTube MP3 Downloader v2.0
6Изисква: yt-dlp, ffmpeg
7Инсталация: pip install yt-dlp
8За ffmpeg: https://ffmpeg.org/download.html
9"""
10
11import os
12import sys
13import json
14import subprocess
15import platform
16from pathlib import Path
17from datetime import datetime
18from typing import Optional, Dict, Any
19
20# Проверка за Python версия
21if sys.version_info < (3, 7):
22 print("❌ Нужна е Python 3.7 или по-нова!")
23 sys.exit(1)
24
25# Опит за импорт на yt-dlp
26try:
27 import yt_dlp
28except ImportError:
29 print("❌ yt-dlp не е инсталиран!")
30 print("📦 Изпълни: pip install yt-dlp")
31 sys.exit(1)
32
33# ========== КЛАСОВЕ И КОНФИГУРАЦИЯ ==========
34
35class 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
47class 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
86class 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
388def 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
510if __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)