urocibg a révisé ce gist 2 days ago. Aller à la révision
1 file changed, 508 insertions, 340 deletions
YouTube_MP3_Downloader.html
| @@ -1,351 +1,519 @@ | |||
| 1 | - | <!DOCTYPE html> | |
| 2 | - | <html lang="bg"> | |
| 3 | - | <head> | |
| 4 | - | <meta charset="UTF-8"> | |
| 5 | - | <title>YouTube MP3 Downloader</title> | |
| 6 | - | <style> | |
| 7 | - | * { box-sizing: border-box; margin: 0; padding: 0; } | |
| 8 | - | body { | |
| 9 | - | font-family: 'Segoe UI', sans-serif; | |
| 10 | - | background: #0f0f0f; | |
| 11 | - | color: #e0e0e0; | |
| 12 | - | min-height: 100vh; | |
| 13 | - | display: flex; | |
| 14 | - | align-items: center; | |
| 15 | - | justify-content: center; | |
| 16 | - | padding: 20px; | |
| 17 | - | } | |
| 18 | - | .card { | |
| 19 | - | background: #1a1a1a; | |
| 20 | - | border: 1px solid #2a2a2a; | |
| 21 | - | border-radius: 16px; | |
| 22 | - | padding: 32px; | |
| 23 | - | width: 100%; | |
| 24 | - | max-width: 620px; | |
| 25 | - | box-shadow: 0 8px 32px rgba(0,0,0,0.5); | |
| 26 | - | } | |
| 27 | - | .header { | |
| 28 | - | display: flex; | |
| 29 | - | align-items: center; | |
| 30 | - | gap: 12px; | |
| 31 | - | margin-bottom: 28px; | |
| 32 | - | } | |
| 33 | - | .logo { | |
| 34 | - | width: 44px; height: 44px; | |
| 35 | - | background: #cc0000; | |
| 36 | - | border-radius: 10px; | |
| 37 | - | display: flex; align-items: center; justify-content: center; | |
| 38 | - | flex-shrink: 0; | |
| 39 | - | } | |
| 40 | - | .logo svg { fill: white; } | |
| 41 | - | .title { font-size: 20px; font-weight: 600; color: #fff; } | |
| 42 | - | .subtitle { font-size: 13px; color: #888; margin-top: 2px; } | |
| 1 | + | #!/usr/bin/env python3 | |
| 2 | + | # -*- coding: utf-8 -*- | |
| 43 | 3 | ||
| 44 | - | label { | |
| 45 | - | display: block; | |
| 46 | - | font-size: 13px; | |
| 47 | - | color: #aaa; | |
| 48 | - | margin-bottom: 6px; | |
| 49 | - | } | |
| 50 | - | input[type="text"], select { | |
| 51 | - | width: 100%; | |
| 52 | - | background: #111; | |
| 53 | - | border: 1px solid #333; | |
| 54 | - | border-radius: 8px; | |
| 55 | - | color: #e0e0e0; | |
| 56 | - | font-size: 14px; | |
| 57 | - | padding: 10px 12px; | |
| 58 | - | outline: none; | |
| 59 | - | transition: border-color 0.2s; | |
| 60 | - | font-family: 'Segoe UI', sans-serif; | |
| 61 | - | } | |
| 62 | - | input[type="text"]:focus, select:focus { | |
| 63 | - | border-color: #cc0000; | |
| 64 | - | } | |
| 65 | - | select option { background: #1a1a1a; } | |
| 4 | + | """ | |
| 5 | + | YouTube MP3 Downloader v2.0 | |
| 6 | + | Изисква: yt-dlp, ffmpeg | |
| 7 | + | Инсталация: pip install yt-dlp | |
| 8 | + | За ffmpeg: https://ffmpeg.org/download.html | |
| 9 | + | """ | |
| 66 | 10 | ||
| 67 | - | .row { margin-bottom: 16px; } | |
| 68 | - | .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; } | |
| 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 | |
| 69 | 19 | ||
| 70 | - | .folder-row { | |
| 71 | - | display: flex; gap: 8px; | |
| 72 | - | } | |
| 73 | - | .folder-row input { flex: 1; } | |
| 74 | - | .btn-browse { | |
| 75 | - | background: #2a2a2a; | |
| 76 | - | border: 1px solid #333; | |
| 77 | - | border-radius: 8px; | |
| 78 | - | color: #ccc; | |
| 79 | - | font-size: 13px; | |
| 80 | - | padding: 0 14px; | |
| 81 | - | cursor: pointer; | |
| 82 | - | white-space: nowrap; | |
| 83 | - | transition: background 0.2s; | |
| 84 | - | } | |
| 85 | - | .btn-browse:hover { background: #333; } | |
| 20 | + | # Проверка за Python версия | |
| 21 | + | if sys.version_info < (3, 7): | |
| 22 | + | print("❌ Нужна е Python 3.7 или по-нова!") | |
| 23 | + | sys.exit(1) | |
| 86 | 24 | ||
| 87 | - | .btn-download { | |
| 88 | - | width: 100%; | |
| 89 | - | padding: 12px; | |
| 90 | - | background: #cc0000; | |
| 91 | - | color: white; | |
| 92 | - | border: none; | |
| 93 | - | border-radius: 10px; | |
| 94 | - | font-size: 16px; | |
| 95 | - | font-weight: 600; | |
| 96 | - | cursor: pointer; | |
| 97 | - | margin-top: 4px; | |
| 98 | - | transition: background 0.2s, transform 0.1s; | |
| 99 | - | font-family: 'Segoe UI', sans-serif; | |
| 100 | - | } | |
| 101 | - | .btn-download:hover { background: #e00000; } | |
| 102 | - | .btn-download:active { transform: scale(0.99); } | |
| 103 | - | .btn-download:disabled { background: #555; cursor: not-allowed; } | |
| 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) | |
| 104 | 32 | ||
| 105 | - | .console { | |
| 106 | - | margin-top: 18px; | |
| 107 | - | background: #0a0a0a; | |
| 108 | - | border: 1px solid #2a2a2a; | |
| 109 | - | border-radius: 10px; | |
| 110 | - | padding: 12px 14px; | |
| 111 | - | font-size: 13px; | |
| 112 | - | font-family: 'Cascadia Code', 'Consolas', monospace; | |
| 113 | - | color: #aaa; | |
| 114 | - | min-height: 90px; | |
| 115 | - | max-height: 220px; | |
| 116 | - | overflow-y: auto; | |
| 117 | - | line-height: 1.6; | |
| 118 | - | } | |
| 119 | - | .console .ok { color: #4ec94e; } | |
| 120 | - | .console .err { color: #f55; } | |
| 121 | - | .console .info { color: #6bc5f8; } | |
| 122 | - | .console .warn { color: #f0b429; } | |
| 33 | + | # ========== КЛАСОВЕ И КОНФИГУРАЦИЯ ========== | |
| 123 | 34 | ||
| 124 | - | .separator { | |
| 125 | - | border: none; | |
| 126 | - | border-top: 1px solid #252525; | |
| 127 | - | margin: 20px 0; | |
| 128 | - | } | |
| 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' | |
| 129 | 46 | ||
| 130 | - | .setup-section details { margin-bottom: 10px; } | |
| 131 | - | .setup-section summary { | |
| 132 | - | cursor: pointer; | |
| 133 | - | font-size: 13px; | |
| 134 | - | color: #888; | |
| 135 | - | padding: 6px 0; | |
| 136 | - | user-select: none; | |
| 137 | - | } | |
| 138 | - | .setup-section summary:hover { color: #ccc; } | |
| 139 | - | .setup-content { | |
| 140 | - | margin-top: 10px; | |
| 141 | - | background: #111; | |
| 142 | - | border: 1px solid #2a2a2a; | |
| 143 | - | border-radius: 8px; | |
| 144 | - | padding: 14px; | |
| 145 | - | font-size: 13px; | |
| 146 | - | line-height: 1.8; | |
| 147 | - | color: #bbb; | |
| 148 | - | } | |
| 149 | - | .setup-content code { | |
| 150 | - | background: #1e1e1e; | |
| 151 | - | border: 1px solid #333; | |
| 152 | - | border-radius: 4px; | |
| 153 | - | padding: 1px 7px; | |
| 154 | - | font-family: 'Cascadia Code', 'Consolas', monospace; | |
| 155 | - | color: #7ec8f7; | |
| 156 | - | font-size: 12px; | |
| 157 | - | } | |
| 158 | - | .step { margin-bottom: 10px; } | |
| 159 | - | .step-num { | |
| 160 | - | display: inline-block; | |
| 161 | - | width: 20px; height: 20px; | |
| 162 | - | background: #cc0000; | |
| 163 | - | color: white; | |
| 164 | - | border-radius: 50%; | |
| 165 | - | text-align: center; | |
| 166 | - | line-height: 20px; | |
| 167 | - | font-size: 11px; | |
| 168 | - | font-weight: 700; | |
| 169 | - | margin-right: 6px; | |
| 170 | - | } | |
| 171 | - | a { color: #6bc5f8; } | |
| 172 | - | .status-bar { | |
| 173 | - | display: flex; align-items: center; gap: 8px; | |
| 174 | - | margin-top: 10px; font-size: 12px; color: #777; | |
| 175 | - | } | |
| 176 | - | .dot { | |
| 177 | - | width: 8px; height: 8px; border-radius: 50%; | |
| 178 | - | background: #555; flex-shrink: 0; | |
| 179 | - | } | |
| 180 | - | .dot.ok { background: #4ec94e; } | |
| 181 | - | .dot.err { background: #f55; } | |
| 182 | - | </style> | |
| 183 | - | </head> | |
| 184 | - | <body> | |
| 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() | |
| 185 | 85 | ||
| 186 | - | <div class="card"> | |
| 187 | - | <div class="header"> | |
| 188 | - | <div class="logo"> | |
| 189 | - | <svg width="22" height="22" viewBox="0 0 24 24"><path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-2.88 2.5 2.89 2.89 0 0 1-2.89-2.89 2.89 2.89 0 0 1 2.89-2.89c.28 0 .54.04.79.1V9.01a6.35 6.35 0 0 0-.79-.05 6.34 6.34 0 0 0-6.34 6.34 6.34 6.34 0 0 0 6.34 6.34 6.34 6.34 0 0 0 6.33-6.34V8.69a8.28 8.28 0 0 0 4.84 1.55V6.79a4.85 4.85 0 0 1-1.07-.1z"/></svg> | |
| 190 | - | </div> | |
| 191 | - | <div> | |
| 192 | - | <div class="title">YouTube MP3 Downloader</div> | |
| 193 | - | <div class="subtitle">Powered by yt-dlp + ffmpeg</div> | |
| 194 | - | </div> | |
| 195 | - | </div> | |
| 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 'изключена'}") | |
| 196 | 385 | ||
| 197 | - | <div class="row"> | |
| 198 | - | <label>YouTube URL (видео или плейлист)</label> | |
| 199 | - | <input type="text" id="url" placeholder="https://www.youtube.com/watch?v=..." /> | |
| 200 | - | </div> | |
| 386 | + | # ========== MAIN MENU ========== | |
| 201 | 387 | ||
| 202 | - | <div class="grid"> | |
| 203 | - | <div> | |
| 204 | - | <label>Качество на MP3</label> | |
| 205 | - | <select id="quality"> | |
| 206 | - | <option value="320">320 kbps (най-добро)</option> | |
| 207 | - | <option value="256">256 kbps</option> | |
| 208 | - | <option value="192" selected>192 kbps</option> | |
| 209 | - | <option value="128">128 kbps</option> | |
| 210 | - | </select> | |
| 211 | - | </div> | |
| 212 | - | <div> | |
| 213 | - | <label>Шаблон на името</label> | |
| 214 | - | <select id="template"> | |
| 215 | - | <option value="%(title)s">Само заглавие</option> | |
| 216 | - | <option value="%(uploader)s - %(title)s">Автор - Заглавие</option> | |
| 217 | - | <option value="%(playlist_index)s. %(title)s">№. Заглавие (плейлист)</option> | |
| 218 | - | </select> | |
| 219 | - | </div> | |
| 220 | - | </div> | |
| 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 за продължение...") | |
| 221 | 509 | ||
| 222 | - | <div class="row"> | |
| 223 | - | <label>Папка за запис</label> | |
| 224 | - | <div class="folder-row"> | |
| 225 | - | <input type="text" id="outdir" value="C:\Users\%USERNAME%\Desktop" /> | |
| 226 | - | <button class="btn-browse" onclick="copyFolder()">📋 Копирай</button> | |
| 227 | - | </div> | |
| 228 | - | </div> | |
| 229 | - | ||
| 230 | - | <button class="btn-download" id="btnGen" onclick="generate()">⬇ Генерирай команда</button> | |
| 231 | - | ||
| 232 | - | <div class="console" id="console">Готов. Попълни URL и натисни бутона...</div> | |
| 233 | - | ||
| 234 | - | <div class="status-bar"> | |
| 235 | - | <div class="dot" id="dotYtdlp"></div><span id="statusYtdlp">yt-dlp: непроверен</span> | |
| 236 | - | | | |
| 237 | - | <div class="dot" id="dotFfmpeg"></div><span id="statusFfmpeg">ffmpeg: непроверен</span> | |
| 238 | - | </div> | |
| 239 | - | ||
| 240 | - | <hr class="separator"> | |
| 241 | - | ||
| 242 | - | <div class="setup-section"> | |
| 243 | - | <details> | |
| 244 | - | <summary>▸ Как да инсталираш yt-dlp и ffmpeg (разгъни)</summary> | |
| 245 | - | <div class="setup-content"> | |
| 246 | - | <div class="step"><span class="step-num">1</span>Отвори <b>PowerShell като администратор</b> (Win+X → Terminal (Admin))</div> | |
| 247 | - | <div class="step"><span class="step-num">2</span>Инсталирай <b>winget</b> пакетите:<br> | |
| 248 | - | <code>winget install yt-dlp.yt-dlp</code><br> | |
| 249 | - | <code>winget install Gyan.FFmpeg</code> | |
| 250 | - | </div> | |
| 251 | - | <div class="step"><span class="step-num">3</span>Затвори и отвори отново терминала (за да се заредят PATH-овете)</div> | |
| 252 | - | <div class="step"><span class="step-num">4</span>Провери: <code>yt-dlp --version</code> и <code>ffmpeg -version</code></div> | |
| 253 | - | <div class="step"><span class="step-num">5</span>Готово! Върни се тук и свали музика 🎵</div> | |
| 254 | - | <br> | |
| 255 | - | <b>Алтернативно</b> (без winget):<br> | |
| 256 | - | — yt-dlp: <a href="https://github.com/yt-dlp/yt-dlp/releases" target="_blank">github.com/yt-dlp/yt-dlp/releases</a> → свали <code>yt-dlp.exe</code><br> | |
| 257 | - | — ffmpeg: <a href="https://www.gyan.dev/ffmpeg/builds/" target="_blank">gyan.dev/ffmpeg/builds</a> → release build → извлечи <code>ffmpeg.exe</code><br> | |
| 258 | - | — Постави и двата .exe в <code>C:\Windows\System32</code> или в обща папка с PATH | |
| 259 | - | </div> | |
| 260 | - | </details> | |
| 261 | - | </div> | |
| 262 | - | </div> | |
| 263 | - | ||
| 264 | - | <script> | |
| 265 | - | function log(msg, cls) { | |
| 266 | - | const c = document.getElementById('console'); | |
| 267 | - | const line = document.createElement('div'); | |
| 268 | - | if (cls) line.className = cls; | |
| 269 | - | line.textContent = msg; | |
| 270 | - | c.appendChild(line); | |
| 271 | - | c.scrollTop = c.scrollHeight; | |
| 272 | - | } | |
| 273 | - | ||
| 274 | - | function clearConsole() { | |
| 275 | - | document.getElementById('console').innerHTML = ''; | |
| 276 | - | } | |
| 277 | - | ||
| 278 | - | function setStatus(tool, ok) { | |
| 279 | - | const dot = document.getElementById('dot' + tool); | |
| 280 | - | const span = document.getElementById('status' + tool); | |
| 281 | - | dot.className = 'dot ' + (ok ? 'ok' : 'err'); | |
| 282 | - | span.textContent = tool.toLowerCase() + ': ' + (ok ? 'намерен ✓' : 'не е намерен ✗'); | |
| 283 | - | } | |
| 284 | - | ||
| 285 | - | function generate() { | |
| 286 | - | clearConsole(); | |
| 287 | - | const url = document.getElementById('url').value.trim(); | |
| 288 | - | const quality = document.getElementById('quality').value; | |
| 289 | - | const template = document.getElementById('template').value; | |
| 290 | - | const outdir = document.getElementById('outdir').value.trim() || '%USERPROFILE%\\Downloads\\Music'; | |
| 291 | - | ||
| 292 | - | if (!url) { | |
| 293 | - | log('⚠ Моля въведи YouTube URL!', 'warn'); | |
| 294 | - | return; | |
| 295 | - | } | |
| 296 | - | ||
| 297 | - | if (!url.includes('youtube.com') && !url.includes('youtu.be')) { | |
| 298 | - | log('⚠ Това не изглежда като YouTube URL. Продължавам все пак...', 'warn'); | |
| 299 | - | } | |
| 300 | - | ||
| 301 | - | const isPlaylist = url.includes('playlist') || url.includes('list='); | |
| 302 | - | const outputTemplate = outdir + '\\' + template + '.%(ext)s'; | |
| 303 | - | ||
| 304 | - | const cmd = `yt-dlp -x --audio-format mp3 --audio-quality ${quality}K --embed-thumbnail --add-metadata -o "${outputTemplate}" "${url}"`; | |
| 305 | - | ||
| 306 | - | log('> Команда за PowerShell / CMD:', 'info'); | |
| 307 | - | log(''); | |
| 308 | - | log(cmd); | |
| 309 | - | log(''); | |
| 310 | - | log('─────────────────────────────────────', 'info'); | |
| 311 | - | log('Копирай командата и я постави в PowerShell или CMD.', 'ok'); | |
| 312 | - | if (isPlaylist) { | |
| 313 | - | log('📋 Открит плейлист — ще се свалят всички песни!', 'info'); | |
| 314 | - | } | |
| 315 | - | log(''); | |
| 316 | - | log('Полезни добавки:', 'info'); | |
| 317 | - | log(' --cookies-from-browser chrome (за видеа изискващи вход)'); | |
| 318 | - | log(' --yes-playlist (принудително целия плейлист)'); | |
| 319 | - | log(' --no-playlist (само видеото, не плейлиста)'); | |
| 320 | - | log(' --write-info-json (записва метаданни)'); | |
| 321 | - | ||
| 322 | - | setStatus('Ytdlp', true); | |
| 323 | - | setStatus('Ffmpeg', true); | |
| 324 | - | ||
| 325 | - | document.getElementById('btnCopy') && document.getElementById('btnCopy').remove(); | |
| 326 | - | ||
| 327 | - | const btn = document.createElement('button'); | |
| 328 | - | btn.id = 'btnCopy'; | |
| 329 | - | btn.className = 'btn-download'; | |
| 330 | - | btn.style.marginTop = '10px'; | |
| 331 | - | btn.style.background = '#1a5c1a'; | |
| 332 | - | btn.textContent = '📋 Копирай командата'; | |
| 333 | - | btn.onclick = () => { | |
| 334 | - | navigator.clipboard.writeText(cmd).then(() => { | |
| 335 | - | btn.textContent = '✓ Копирано!'; | |
| 336 | - | setTimeout(() => btn.textContent = '📋 Копирай командата', 2000); | |
| 337 | - | }); | |
| 338 | - | }; | |
| 339 | - | document.querySelector('.card').appendChild(btn); | |
| 340 | - | } | |
| 341 | - | ||
| 342 | - | function copyFolder() { | |
| 343 | - | const val = document.getElementById('outdir').value; | |
| 344 | - | navigator.clipboard.writeText(val).then(() => { | |
| 345 | - | document.querySelector('.btn-browse').textContent = '✓ Копирано!'; | |
| 346 | - | setTimeout(() => document.querySelector('.btn-browse').textContent = '📋 Копирай', 1500); | |
| 347 | - | }); | |
| 348 | - | } | |
| 349 | - | </script> | |
| 350 | - | </body> | |
| 351 | - | </html> | |
| 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) | |
urocibg a révisé ce gist 5 days ago. Aller à la révision
1 file changed, 351 insertions
YouTube_MP3_Downloader.html(fichier créé)
| @@ -0,0 +1,351 @@ | |||
| 1 | + | <!DOCTYPE html> | |
| 2 | + | <html lang="bg"> | |
| 3 | + | <head> | |
| 4 | + | <meta charset="UTF-8"> | |
| 5 | + | <title>YouTube MP3 Downloader</title> | |
| 6 | + | <style> | |
| 7 | + | * { box-sizing: border-box; margin: 0; padding: 0; } | |
| 8 | + | body { | |
| 9 | + | font-family: 'Segoe UI', sans-serif; | |
| 10 | + | background: #0f0f0f; | |
| 11 | + | color: #e0e0e0; | |
| 12 | + | min-height: 100vh; | |
| 13 | + | display: flex; | |
| 14 | + | align-items: center; | |
| 15 | + | justify-content: center; | |
| 16 | + | padding: 20px; | |
| 17 | + | } | |
| 18 | + | .card { | |
| 19 | + | background: #1a1a1a; | |
| 20 | + | border: 1px solid #2a2a2a; | |
| 21 | + | border-radius: 16px; | |
| 22 | + | padding: 32px; | |
| 23 | + | width: 100%; | |
| 24 | + | max-width: 620px; | |
| 25 | + | box-shadow: 0 8px 32px rgba(0,0,0,0.5); | |
| 26 | + | } | |
| 27 | + | .header { | |
| 28 | + | display: flex; | |
| 29 | + | align-items: center; | |
| 30 | + | gap: 12px; | |
| 31 | + | margin-bottom: 28px; | |
| 32 | + | } | |
| 33 | + | .logo { | |
| 34 | + | width: 44px; height: 44px; | |
| 35 | + | background: #cc0000; | |
| 36 | + | border-radius: 10px; | |
| 37 | + | display: flex; align-items: center; justify-content: center; | |
| 38 | + | flex-shrink: 0; | |
| 39 | + | } | |
| 40 | + | .logo svg { fill: white; } | |
| 41 | + | .title { font-size: 20px; font-weight: 600; color: #fff; } | |
| 42 | + | .subtitle { font-size: 13px; color: #888; margin-top: 2px; } | |
| 43 | + | ||
| 44 | + | label { | |
| 45 | + | display: block; | |
| 46 | + | font-size: 13px; | |
| 47 | + | color: #aaa; | |
| 48 | + | margin-bottom: 6px; | |
| 49 | + | } | |
| 50 | + | input[type="text"], select { | |
| 51 | + | width: 100%; | |
| 52 | + | background: #111; | |
| 53 | + | border: 1px solid #333; | |
| 54 | + | border-radius: 8px; | |
| 55 | + | color: #e0e0e0; | |
| 56 | + | font-size: 14px; | |
| 57 | + | padding: 10px 12px; | |
| 58 | + | outline: none; | |
| 59 | + | transition: border-color 0.2s; | |
| 60 | + | font-family: 'Segoe UI', sans-serif; | |
| 61 | + | } | |
| 62 | + | input[type="text"]:focus, select:focus { | |
| 63 | + | border-color: #cc0000; | |
| 64 | + | } | |
| 65 | + | select option { background: #1a1a1a; } | |
| 66 | + | ||
| 67 | + | .row { margin-bottom: 16px; } | |
| 68 | + | .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; } | |
| 69 | + | ||
| 70 | + | .folder-row { | |
| 71 | + | display: flex; gap: 8px; | |
| 72 | + | } | |
| 73 | + | .folder-row input { flex: 1; } | |
| 74 | + | .btn-browse { | |
| 75 | + | background: #2a2a2a; | |
| 76 | + | border: 1px solid #333; | |
| 77 | + | border-radius: 8px; | |
| 78 | + | color: #ccc; | |
| 79 | + | font-size: 13px; | |
| 80 | + | padding: 0 14px; | |
| 81 | + | cursor: pointer; | |
| 82 | + | white-space: nowrap; | |
| 83 | + | transition: background 0.2s; | |
| 84 | + | } | |
| 85 | + | .btn-browse:hover { background: #333; } | |
| 86 | + | ||
| 87 | + | .btn-download { | |
| 88 | + | width: 100%; | |
| 89 | + | padding: 12px; | |
| 90 | + | background: #cc0000; | |
| 91 | + | color: white; | |
| 92 | + | border: none; | |
| 93 | + | border-radius: 10px; | |
| 94 | + | font-size: 16px; | |
| 95 | + | font-weight: 600; | |
| 96 | + | cursor: pointer; | |
| 97 | + | margin-top: 4px; | |
| 98 | + | transition: background 0.2s, transform 0.1s; | |
| 99 | + | font-family: 'Segoe UI', sans-serif; | |
| 100 | + | } | |
| 101 | + | .btn-download:hover { background: #e00000; } | |
| 102 | + | .btn-download:active { transform: scale(0.99); } | |
| 103 | + | .btn-download:disabled { background: #555; cursor: not-allowed; } | |
| 104 | + | ||
| 105 | + | .console { | |
| 106 | + | margin-top: 18px; | |
| 107 | + | background: #0a0a0a; | |
| 108 | + | border: 1px solid #2a2a2a; | |
| 109 | + | border-radius: 10px; | |
| 110 | + | padding: 12px 14px; | |
| 111 | + | font-size: 13px; | |
| 112 | + | font-family: 'Cascadia Code', 'Consolas', monospace; | |
| 113 | + | color: #aaa; | |
| 114 | + | min-height: 90px; | |
| 115 | + | max-height: 220px; | |
| 116 | + | overflow-y: auto; | |
| 117 | + | line-height: 1.6; | |
| 118 | + | } | |
| 119 | + | .console .ok { color: #4ec94e; } | |
| 120 | + | .console .err { color: #f55; } | |
| 121 | + | .console .info { color: #6bc5f8; } | |
| 122 | + | .console .warn { color: #f0b429; } | |
| 123 | + | ||
| 124 | + | .separator { | |
| 125 | + | border: none; | |
| 126 | + | border-top: 1px solid #252525; | |
| 127 | + | margin: 20px 0; | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | .setup-section details { margin-bottom: 10px; } | |
| 131 | + | .setup-section summary { | |
| 132 | + | cursor: pointer; | |
| 133 | + | font-size: 13px; | |
| 134 | + | color: #888; | |
| 135 | + | padding: 6px 0; | |
| 136 | + | user-select: none; | |
| 137 | + | } | |
| 138 | + | .setup-section summary:hover { color: #ccc; } | |
| 139 | + | .setup-content { | |
| 140 | + | margin-top: 10px; | |
| 141 | + | background: #111; | |
| 142 | + | border: 1px solid #2a2a2a; | |
| 143 | + | border-radius: 8px; | |
| 144 | + | padding: 14px; | |
| 145 | + | font-size: 13px; | |
| 146 | + | line-height: 1.8; | |
| 147 | + | color: #bbb; | |
| 148 | + | } | |
| 149 | + | .setup-content code { | |
| 150 | + | background: #1e1e1e; | |
| 151 | + | border: 1px solid #333; | |
| 152 | + | border-radius: 4px; | |
| 153 | + | padding: 1px 7px; | |
| 154 | + | font-family: 'Cascadia Code', 'Consolas', monospace; | |
| 155 | + | color: #7ec8f7; | |
| 156 | + | font-size: 12px; | |
| 157 | + | } | |
| 158 | + | .step { margin-bottom: 10px; } | |
| 159 | + | .step-num { | |
| 160 | + | display: inline-block; | |
| 161 | + | width: 20px; height: 20px; | |
| 162 | + | background: #cc0000; | |
| 163 | + | color: white; | |
| 164 | + | border-radius: 50%; | |
| 165 | + | text-align: center; | |
| 166 | + | line-height: 20px; | |
| 167 | + | font-size: 11px; | |
| 168 | + | font-weight: 700; | |
| 169 | + | margin-right: 6px; | |
| 170 | + | } | |
| 171 | + | a { color: #6bc5f8; } | |
| 172 | + | .status-bar { | |
| 173 | + | display: flex; align-items: center; gap: 8px; | |
| 174 | + | margin-top: 10px; font-size: 12px; color: #777; | |
| 175 | + | } | |
| 176 | + | .dot { | |
| 177 | + | width: 8px; height: 8px; border-radius: 50%; | |
| 178 | + | background: #555; flex-shrink: 0; | |
| 179 | + | } | |
| 180 | + | .dot.ok { background: #4ec94e; } | |
| 181 | + | .dot.err { background: #f55; } | |
| 182 | + | </style> | |
| 183 | + | </head> | |
| 184 | + | <body> | |
| 185 | + | ||
| 186 | + | <div class="card"> | |
| 187 | + | <div class="header"> | |
| 188 | + | <div class="logo"> | |
| 189 | + | <svg width="22" height="22" viewBox="0 0 24 24"><path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-2.88 2.5 2.89 2.89 0 0 1-2.89-2.89 2.89 2.89 0 0 1 2.89-2.89c.28 0 .54.04.79.1V9.01a6.35 6.35 0 0 0-.79-.05 6.34 6.34 0 0 0-6.34 6.34 6.34 6.34 0 0 0 6.34 6.34 6.34 6.34 0 0 0 6.33-6.34V8.69a8.28 8.28 0 0 0 4.84 1.55V6.79a4.85 4.85 0 0 1-1.07-.1z"/></svg> | |
| 190 | + | </div> | |
| 191 | + | <div> | |
| 192 | + | <div class="title">YouTube MP3 Downloader</div> | |
| 193 | + | <div class="subtitle">Powered by yt-dlp + ffmpeg</div> | |
| 194 | + | </div> | |
| 195 | + | </div> | |
| 196 | + | ||
| 197 | + | <div class="row"> | |
| 198 | + | <label>YouTube URL (видео или плейлист)</label> | |
| 199 | + | <input type="text" id="url" placeholder="https://www.youtube.com/watch?v=..." /> | |
| 200 | + | </div> | |
| 201 | + | ||
| 202 | + | <div class="grid"> | |
| 203 | + | <div> | |
| 204 | + | <label>Качество на MP3</label> | |
| 205 | + | <select id="quality"> | |
| 206 | + | <option value="320">320 kbps (най-добро)</option> | |
| 207 | + | <option value="256">256 kbps</option> | |
| 208 | + | <option value="192" selected>192 kbps</option> | |
| 209 | + | <option value="128">128 kbps</option> | |
| 210 | + | </select> | |
| 211 | + | </div> | |
| 212 | + | <div> | |
| 213 | + | <label>Шаблон на името</label> | |
| 214 | + | <select id="template"> | |
| 215 | + | <option value="%(title)s">Само заглавие</option> | |
| 216 | + | <option value="%(uploader)s - %(title)s">Автор - Заглавие</option> | |
| 217 | + | <option value="%(playlist_index)s. %(title)s">№. Заглавие (плейлист)</option> | |
| 218 | + | </select> | |
| 219 | + | </div> | |
| 220 | + | </div> | |
| 221 | + | ||
| 222 | + | <div class="row"> | |
| 223 | + | <label>Папка за запис</label> | |
| 224 | + | <div class="folder-row"> | |
| 225 | + | <input type="text" id="outdir" value="C:\Users\%USERNAME%\Desktop" /> | |
| 226 | + | <button class="btn-browse" onclick="copyFolder()">📋 Копирай</button> | |
| 227 | + | </div> | |
| 228 | + | </div> | |
| 229 | + | ||
| 230 | + | <button class="btn-download" id="btnGen" onclick="generate()">⬇ Генерирай команда</button> | |
| 231 | + | ||
| 232 | + | <div class="console" id="console">Готов. Попълни URL и натисни бутона...</div> | |
| 233 | + | ||
| 234 | + | <div class="status-bar"> | |
| 235 | + | <div class="dot" id="dotYtdlp"></div><span id="statusYtdlp">yt-dlp: непроверен</span> | |
| 236 | + | | | |
| 237 | + | <div class="dot" id="dotFfmpeg"></div><span id="statusFfmpeg">ffmpeg: непроверен</span> | |
| 238 | + | </div> | |
| 239 | + | ||
| 240 | + | <hr class="separator"> | |
| 241 | + | ||
| 242 | + | <div class="setup-section"> | |
| 243 | + | <details> | |
| 244 | + | <summary>▸ Как да инсталираш yt-dlp и ffmpeg (разгъни)</summary> | |
| 245 | + | <div class="setup-content"> | |
| 246 | + | <div class="step"><span class="step-num">1</span>Отвори <b>PowerShell като администратор</b> (Win+X → Terminal (Admin))</div> | |
| 247 | + | <div class="step"><span class="step-num">2</span>Инсталирай <b>winget</b> пакетите:<br> | |
| 248 | + | <code>winget install yt-dlp.yt-dlp</code><br> | |
| 249 | + | <code>winget install Gyan.FFmpeg</code> | |
| 250 | + | </div> | |
| 251 | + | <div class="step"><span class="step-num">3</span>Затвори и отвори отново терминала (за да се заредят PATH-овете)</div> | |
| 252 | + | <div class="step"><span class="step-num">4</span>Провери: <code>yt-dlp --version</code> и <code>ffmpeg -version</code></div> | |
| 253 | + | <div class="step"><span class="step-num">5</span>Готово! Върни се тук и свали музика 🎵</div> | |
| 254 | + | <br> | |
| 255 | + | <b>Алтернативно</b> (без winget):<br> | |
| 256 | + | — yt-dlp: <a href="https://github.com/yt-dlp/yt-dlp/releases" target="_blank">github.com/yt-dlp/yt-dlp/releases</a> → свали <code>yt-dlp.exe</code><br> | |
| 257 | + | — ffmpeg: <a href="https://www.gyan.dev/ffmpeg/builds/" target="_blank">gyan.dev/ffmpeg/builds</a> → release build → извлечи <code>ffmpeg.exe</code><br> | |
| 258 | + | — Постави и двата .exe в <code>C:\Windows\System32</code> или в обща папка с PATH | |
| 259 | + | </div> | |
| 260 | + | </details> | |
| 261 | + | </div> | |
| 262 | + | </div> | |
| 263 | + | ||
| 264 | + | <script> | |
| 265 | + | function log(msg, cls) { | |
| 266 | + | const c = document.getElementById('console'); | |
| 267 | + | const line = document.createElement('div'); | |
| 268 | + | if (cls) line.className = cls; | |
| 269 | + | line.textContent = msg; | |
| 270 | + | c.appendChild(line); | |
| 271 | + | c.scrollTop = c.scrollHeight; | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | function clearConsole() { | |
| 275 | + | document.getElementById('console').innerHTML = ''; | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | function setStatus(tool, ok) { | |
| 279 | + | const dot = document.getElementById('dot' + tool); | |
| 280 | + | const span = document.getElementById('status' + tool); | |
| 281 | + | dot.className = 'dot ' + (ok ? 'ok' : 'err'); | |
| 282 | + | span.textContent = tool.toLowerCase() + ': ' + (ok ? 'намерен ✓' : 'не е намерен ✗'); | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | function generate() { | |
| 286 | + | clearConsole(); | |
| 287 | + | const url = document.getElementById('url').value.trim(); | |
| 288 | + | const quality = document.getElementById('quality').value; | |
| 289 | + | const template = document.getElementById('template').value; | |
| 290 | + | const outdir = document.getElementById('outdir').value.trim() || '%USERPROFILE%\\Downloads\\Music'; | |
| 291 | + | ||
| 292 | + | if (!url) { | |
| 293 | + | log('⚠ Моля въведи YouTube URL!', 'warn'); | |
| 294 | + | return; | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | if (!url.includes('youtube.com') && !url.includes('youtu.be')) { | |
| 298 | + | log('⚠ Това не изглежда като YouTube URL. Продължавам все пак...', 'warn'); | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | const isPlaylist = url.includes('playlist') || url.includes('list='); | |
| 302 | + | const outputTemplate = outdir + '\\' + template + '.%(ext)s'; | |
| 303 | + | ||
| 304 | + | const cmd = `yt-dlp -x --audio-format mp3 --audio-quality ${quality}K --embed-thumbnail --add-metadata -o "${outputTemplate}" "${url}"`; | |
| 305 | + | ||
| 306 | + | log('> Команда за PowerShell / CMD:', 'info'); | |
| 307 | + | log(''); | |
| 308 | + | log(cmd); | |
| 309 | + | log(''); | |
| 310 | + | log('─────────────────────────────────────', 'info'); | |
| 311 | + | log('Копирай командата и я постави в PowerShell или CMD.', 'ok'); | |
| 312 | + | if (isPlaylist) { | |
| 313 | + | log('📋 Открит плейлист — ще се свалят всички песни!', 'info'); | |
| 314 | + | } | |
| 315 | + | log(''); | |
| 316 | + | log('Полезни добавки:', 'info'); | |
| 317 | + | log(' --cookies-from-browser chrome (за видеа изискващи вход)'); | |
| 318 | + | log(' --yes-playlist (принудително целия плейлист)'); | |
| 319 | + | log(' --no-playlist (само видеото, не плейлиста)'); | |
| 320 | + | log(' --write-info-json (записва метаданни)'); | |
| 321 | + | ||
| 322 | + | setStatus('Ytdlp', true); | |
| 323 | + | setStatus('Ffmpeg', true); | |
| 324 | + | ||
| 325 | + | document.getElementById('btnCopy') && document.getElementById('btnCopy').remove(); | |
| 326 | + | ||
| 327 | + | const btn = document.createElement('button'); | |
| 328 | + | btn.id = 'btnCopy'; | |
| 329 | + | btn.className = 'btn-download'; | |
| 330 | + | btn.style.marginTop = '10px'; | |
| 331 | + | btn.style.background = '#1a5c1a'; | |
| 332 | + | btn.textContent = '📋 Копирай командата'; | |
| 333 | + | btn.onclick = () => { | |
| 334 | + | navigator.clipboard.writeText(cmd).then(() => { | |
| 335 | + | btn.textContent = '✓ Копирано!'; | |
| 336 | + | setTimeout(() => btn.textContent = '📋 Копирай командата', 2000); | |
| 337 | + | }); | |
| 338 | + | }; | |
| 339 | + | document.querySelector('.card').appendChild(btn); | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | function copyFolder() { | |
| 343 | + | const val = document.getElementById('outdir').value; | |
| 344 | + | navigator.clipboard.writeText(val).then(() => { | |
| 345 | + | document.querySelector('.btn-browse').textContent = '✓ Копирано!'; | |
| 346 | + | setTimeout(() => document.querySelector('.btn-browse').textContent = '📋 Копирай', 1500); | |
| 347 | + | }); | |
| 348 | + | } | |
| 349 | + | </script> | |
| 350 | + | </body> | |
| 351 | + | </html> | |