YouTube_MP3_Downloader.html
· 21 KiB · HTML
Sin formato
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
YouTube MP3 Downloader v2.0
Изисква: yt-dlp, ffmpeg
Инсталация: pip install yt-dlp
За ffmpeg: https://ffmpeg.org/download.html
"""
import os
import sys
import json
import subprocess
import platform
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, Any
# Проверка за Python версия
if sys.version_info < (3, 7):
print("❌ Нужна е Python 3.7 или по-нова!")
sys.exit(1)
# Опит за импорт на yt-dlp
try:
import yt_dlp
except ImportError:
print("❌ yt-dlp не е инсталиран!")
print("📦 Изпълни: pip install yt-dlp")
sys.exit(1)
# ========== КЛАСОВЕ И КОНФИГУРАЦИЯ ==========
class Colors:
"""Цветове за терминала"""
HEADER = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
RESET = '\033[0m'
BOLD = '\033[1m'
DIM = '\033[2m'
class Config:
"""Управление на конфигурацията"""
def __init__(self):
self.config_file = Path.home() / ".ytmp3downloader.json"
self.default_config = {
"quality": "320",
"output_dir": str(Path.home() / "Desktop" / "YouTube MP3"),
"embed_thumbnail": True,
"add_metadata": True,
"extract_audio": True,
"last_updated": datetime.now().isoformat()
}
self.load()
def load(self):
"""Зареждане на конфигурацията"""
if self.config_file.exists():
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
saved = json.load(f)
self.config = {**self.default_config, **saved}
except:
self.config = self.default_config.copy()
else:
self.config = self.default_config.copy()
def save(self):
"""Запазване на конфигурацията"""
self.config['last_updated'] = datetime.now().isoformat()
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(self.config, f, indent=2, ensure_ascii=False)
def get(self, key: str, default=None):
return self.config.get(key, default)
def set(self, key: str, value):
self.config[key] = value
self.save()
class YouTubeDownloader:
"""Основен клас за сваляне"""
def __init__(self):
self.config = Config()
self.check_dependencies()
@staticmethod
def clear_screen():
"""Изчистване на екрана"""
os.system('cls' if platform.system() == 'Windows' else 'clear')
@staticmethod
def print_header():
"""Показване на хедъра"""
YouTubeDownloader.clear_screen()
print(f"\n{Colors.RED} ╔══════════════════════════════════════════╗{Colors.RESET}")
print(f"{Colors.RED} ║ YouTube → MP3 Downloader v2.0 ║{Colors.RESET}")
print(f"{Colors.DIM} ║ powered by yt-dlp ║{Colors.RESET}")
print(f"{Colors.RED} ╚══════════════════════════════════════════╝{Colors.RESET}")
print("")
def print_ok(self, msg: str):
print(f" {Colors.GREEN}✓{Colors.RESET} {msg}")
def print_error(self, msg: str):
print(f" {Colors.RED}✗{Colors.RESET} {msg}")
def print_info(self, msg: str):
print(f" {Colors.CYAN}ℹ{Colors.RESET} {msg}")
def print_warning(self, msg: str):
print(f" {Colors.YELLOW}⚠{Colors.RESET} {msg}")
def print_step(self, msg: str):
print(f" {Colors.BLUE}→{Colors.RESET} {msg}")
def check_ffmpeg(self) -> bool:
"""Проверка за ffmpeg"""
try:
result = subprocess.run(
['ffmpeg', '-version'],
capture_output=True,
text=True,
creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == 'Windows' else 0
)
if result.returncode == 0:
version = result.stdout.split('\n')[0].replace('ffmpeg version', '').strip()
self.print_ok(f"ffmpeg {version}")
return True
except:
pass
self.print_error("ffmpeg не е намерен!")
self.print_warning("Изтегли от: https://ffmpeg.org/download.html")
return False
def check_dependencies(self):
"""Проверка на всички зависимости"""
self.print_info("Проверка на зависимости...")
# Проверка за yt-dlp (вече е импортнат)
try:
import yt_dlp
self.print_ok(f"yt-dlp {yt_dlp.version.__version__}")
except:
self.print_error("yt-dlp не е намерен!")
self.print_warning("Изпълни: pip install yt-dlp")
sys.exit(1)
# Проверка за ffmpeg
if not self.check_ffmpeg():
self.print_warning("Ще продължа, но някои функции няма да работят!")
print("")
def get_video_info(self, url: str) -> Optional[Dict[str, Any]]:
"""Взема информация за видеото"""
try:
ydl_opts = {
'quiet': True,
'no_warnings': True,
'extract_flat': False,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=False)
return info
except Exception as e:
self.print_error(f"Грешка при получаване на информация: {str(e)}")
return None
def select_quality(self) -> str:
"""Избор на качество"""
print(f" {Colors.BOLD}Качество на MP3:{Colors.RESET}")
print(f" 1) 320 kbps {Colors.DIM}(най-добро){Colors.RESET}")
print(f" 2) 192 kbps {Colors.DIM}(препоръчително){Colors.RESET}")
print(f" 3) 128 kbps {Colors.DIM}(компактно){Colors.RESET}")
print(f" {Colors.DIM}(Enter = {self.config.get('quality')} kbps){Colors.RESET}")
choice = input("\n Избор [1-3]: ").strip()
quality_map = {
"1": "320",
"2": "192",
"3": "128"
}
quality = quality_map.get(choice, self.config.get('quality'))
self.config.set('quality', quality)
return quality
def select_output_dir(self) -> str:
"""Избор на изходна папка"""
default = self.config.get('output_dir')
print(f" {Colors.BOLD}Папка за запис:{Colors.RESET}")
print(f" {Colors.DIM}(Enter = {default}){Colors.RESET}")
user_input = input(" Папка: ").strip().strip('"').strip("'")
if not user_input:
output_dir = default
else:
output_dir = user_input
# Създаване на папката ако не съществува
Path(output_dir).mkdir(parents=True, exist_ok=True)
self.config.set('output_dir', output_dir)
return output_dir
def download_single(self, url: str, quality: str, output_dir: str):
"""Сваляне на единично видео"""
# Валидация на URL
if 'youtube.com' not in url and 'youtu.be' not in url:
self.print_error("Невалиден YouTube URL!")
return
# Показване на информация
self.print_step("Получаване на информация...")
info = self.get_video_info(url)
if info:
duration_min = info.get('duration', 0) / 60
self.print_info(f"Заглавие: {info.get('title', 'N/A')[:60]}")
self.print_info(f"Продължителност: {duration_min:.1f} минути")
self.print_info(f"Качване: {info.get('upload_date', 'N/A')}")
print("")
# Конфигурация за сваляне
ydl_opts = {
'format': 'bestaudio/best',
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': quality,
}],
'outtmpl': str(Path(output_dir) / '%(title)s.%(ext)s'),
'quiet': False,
'no_warnings': False,
'progress_hooks': [self.progress_hook],
}
# Добавяне на thumbnail и metadata ако са включени
if self.config.get('embed_thumbnail'):
ydl_opts['postprocessors'].append({
'key': 'EmbedThumbnail',
'already_have_thumbnail': False,
})
if self.config.get('add_metadata'):
ydl_opts['postprocessors'].append({
'key': 'FFmpegMetadata',
})
try:
self.print_step("Стартиране на сваляне...")
self.print_step(f"Качество: {quality} kbps")
print(f"\n {Colors.DIM}─" * 50 + f"{Colors.RESET}")
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
print(f"\n {Colors.DIM}─" * 50 + f"{Colors.RESET}")
self.print_ok(f"Готово! Файлът е записан в: {output_dir}")
# Въпрос за отваряне на папката
open_folder = input("\n Да се отвори ли папката? (д/н) [н]: ").strip().lower()
if open_folder == 'д':
if platform.system() == 'Windows':
os.startfile(output_dir)
else:
subprocess.run(['open', output_dir] if platform.system() == 'Darwin' else ['xdg-open', output_dir])
except Exception as e:
self.print_error(f"Грешка при сваляне: {str(e)}")
def download_playlist(self, url: str, quality: str, output_dir: str):
"""Сваляне на плейлист"""
# Създаване на папка за плейлиста
playlist_dir = Path(output_dir) / "Playlists"
playlist_dir.mkdir(parents=True, exist_ok=True)
# Показване на информация за плейлиста
self.print_step("Получаване на информация за плейлиста...")
info = self.get_video_info(url)
if info and 'entries' in info:
self.print_info(f"Намерени {len(info['entries'])} видеа в плейлиста")
print("")
ydl_opts = {
'format': 'bestaudio/best',
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': quality,
}],
'outtmpl': str(playlist_dir / '%(playlist_index)s - %(title)s.%(ext)s'),
'quiet': False,
'no_warnings': False,
'ignoreerrors': True,
'extract_flat': False,
}
# Добавяне на thumbnail и metadata
if self.config.get('embed_thumbnail'):
ydl_opts['postprocessors'].append({
'key': 'EmbedThumbnail',
'already_have_thumbnail': False,
})
if self.config.get('add_metadata'):
ydl_opts['postprocessors'].append({
'key': 'FFmpegMetadata',
})
try:
self.print_step("Стартиране на сваляне на плейлиста...")
print(f"\n {Colors.DIM}─" * 50 + f"{Colors.RESET}")
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
print(f"\n {Colors.DIM}─" * 50 + f"{Colors.RESET}")
self.print_ok(f"Плейлистът е свален успешно в: {playlist_dir}")
except Exception as e:
self.print_error(f"Грешка при сваляне на плейлист: {str(e)}")
def progress_hook(self, d):
"""Hook за показване на прогреса"""
if d['status'] == 'downloading':
if 'total_bytes' in d:
percent = d['downloaded_bytes'] / d['total_bytes'] * 100
print(f"\r {Colors.CYAN}Сваляне: {percent:.1f}%{Colors.RESET}", end='')
elif d['status'] == 'finished':
print(f"\r {Colors.GREEN}Обработка на аудио...{Colors.RESET}")
def update_ytdlp(self):
"""Обновяване на yt-dlp"""
self.print_step("Обновяване на yt-dlp...")
try:
subprocess.run([sys.executable, '-m', 'pip', 'install', '--upgrade', 'yt-dlp'])
self.print_ok("yt-dlp е обновен успешно!")
except Exception as e:
self.print_error(f"Грешка при обновяване: {str(e)}")
def clear_cache(self):
"""Изчистване на кеша"""
self.print_step("Изчистване на кеша...")
cache_dir = Path.home() / ".cache" / "yt-dlp"
if cache_dir.exists():
import shutil
shutil.rmtree(cache_dir)
self.print_ok("Кешът е изчистен!")
else:
self.print_info("Няма намерен кеш")
def show_stats(self):
"""Показване на статистика"""
print(f"\n {Colors.BOLD}📊 Статистика:{Colors.RESET}")
print(f" Качество: {self.config.get('quality')} kbps")
print(f" Папка: {self.config.get('output_dir')}")
print(f" Thumbnail: {'Да' if self.config.get('embed_thumbnail') else 'Не'}")
print(f" Metadata: {'Да' if self.config.get('add_metadata') else 'Не'}")
print(f" Последна промяна: {self.config.get('last_updated')}")
# Броене на MP3 файловете
output_dir = Path(self.config.get('output_dir'))
if output_dir.exists():
mp3_files = list(output_dir.rglob("*.mp3"))
print(f" Общо MP3 файлове: {len(mp3_files)}")
def toggle_setting(self, setting: str):
"""Превключване на настройка"""
current = self.config.get(setting)
self.config.set(setting, not current)
self.print_ok(f"{setting} е {'включена' if not current else 'изключена'}")
# ========== MAIN MENU ==========
def main():
"""Основно меню"""
downloader = YouTubeDownloader()
while True:
downloader.print_header()
print(f" {Colors.DIM}═" * 50 + f"{Colors.RESET}")
print(f" {Colors.BOLD}МЕНЮ{Colors.RESET}")
print(f" {Colors.DIM}─" * 50 + f"{Colors.RESET}")
print(f" {Colors.CYAN}1){Colors.RESET} Свали едно видео като MP3")
print(f" {Colors.CYAN}2){Colors.RESET} Свали целия плейлист")
print(f" {Colors.CYAN}3){Colors.RESET} Само аудио (без thumbnail)")
print(f" {Colors.DIM}─" * 50 + f"{Colors.RESET}")
print(f" {Colors.YELLOW}4){Colors.RESET} Обнови yt-dlp")
print(f" {Colors.YELLOW}5){Colors.RESET} Настройки")
print(f" {Colors.YELLOW}6){Colors.RESET} Статистика")
print(f" {Colors.YELLOW}7){Colors.RESET} Изчисти кеша")
print(f" {Colors.DIM}─" * 50 + f"{Colors.RESET}")
print(f" {Colors.RED}0){Colors.RESET} Изход")
print(f" {Colors.DIM}═" * 50 + f"{Colors.RESET}")
choice = input("\n Избор [0-7]: ").strip()
if choice == "1":
print("")
url = input(" YouTube URL: ").strip()
if not url:
downloader.print_warning("Не е въведен URL.")
input("\n Натисни Enter за продължение...")
continue
quality = downloader.select_quality()
output_dir = downloader.select_output_dir()
downloader.download_single(url, quality, output_dir)
input("\n Натисни Enter за менюто...")
elif choice == "2":
print("")
url = input(" YouTube плейлист URL: ").strip()
if not url:
downloader.print_warning("Не е въведен URL.")
input("\n Натисни Enter за продължение...")
continue
quality = downloader.select_quality()
output_dir = downloader.select_output_dir()
downloader.download_playlist(url, quality, output_dir)
input("\n Натисни Enter за менюто...")
elif choice == "3":
print("")
url = input(" YouTube URL: ").strip()
if not url:
downloader.print_warning("Не е въведен URL.")
input("\n Натисни Enter за продължение...")
continue
quality = downloader.select_quality()
output_dir = downloader.select_output_dir()
# Временно изключване на thumbnail и metadata
old_thumb = downloader.config.get('embed_thumbnail')
old_meta = downloader.config.get('add_metadata')
downloader.config.set('embed_thumbnail', False)
downloader.config.set('add_metadata', False)
downloader.download_single(url, quality, output_dir)
# Възстановяване
downloader.config.set('embed_thumbnail', old_thumb)
downloader.config.set('add_metadata', old_meta)
input("\n Натисни Enter за менюто...")
elif choice == "4":
downloader.update_ytdlp()
input("\n Натисни Enter за менюто...")
elif choice == "5":
while True:
downloader.print_header()
print(f" {Colors.BOLD}⚙️ НАСТРОЙКИ{Colors.RESET}\n")
print(f" 1) Качество: {downloader.config.get('quality')} kbps")
print(f" 2) Папка: {downloader.config.get('output_dir')}")
print(f" 3) Thumbnail: {'✓' if downloader.config.get('embed_thumbnail') else '✗'}")
print(f" 4) Metadata: {'✓' if downloader.config.get('add_metadata') else '✗'}")
print(f" {Colors.DIM}─" * 50 + f"{Colors.RESET}")
print(f" 0) Назад")
sub_choice = input("\n Избор [0-4]: ").strip()
if sub_choice == "1":
downloader.select_quality()
elif sub_choice == "2":
downloader.select_output_dir()
elif sub_choice == "3":
downloader.toggle_setting('embed_thumbnail')
elif sub_choice == "4":
downloader.toggle_setting('add_metadata')
elif sub_choice == "0":
break
else:
downloader.print_warning("Невалиден избор!")
elif choice == "6":
downloader.show_stats()
input("\n Натисни Enter за менюто...")
elif choice == "7":
downloader.clear_cache()
input("\n Натисни Enter за менюто...")
elif choice == "0":
print("")
downloader.print_info("Довиждане!")
print("")
sys.exit(0)
else:
downloader.print_warning("Невалиден избор. Моля въведи 0-7.")
input("\n Натисни Enter за продължение...")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print(f"\n\n {Colors.YELLOW}⚠ Програмата е спряна от потребителя{Colors.RESET}")
sys.exit(0)
except Exception as e:
print(f"\n\n {Colors.RED}❌ Критична грешка: {str(e)}{Colors.RESET}")
input("\n Натисни Enter за изход...")
sys.exit(1)
| 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) |