FedyaMP3.py
· 19 KiB · Python
原始文件
#!/usr/bin/env python3
"""
╔══════════════════════════════════════════════════════════╗
║ Fedya-MP3 Downloader ║
║ YouTube & Facebook → MP3 Downloader ║
║ https://itpraktika.com/ ║
╚══════════════════════════════════════════════════════════╝
Изисквания:
pip install yt-dlp colorama
+ FFmpeg инсталиран (или ffmpeg.exe в същата папка)
"""
import os
import sys
import re
import subprocess
import time
from pathlib import Path
from datetime import datetime
# ── Colorama (Windows ANSI support) ──────────────────────────────────────────
try:
from colorama import just_fix_windows_console
just_fix_windows_console()
except ImportError:
pass # не е критично
# ── Цветова палитра ───────────────────────────────────────────────────────────
class C:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
# основни
WHITE = "\033[97m"
CYAN = "\033[96m"
BLUE = "\033[94m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
MAGENTA = "\033[95m"
GRAY = "\033[90m"
# акцентни
BG_DARK = "\033[40m"
def c(*codes):
return "".join(codes)
def paint(text, *codes):
return c(*codes) + str(text) + C.RESET
# ── Банер ─────────────────────────────────────────────────────────────────────
BANNER = f"""
{c(C.CYAN, C.BOLD)}
______ ______ _______ __ __ __ _____ ____
| ____| ____| __ \ \ / //\ | \/ | __ \___ \
| |__ | |__ | | | \ \_/ // \ | \ / | |__) |__) |
| __| | __| | | | |\ // /\ \ | |\/| | ___/|__ <
| | | |____| |__| | | |/ ____ \ | | | | | ___) |
|_| |______|_____/ |_/_/ \_\ |_| |_|_| |____/
{C.RESET}{c(C.BLUE, C.BOLD)}
──────────────────────────────────────────────────────────
YouTube & Facebook → MP3 Downloader
──────────────────────────────────────────────────────────
{C.RESET}"""
def banner():
os.system("cls" if sys.platform == "win32" else "clear")
print(BANNER)
# ── Проверка / инсталация на зависимости ─────────────────────────────────────
def ensure_dependencies():
missing = []
try:
import yt_dlp # noqa
except ImportError:
missing.append("yt-dlp")
try:
import colorama # noqa
except ImportError:
missing.append("colorama")
if missing:
print(paint(f" ⚙ Инсталирам: {', '.join(missing)} …", C.YELLOW))
subprocess.run(
[sys.executable, "-m", "pip", "install", "--quiet"] + missing,
check=True
)
print(paint(" ✓ Готово.\n", C.GREEN))
# ── Проверка за нова версия на yt-dlp ────────────────────────────────────────
def check_ytdlp_update():
"""
Сравнява инсталираната версия на yt-dlp с последната в PyPI.
Ако има по-нова – пита потребителя дали да обнови.
Работи в отделна нишка, за да не бави стартирането.
"""
import threading
def _check():
try:
import urllib.request
import json
import yt_dlp
installed = yt_dlp.version.__version__
url = "https://pypi.org/pypi/yt-dlp/json"
req = urllib.request.Request(url, headers={"User-Agent": "MediaSnatch/1.0"})
with urllib.request.urlopen(req, timeout=5) as resp:
data = json.loads(resp.read())
latest = data["info"]["version"]
def parse_ver(v):
"""2026.03.17 → (2026, 3, 17) за коректно сравнение"""
try:
return tuple(int(x) for x in v.split("."))
except ValueError:
return (0,)
if parse_ver(installed) >= parse_ver(latest):
print(
f" {paint('✓', C.GREEN)} yt-dlp {paint(installed, C.CYAN)} "
f"{paint('– актуална версия', C.GRAY)}"
)
else:
print(
f"\n {paint('⚠ Налична е нова версия на yt-dlp!', C.YELLOW, C.BOLD)}\n"
f" Инсталирана : {paint(installed, C.RED)}\n"
f" Нова : {paint(latest, C.GREEN, C.BOLD)}\n"
)
ans = input(
paint(" Обновявам сега? [Y/n]: ", C.BOLD)
).strip().lower()
if ans in {"", "y", "yes", "да"}:
print(paint(" ⚙ Обновявам yt-dlp…", C.YELLOW))
result = subprocess.run(
[sys.executable, "-m", "pip", "install", "--upgrade", "--quiet", "yt-dlp"],
capture_output=True, text=True
)
if result.returncode == 0:
print(paint(f" ✓ yt-dlp обновен до {latest}\n", C.GREEN, C.BOLD))
else:
print(paint(" ✗ Обновяването неуспешно. Опитай ръчно: pip install -U yt-dlp\n", C.RED))
else:
print(paint(
f" ℹ Пропускам. Ако сваляниeто гърми, пусни:\n"
f" pip install -U yt-dlp\n",
C.GRAY
))
except Exception:
# Мълчим при грешка – няма интернет или PyPI timeout; не е критично
pass
t = threading.Thread(target=_check, daemon=True)
t.start()
t.join(timeout=7) # изчакваме макс. 7 секунди; после продължаваме
# ── FFmpeg локация ────────────────────────────────────────────────────────────
def find_ffmpeg() -> str | None:
"""Търси ffmpeg: в PATH, до скрипта/exe-то."""
import shutil
# 1. В PATH
if shutil.which("ffmpeg"):
return None # yt-dlp ще го намери сам
# 2. До .exe (PyInstaller _MEIPASS) или до .py
base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
candidate = os.path.join(base, "ffmpeg.exe" if sys.platform == "win32" else "ffmpeg")
if os.path.isfile(candidate):
return candidate
return "NOT_FOUND"
# ── Десктоп ───────────────────────────────────────────────────────────────────
def get_desktop() -> Path:
if sys.platform == "win32":
desktop = Path(os.environ.get("USERPROFILE", Path.home())) / "Desktop"
else:
desktop = Path.home() / "Desktop"
if not desktop.exists():
desktop = Path.home() / "Downloads"
desktop.mkdir(exist_ok=True)
# Подпапка Fedya-MP3
folder = desktop / "Fedya-MP3"
folder.mkdir(exist_ok=True)
return folder
# ── URL разпознаване ──────────────────────────────────────────────────────────
YT_RE = re.compile(
r"https?://(www\.)?(youtube\.com|youtu\.be|music\.youtube\.com)/.+"
)
FB_RE = re.compile(
r"https?://(www\.|m\.|web\.)?facebook\.com/.+|https?://fb\.watch/.+"
)
def detect_source(url: str) -> str:
url = url.strip()
if YT_RE.match(url): return "YouTube"
if FB_RE.match(url): return "Facebook"
return "Unknown"
# ── Progress hook ─────────────────────────────────────────────────────────────
_last_pct = -1
def progress_hook(d):
global _last_pct
status = d.get("status")
if status == "downloading":
pct_raw = d.get("_percent_str", "").strip().replace("%", "")
try:
pct = int(float(pct_raw))
except (ValueError, TypeError):
pct = 0
if pct == _last_pct:
return
_last_pct = pct
filled = int(pct / 5) # 20-char bar
bar = paint("█" * filled, C.CYAN) + paint("░" * (20 - filled), C.GRAY)
speed = d.get("_speed_str", "?").strip()
eta = d.get("_eta_str", "?").strip()
line = (
f"\r {bar} "
f"{paint(f'{pct:3d}%', C.BOLD, C.WHITE)} "
f"{paint(speed, C.YELLOW)} "
f"ETA {paint(eta, C.BLUE)}"
)
print(line, end="", flush=True)
elif status == "finished":
print(f"\r {paint('█'*20, C.GREEN)} {paint('100% ✓ конвертирам в MP3…', C.GREEN, C.BOLD)}")
_last_pct = -1
# ── Изтегляне ─────────────────────────────────────────────────────────────────
def download(url: str, output_dir: Path, source: str, quality: str) -> bool:
import yt_dlp
global _last_pct
_last_pct = -1
output_template = str(output_dir / "%(title).80s.%(ext)s")
ffmpeg_loc = find_ffmpeg()
if ffmpeg_loc == "NOT_FOUND":
print(paint(
"\n ✗ FFmpeg не е намерен!\n"
" Windows: winget install ffmpeg\n"
" macOS : brew install ffmpeg\n"
" Linux : sudo apt install ffmpeg\n"
" ИЛИ сложи ffmpeg.exe до програмата.\n",
C.RED
))
return False
ydl_opts = {
"format" : "bestaudio/best",
"postprocessors": [{
"key" : "FFmpegExtractAudio",
"preferredcodec" : "mp3",
"preferredquality": quality,
}],
"outtmpl" : output_template,
"quiet" : True,
"no_warnings" : True,
"noplaylist" : True,
"socket_timeout" : 30,
"progress_hooks" : [progress_hook],
"postprocessor_hooks": [],
}
if ffmpeg_loc: # намерен до exe-то
ydl_opts["ffmpeg_location"] = os.path.dirname(ffmpeg_loc)
# Facebook лични видеа – cookies от Chrome
if source == "Facebook":
try:
ydl_opts["cookiesfrombrowser"] = ("chrome",)
except Exception:
pass
icon_yt = paint("▶", C.RED, C.BOLD)
icon_fb = paint("f", C.BLUE, C.BOLD)
icon = icon_yt if source == "YouTube" else icon_fb
print(f"\n {icon} {paint(source, C.BOLD)} {paint(url[:72] + ('…' if len(url)>72 else ''), C.GRAY)}")
print(f" {paint('Изтеглям…', C.DIM)}")
t_start = time.time()
try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True)
title = info.get("title", "Unknown")
elapsed = time.time() - t_start
print(
f"\n {paint('✓', C.GREEN, C.BOLD)} "
f"{paint(title[:60], C.WHITE, C.BOLD)}\n"
f" {paint(f'Качество: {quality} kbps | Времетраене: {elapsed:.1f}s', C.GRAY)}"
)
return True
except yt_dlp.utils.DownloadError as e:
msg = str(e).lower()
print(f"\n {paint('✗ Грешка:', C.RED, C.BOLD)}", str(e)[:120])
if "login" in msg or "private" in msg or "sign in" in msg:
print(paint(
" 💡 Видеото изисква вход. За Facebook добавете cookies от Chrome.\n"
" За YouTube: youtube.com/premium или публично видео.",
C.YELLOW
))
return False
except Exception as e:
print(f"\n {paint('✗ Неочаквана грешка:', C.RED)} {e}")
return False
# ── Меню за качество ──────────────────────────────────────────────────────────
QUALITY_MAP = {
"1": ("320", "320 kbps – Максимално (по-голям файл)"),
"2": ("192", "192 kbps – Отлично (препоръчително)"),
"3": ("128", "128 kbps – Добро (по-малък файл)"),
"4": ("64", " 64 kbps – Ниско (podcasts/говор)"),
}
def choose_quality() -> str:
print(f"\n {paint('Качество на MP3:', C.CYAN, C.BOLD)}")
for k, (q, label) in QUALITY_MAP.items():
print(f" {paint(k, C.YELLOW, C.BOLD)}. {label}")
while True:
choice = input(paint("\n Избор [1-4, Enter=2]: ", C.BOLD)).strip() or "2"
if choice in QUALITY_MAP:
return QUALITY_MAP[choice][0]
# ── История ───────────────────────────────────────────────────────────────────
def log_history(output_dir: Path, url: str, title: str, source: str, quality: str):
log_path = output_dir / "download_history.txt"
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(log_path, "a", encoding="utf-8") as f:
f.write(f"[{now}] [{source}] [{quality}kbps] {title}\n {url}\n\n")
# ── Разделители ───────────────────────────────────────────────────────────────
def divider(char="─", color=C.GRAY):
try:
w = os.get_terminal_size().columns
except Exception:
w = 72
print(paint(char * min(w, 72), color))
def section(title: str):
divider("═", C.BLUE)
print(f" {paint(title, C.CYAN, C.BOLD)}")
divider("─")
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
ensure_dependencies()
banner()
# Проверка за нова версия на yt-dlp (при всяко стартиране)
print(paint(" Проверявам версията на yt-dlp…", C.GRAY))
check_ytdlp_update()
output_dir = get_desktop()
print(
f" {paint('📁', '')} Файловете се запазват в: "
f"{paint(str(output_dir), C.CYAN, C.BOLD)}\n"
)
quality = choose_quality()
success = 0
failed = 0
section("ВМЪКНИ URL (q = изход, c = смени качество)")
while True:
print()
try:
raw = input(paint(" URL ▸ ", C.BOLD, C.WHITE)).strip()
except (KeyboardInterrupt, EOFError):
raw = "q"
if not raw:
continue
if raw.lower() in {"q", "quit", "exit", "изход"}:
break
if raw.lower() in {"c", "quality", "качество"}:
quality = choose_quality()
section("ВМЪКНИ URL (q = изход, c = смени качество)")
continue
source = detect_source(raw)
if source == "Unknown":
print(paint(" ⚠ Не е разпознат YouTube или Facebook URL.", C.YELLOW))
continue
ok = download(raw, output_dir, source, quality)
if ok:
success += 1
else:
failed += 1
divider("─", C.GRAY)
status = (
paint(f"✓ {success}", C.GREEN, C.BOLD) +
paint(" ✗ ", C.GRAY) +
paint(str(failed), C.RED if failed else C.GRAY)
)
print(f" Сесия: {status} │ "
f"{paint('c', C.YELLOW)} = качество "
f"{paint('q', C.YELLOW)} = изход")
# Край
divider("═", C.BLUE)
print(
f"\n {paint('Сесията приключи.', C.CYAN, C.BOLD)}\n"
f" Свалени: {paint(success, C.GREEN, C.BOLD)} "
f"Неуспешни: {paint(failed, C.RED if failed else C.GRAY, C.BOLD)}\n"
f" Папка: {paint(str(output_dir), C.CYAN)}\n"
)
if sys.platform == "win32":
input(paint(" Натисни Enter за изход…", C.GRAY))
if __name__ == "__main__":
main()
# ═══════════════════════════════════════════════════════════════════════════════
# КОМПИЛИРАНЕ В .EXE (с PyInstaller)
# ═══════════════════════════════════════════════════════════════════════════════
#
# 1. Инсталирай зависимостите:
# pip install yt-dlp colorama pyinstaller
#
# 2. Свали ffmpeg.exe и го сложи в СЪЩАТА папка като mediasnatch.py
# (от https://ffmpeg.org/download.html → Windows Builds)
#
# 3. Компилирай:
# pyinstaller --onefile --console --icon=downloader.ico ^
# --add-binary "ffmpeg.exe;." ^
# --name MediaSnatch ^
# mediasnatch.py
#
# 4. Готовият .exe е в папка dist\MediaSnatch.exe
#
# ЗАБЕЛЕЖКА: --add-binary синтаксис на Linux/macOS е с ":" вместо ";"
# pyinstaller --onefile --console --icon=downloader.ico \
# --add-binary "ffmpeg:." \
# --name MediaSnatch \
# mediasnatch.py
# ═══════════════════════════════════════════════════════════════════════════════
| 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 | |
| 14 | import os |
| 15 | import sys |
| 16 | import re |
| 17 | import subprocess |
| 18 | import time |
| 19 | from pathlib import Path |
| 20 | from datetime import datetime |
| 21 | |
| 22 | # ── Colorama (Windows ANSI support) ────────────────────────────────────────── |
| 23 | try: |
| 24 | from colorama import just_fix_windows_console |
| 25 | just_fix_windows_console() |
| 26 | except ImportError: |
| 27 | pass # не е критично |
| 28 | |
| 29 | # ── Цветова палитра ─────────────────────────────────────────────────────────── |
| 30 | class 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 | |
| 46 | def c(*codes): |
| 47 | return "".join(codes) |
| 48 | |
| 49 | def paint(text, *codes): |
| 50 | return c(*codes) + str(text) + C.RESET |
| 51 | |
| 52 | |
| 53 | # ── Банер ───────────────────────────────────────────────────────────────────── |
| 54 | BANNER = 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 | |
| 71 | def banner(): |
| 72 | os.system("cls" if sys.platform == "win32" else "clear") |
| 73 | print(BANNER) |
| 74 | |
| 75 | |
| 76 | # ── Проверка / инсталация на зависимости ───────────────────────────────────── |
| 77 | def 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 ──────────────────────────────────────── |
| 98 | def 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 локация ──────────────────────────────────────────────────────────── |
| 169 | def 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 | # ── Десктоп ─────────────────────────────────────────────────────────────────── |
| 187 | def 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 разпознаване ────────────────────────────────────────────────────────── |
| 204 | YT_RE = re.compile( |
| 205 | r"https?://(www\.)?(youtube\.com|youtu\.be|music\.youtube\.com)/.+" |
| 206 | ) |
| 207 | FB_RE = re.compile( |
| 208 | r"https?://(www\.|m\.|web\.)?facebook\.com/.+|https?://fb\.watch/.+" |
| 209 | ) |
| 210 | |
| 211 | def 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 | |
| 221 | def 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 | # ── Изтегляне ───────────────────────────────────────────────────────────────── |
| 256 | def 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 | # ── Меню за качество ────────────────────────────────────────────────────────── |
| 341 | QUALITY_MAP = { |
| 342 | "1": ("320", "320 kbps – Максимално (по-голям файл)"), |
| 343 | "2": ("192", "192 kbps – Отлично (препоръчително)"), |
| 344 | "3": ("128", "128 kbps – Добро (по-малък файл)"), |
| 345 | "4": ("64", " 64 kbps – Ниско (podcasts/говор)"), |
| 346 | } |
| 347 | |
| 348 | def 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 | # ── История ─────────────────────────────────────────────────────────────────── |
| 359 | def 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 | # ── Разделители ─────────────────────────────────────────────────────────────── |
| 367 | def 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 | |
| 375 | def section(title: str): |
| 376 | divider("═", C.BLUE) |
| 377 | print(f" {paint(title, C.CYAN, C.BOLD)}") |
| 378 | divider("─") |
| 379 | |
| 380 | |
| 381 | # ── Main ────────────────────────────────────────────────────────────────────── |
| 382 | def 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 | |
| 454 | if __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 |