Última atividade 1 day ago

YouTube & Facebook → MP3 Downloader

Revisão de5956b8e95f403a67651e62d7bf2132bda141e7

FedyaMP3.py Bruto
1#!/usr/bin/env python3
2"""
3╔══════════════════════════════════════════════════════════╗
4║ Fedya-MP3 Downloader
5║ YouTube & Facebook → MP3 Downloader ║
6║ https://itpraktika.com/
7╚══════════════════════════════════════════════════════════╝
8
9Изисквания:
10 pip install yt-dlp colorama
11 + FFmpeg инсталиран (или ffmpeg.exe в същата папка)
12"""
13
14import os
15import sys
16import re
17import subprocess
18import time
19from pathlib import Path
20from datetime import datetime
21
22# ── Colorama (Windows ANSI support) ──────────────────────────────────────────
23try:
24 from colorama import just_fix_windows_console
25 just_fix_windows_console()
26except ImportError:
27 pass # не е критично
28
29# ── Цветова палитра ───────────────────────────────────────────────────────────
30class C:
31 RESET = "\033[0m"
32 BOLD = "\033[1m"
33 DIM = "\033[2m"
34 # основни
35 WHITE = "\033[97m"
36 CYAN = "\033[96m"
37 BLUE = "\033[94m"
38 GREEN = "\033[92m"
39 YELLOW = "\033[93m"
40 RED = "\033[91m"
41 MAGENTA = "\033[95m"
42 GRAY = "\033[90m"
43 # акцентни
44 BG_DARK = "\033[40m"
45
46def c(*codes):
47 return "".join(codes)
48
49def paint(text, *codes):
50 return c(*codes) + str(text) + C.RESET
51
52
53# ── Банер ─────────────────────────────────────────────────────────────────────
54BANNER = f"""
55{c(C.CYAN, C.BOLD)}
56 ______ ______ _______ __ __ __ _____ ____
57 | ____| ____| __ \ \ / //\ | \/ | __ \___ \
58 | |__ | |__ | | | \ \_/ // \ | \ / | |__) |__) |
59 | __| | __| | | | |\ // /\ \ | |\/| | ___/|__ <
60 | | | |____| |__| | | |/ ____ \ | | | | | ___) |
61 |_| |______|_____/ |_/_/ \_\ |_| |_|_| |____/
62
63
64
65 {C.RESET}{c(C.BLUE, C.BOLD)}
66 ──────────────────────────────────────────────────────────
67 YouTube & Facebook → MP3 Downloader
68 ──────────────────────────────────────────────────────────
69{C.RESET}"""
70
71def banner():
72 os.system("cls" if sys.platform == "win32" else "clear")
73 print(BANNER)
74
75
76# ── Проверка / инсталация на зависимости ─────────────────────────────────────
77def ensure_dependencies():
78 missing = []
79 try:
80 import yt_dlp # noqa
81 except ImportError:
82 missing.append("yt-dlp")
83 try:
84 import colorama # noqa
85 except ImportError:
86 missing.append("colorama")
87
88 if missing:
89 print(paint(f" ⚙ Инсталирам: {', '.join(missing)}", C.YELLOW))
90 subprocess.run(
91 [sys.executable, "-m", "pip", "install", "--quiet"] + missing,
92 check=True
93 )
94 print(paint(" ✓ Готово.\n", C.GREEN))
95
96
97# ── Проверка за нова версия на yt-dlp ────────────────────────────────────────
98def check_ytdlp_update():
99 """
100 Сравнява инсталираната версия на yt-dlp с последната в PyPI.
101 Ако има по-нова – пита потребителя дали да обнови.
102 Работи в отделна нишка, за да не бави стартирането.
103 """
104 import threading
105
106 def _check():
107 try:
108 import urllib.request
109 import json
110 import yt_dlp
111
112 installed = yt_dlp.version.__version__
113
114 url = "https://pypi.org/pypi/yt-dlp/json"
115 req = urllib.request.Request(url, headers={"User-Agent": "MediaSnatch/1.0"})
116 with urllib.request.urlopen(req, timeout=5) as resp:
117 data = json.loads(resp.read())
118 latest = data["info"]["version"]
119
120 def parse_ver(v):
121 """2026.03.17 → (2026, 3, 17) за коректно сравнение"""
122 try:
123 return tuple(int(x) for x in v.split("."))
124 except ValueError:
125 return (0,)
126
127 if parse_ver(installed) >= parse_ver(latest):
128 print(
129 f" {paint('', C.GREEN)} yt-dlp {paint(installed, C.CYAN)} "
130 f"{paint('– актуална версия', C.GRAY)}"
131 )
132 else:
133 print(
134 f"\n {paint('⚠ Налична е нова версия на yt-dlp!', C.YELLOW, C.BOLD)}\n"
135 f" Инсталирана : {paint(installed, C.RED)}\n"
136 f" Нова : {paint(latest, C.GREEN, C.BOLD)}\n"
137 )
138 ans = input(
139 paint(" Обновявам сега? [Y/n]: ", C.BOLD)
140 ).strip().lower()
141
142 if ans in {"", "y", "yes", "да"}:
143 print(paint(" ⚙ Обновявам yt-dlp…", C.YELLOW))
144 result = subprocess.run(
145 [sys.executable, "-m", "pip", "install", "--upgrade", "--quiet", "yt-dlp"],
146 capture_output=True, text=True
147 )
148 if result.returncode == 0:
149 print(paint(f" ✓ yt-dlp обновен до {latest}\n", C.GREEN, C.BOLD))
150 else:
151 print(paint(" ✗ Обновяването неуспешно. Опитай ръчно: pip install -U yt-dlp\n", C.RED))
152 else:
153 print(paint(
154 f" ℹ Пропускам. Ако сваляниeто гърми, пусни:\n"
155 f" pip install -U yt-dlp\n",
156 C.GRAY
157 ))
158
159 except Exception:
160 # Мълчим при грешка – няма интернет или PyPI timeout; не е критично
161 pass
162
163 t = threading.Thread(target=_check, daemon=True)
164 t.start()
165 t.join(timeout=7) # изчакваме макс. 7 секунди; после продължаваме
166
167
168# ── FFmpeg локация ────────────────────────────────────────────────────────────
169def find_ffmpeg() -> str | None:
170 """Търси ffmpeg: в PATH, до скрипта/exe-то."""
171 import shutil
172
173 # 1. В PATH
174 if shutil.which("ffmpeg"):
175 return None # yt-dlp ще го намери сам
176
177 # 2. До .exe (PyInstaller _MEIPASS) или до .py
178 base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
179 candidate = os.path.join(base, "ffmpeg.exe" if sys.platform == "win32" else "ffmpeg")
180 if os.path.isfile(candidate):
181 return candidate
182
183 return "NOT_FOUND"
184
185
186# ── Десктоп ───────────────────────────────────────────────────────────────────
187def get_desktop() -> Path:
188 if sys.platform == "win32":
189 desktop = Path(os.environ.get("USERPROFILE", Path.home())) / "Desktop"
190 else:
191 desktop = Path.home() / "Desktop"
192
193 if not desktop.exists():
194 desktop = Path.home() / "Downloads"
195 desktop.mkdir(exist_ok=True)
196
197 # Подпапка Fedya-MP3
198 folder = desktop / "Fedya-MP3"
199 folder.mkdir(exist_ok=True)
200 return folder
201
202
203# ── URL разпознаване ──────────────────────────────────────────────────────────
204YT_RE = re.compile(
205 r"https?://(www\.)?(youtube\.com|youtu\.be|music\.youtube\.com)/.+"
206)
207FB_RE = re.compile(
208 r"https?://(www\.|m\.|web\.)?facebook\.com/.+|https?://fb\.watch/.+"
209)
210
211def detect_source(url: str) -> str:
212 url = url.strip()
213 if YT_RE.match(url): return "YouTube"
214 if FB_RE.match(url): return "Facebook"
215 return "Unknown"
216
217
218# ── Progress hook ─────────────────────────────────────────────────────────────
219_last_pct = -1
220
221def progress_hook(d):
222 global _last_pct
223 status = d.get("status")
224
225 if status == "downloading":
226 pct_raw = d.get("_percent_str", "").strip().replace("%", "")
227 try:
228 pct = int(float(pct_raw))
229 except (ValueError, TypeError):
230 pct = 0
231
232 if pct == _last_pct:
233 return
234 _last_pct = pct
235
236 filled = int(pct / 5) # 20-char bar
237 bar = paint("" * filled, C.CYAN) + paint("" * (20 - filled), C.GRAY)
238
239 speed = d.get("_speed_str", "?").strip()
240 eta = d.get("_eta_str", "?").strip()
241
242 line = (
243 f"\r {bar} "
244 f"{paint(f'{pct:3d}%', C.BOLD, C.WHITE)} "
245 f"{paint(speed, C.YELLOW)} "
246 f"ETA {paint(eta, C.BLUE)}"
247 )
248 print(line, end="", flush=True)
249
250 elif status == "finished":
251 print(f"\r {paint(''*20, C.GREEN)} {paint('100% ✓ конвертирам в MP3…', C.GREEN, C.BOLD)}")
252 _last_pct = -1
253
254
255# ── Изтегляне ─────────────────────────────────────────────────────────────────
256def download(url: str, output_dir: Path, source: str, quality: str) -> bool:
257 import yt_dlp
258
259 global _last_pct
260 _last_pct = -1
261
262 output_template = str(output_dir / "%(title).80s.%(ext)s")
263
264 ffmpeg_loc = find_ffmpeg()
265 if ffmpeg_loc == "NOT_FOUND":
266 print(paint(
267 "\n ✗ FFmpeg не е намерен!\n"
268 " Windows: winget install ffmpeg\n"
269 " macOS : brew install ffmpeg\n"
270 " Linux : sudo apt install ffmpeg\n"
271 " ИЛИ сложи ffmpeg.exe до програмата.\n",
272 C.RED
273 ))
274 return False
275
276 ydl_opts = {
277 "format" : "bestaudio/best",
278 "postprocessors": [{
279 "key" : "FFmpegExtractAudio",
280 "preferredcodec" : "mp3",
281 "preferredquality": quality,
282 }],
283 "outtmpl" : output_template,
284 "quiet" : True,
285 "no_warnings" : True,
286 "noplaylist" : True,
287 "socket_timeout" : 30,
288 "progress_hooks" : [progress_hook],
289 "postprocessor_hooks": [],
290 }
291
292 if ffmpeg_loc: # намерен до exe-то
293 ydl_opts["ffmpeg_location"] = os.path.dirname(ffmpeg_loc)
294
295 # Facebook лични видеа – cookies от Chrome
296 if source == "Facebook":
297 try:
298 ydl_opts["cookiesfrombrowser"] = ("chrome",)
299 except Exception:
300 pass
301
302 icon_yt = paint("", C.RED, C.BOLD)
303 icon_fb = paint("f", C.BLUE, C.BOLD)
304 icon = icon_yt if source == "YouTube" else icon_fb
305
306 print(f"\n {icon} {paint(source, C.BOLD)} {paint(url[:72] + ('' if len(url)>72 else ''), C.GRAY)}")
307 print(f" {paint('Изтеглям…', C.DIM)}")
308
309 t_start = time.time()
310
311 try:
312 with yt_dlp.YoutubeDL(ydl_opts) as ydl:
313 info = ydl.extract_info(url, download=True)
314 title = info.get("title", "Unknown")
315
316 elapsed = time.time() - t_start
317 print(
318 f"\n {paint('', C.GREEN, C.BOLD)} "
319 f"{paint(title[:60], C.WHITE, C.BOLD)}\n"
320 f" {paint(f'Качество: {quality} kbps | Времетраене: {elapsed:.1f}s', C.GRAY)}"
321 )
322 return True
323
324 except yt_dlp.utils.DownloadError as e:
325 msg = str(e).lower()
326 print(f"\n {paint('✗ Грешка:', C.RED, C.BOLD)}", str(e)[:120])
327 if "login" in msg or "private" in msg or "sign in" in msg:
328 print(paint(
329 " 💡 Видеото изисква вход. За Facebook добавете cookies от Chrome.\n"
330 " За YouTube: youtube.com/premium или публично видео.",
331 C.YELLOW
332 ))
333 return False
334
335 except Exception as e:
336 print(f"\n {paint('✗ Неочаквана грешка:', C.RED)} {e}")
337 return False
338
339
340# ── Меню за качество ──────────────────────────────────────────────────────────
341QUALITY_MAP = {
342 "1": ("320", "320 kbps – Максимално (по-голям файл)"),
343 "2": ("192", "192 kbps – Отлично (препоръчително)"),
344 "3": ("128", "128 kbps – Добро (по-малък файл)"),
345 "4": ("64", " 64 kbps – Ниско (podcasts/говор)"),
346}
347
348def choose_quality() -> str:
349 print(f"\n {paint('Качество на MP3:', C.CYAN, C.BOLD)}")
350 for k, (q, label) in QUALITY_MAP.items():
351 print(f" {paint(k, C.YELLOW, C.BOLD)}. {label}")
352 while True:
353 choice = input(paint("\n Избор [1-4, Enter=2]: ", C.BOLD)).strip() or "2"
354 if choice in QUALITY_MAP:
355 return QUALITY_MAP[choice][0]
356
357
358# ── История ───────────────────────────────────────────────────────────────────
359def log_history(output_dir: Path, url: str, title: str, source: str, quality: str):
360 log_path = output_dir / "download_history.txt"
361 now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
362 with open(log_path, "a", encoding="utf-8") as f:
363 f.write(f"[{now}] [{source}] [{quality}kbps] {title}\n {url}\n\n")
364
365
366# ── Разделители ───────────────────────────────────────────────────────────────
367def divider(char="", color=C.GRAY):
368 try:
369 w = os.get_terminal_size().columns
370 except Exception:
371 w = 72
372 print(paint(char * min(w, 72), color))
373
374
375def section(title: str):
376 divider("", C.BLUE)
377 print(f" {paint(title, C.CYAN, C.BOLD)}")
378 divider("")
379
380
381# ── Main ──────────────────────────────────────────────────────────────────────
382def main():
383 ensure_dependencies()
384 banner()
385
386 # Проверка за нова версия на yt-dlp (при всяко стартиране)
387 print(paint(" Проверявам версията на yt-dlp…", C.GRAY))
388 check_ytdlp_update()
389
390 output_dir = get_desktop()
391 print(
392 f" {paint('📁', '')} Файловете се запазват в: "
393 f"{paint(str(output_dir), C.CYAN, C.BOLD)}\n"
394 )
395
396 quality = choose_quality()
397 success = 0
398 failed = 0
399
400 section("ВМЪКНИ URL (q = изход, c = смени качество)")
401
402 while True:
403 print()
404 try:
405 raw = input(paint(" URL ▸ ", C.BOLD, C.WHITE)).strip()
406 except (KeyboardInterrupt, EOFError):
407 raw = "q"
408
409 if not raw:
410 continue
411
412 if raw.lower() in {"q", "quit", "exit", "изход"}:
413 break
414
415 if raw.lower() in {"c", "quality", "качество"}:
416 quality = choose_quality()
417 section("ВМЪКНИ URL (q = изход, c = смени качество)")
418 continue
419
420 source = detect_source(raw)
421 if source == "Unknown":
422 print(paint(" ⚠ Не е разпознат YouTube или Facebook URL.", C.YELLOW))
423 continue
424
425 ok = download(raw, output_dir, source, quality)
426 if ok:
427 success += 1
428 else:
429 failed += 1
430
431 divider("", C.GRAY)
432 status = (
433 paint(f"{success}", C.GREEN, C.BOLD) +
434 paint("", C.GRAY) +
435 paint(str(failed), C.RED if failed else C.GRAY)
436 )
437 print(f" Сесия: {status}"
438 f"{paint('c', C.YELLOW)} = качество "
439 f"{paint('q', C.YELLOW)} = изход")
440
441 # Край
442 divider("", C.BLUE)
443 print(
444 f"\n {paint('Сесията приключи.', C.CYAN, C.BOLD)}\n"
445 f" Свалени: {paint(success, C.GREEN, C.BOLD)} "
446 f"Неуспешни: {paint(failed, C.RED if failed else C.GRAY, C.BOLD)}\n"
447 f" Папка: {paint(str(output_dir), C.CYAN)}\n"
448 )
449
450 if sys.platform == "win32":
451 input(paint(" Натисни Enter за изход…", C.GRAY))
452
453
454if __name__ == "__main__":
455 main()
456
457
458# ═══════════════════════════════════════════════════════════════════════════════
459# КОМПИЛИРАНЕ В .EXE (с PyInstaller)
460# ═══════════════════════════════════════════════════════════════════════════════
461#
462# 1. Инсталирай зависимостите:
463# pip install yt-dlp colorama pyinstaller
464#
465# 2. Свали ffmpeg.exe и го сложи в СЪЩАТА папка като mediasnatch.py
466# (от https://ffmpeg.org/download.html → Windows Builds)
467#
468# 3. Компилирай:
469# pyinstaller --onefile --console --icon=downloader.ico ^
470# --add-binary "ffmpeg.exe;." ^
471# --name MediaSnatch ^
472# mediasnatch.py
473#
474# 4. Готовият .exe е в папка dist\MediaSnatch.exe
475#
476# ЗАБЕЛЕЖКА: --add-binary синтаксис на Linux/macOS е с ":" вместо ";"
477# pyinstaller --onefile --console --icon=downloader.ico \
478# --add-binary "ffmpeg:." \
479# --name MediaSnatch \
480# mediasnatch.py
481# ═══════════════════════════════════════════════════════════════════════════════
482