Dernière activité 2 days ago

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 - &nbsp;|&nbsp;
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 + &nbsp;|&nbsp;
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>
Plus récent Plus ancien