#!/usr/bin/env python3 """ stahni_video.py — stahování videí (Vimeo, YouTube, …) přes yt-dlp. Co umí: * Automaticky nastaví cestu k ffmpeg (přes balík static-ffmpeg) — netřeba ho mít v systému. * Stáhne nejlepší dostupnou kvalitu a sloučí video+audio do .mp4. * Pokud je video SOUKROMÉ / nedostupné / odstraněné, sám to pozná, vypíše srozumitelnou hlášku a přeskočí ho (nespadne, jede dál na další URL). Použití: python stahni_video.py URL [URL2 ...] python stahni_video.py # vezme URL z urls.txt (1 na řádek) python stahni_video.py --cookies-from-browser firefox URL # video za přihlášením Instalace závislostí (jednorázově): python -m pip install -U yt-dlp static-ffmpeg """ import argparse import os import shutil import sys from pathlib import Path # Pojistka: ať výpis nikdy nespadne na znaku, který kódování konzole nezná # (zachová kódování konzole, jen neznámý znak escapne místo pádu programu). for _stream in (sys.stdout, sys.stderr): try: _stream.reconfigure(errors="backslashreplace") except Exception: pass SKRIPT_DIR = Path(__file__).resolve().parent # --- důvody, proč video NEJDE stáhnout (→ přeskočit, ne padat) ---------------- # klíč hledáme (case-insensitive) v textu chyby od yt-dlp DUVODY_PRESKOCIT = [ ("private video", "video je soukromé"), ("video is private", "video je soukromé"), ("this is a private video", "video je soukromé"), ("video unavailable", "video není dostupné"), ("this video is unavailable", "video není dostupné"), ("video has been removed", "video bylo odstraněno"), ("removed by the uploader", "video odstranil autor"), ("no longer available", "video už není dostupné"), ("members-only", "jen pro členy kanálu"), ("available to members", "jen pro členy kanálu"), ("account associated with this video has been terminated", "účet autora byl zrušen"), ("has been terminated", "účet autora byl zrušen"), ("blocked it on copyright", "blokováno kvůli autorským právům"), ("not available in your country", "nedostupné ve tvé zemi"), ("not available on this app", "nedostupné pro tohoto klienta"), ("sign in to confirm your age", "věkově omezené (nutné přihlášení)"), ("requires payment", "placené video"), ("this live event will begin", "živý přenos zatím nezačal"), ("premieres in", "video bude teprve uvedeno (premiéra)"), ] def klasifikuj_chybu(msg: str): """Vrátí český popis důvodu k přeskočení, nebo None pokud jde o jinou chybu.""" m = msg.lower() for klic, popis in DUVODY_PRESKOCIT: if klic in m: return popis return None class _TichyLogger: """Potlačí ukecaný výpis yt-dlp; chyby si hlídáme sami přes výjimky.""" def debug(self, msg): pass def info(self, msg): pass def warning(self, msg): pass def error(self, msg): pass def _progress_hook(d): if d.get("status") == "downloading": pct = (d.get("_percent_str") or "").strip() spd = (d.get("_speed_str") or "").strip() print(f"\r stahuji {pct} {spd} ", end="", flush=True) elif d.get("status") == "finished": print(f"\r staženo, zpracovávám… ") def priprav_ffmpeg(): """Zajistí ffmpeg/ffprobe a vrátí adresář s binárkami (nebo None).""" try: import static_ffmpeg static_ffmpeg.add_paths() # přidá ffmpeg/ffprobe do PATH (1. běh = stáhne) except ImportError: pass ff = shutil.which("ffmpeg") if ff: return os.path.dirname(ff) print("UPOZORNĚNÍ: ffmpeg nenalezen — sloučení video+audio nemusí fungovat.") print(" Nainstaluj: python -m pip install -U static-ffmpeg") return None def nacti_urls(args_urls): if args_urls: return args_urls soubor = SKRIPT_DIR / "urls.txt" if soubor.exists(): radky = [r.strip() for r in soubor.read_text(encoding="utf-8").splitlines()] return [r for r in radky if r and not r.startswith("#")] return [] def stahni(urls, out_dir: Path, cookies_browser=None): try: import yt_dlp from yt_dlp.utils import DownloadError except ImportError: sys.exit("Chybí yt-dlp. Nainstaluj: python -m pip install -U yt-dlp") ff_dir = priprav_ffmpeg() out_dir.mkdir(parents=True, exist_ok=True) ydl_opts = { "outtmpl": str(out_dir / "%(title)s [%(id)s].%(ext)s"), "format": "bestvideo*+bestaudio/best", "merge_output_format": "mp4", "logger": _TichyLogger(), "progress_hooks": [_progress_hook], "noprogress": True, # vlastní progress řešíme hookem "noplaylist": True, } if ff_dir: ydl_opts["ffmpeg_location"] = ff_dir if cookies_browser: ydl_opts["cookiesfrombrowser"] = (cookies_browser,) stazeno, preskoceno, chyby = 0, [], [] with yt_dlp.YoutubeDL(ydl_opts) as ydl: for i, url in enumerate(urls, 1): print(f"\n[{i}/{len(urls)}] {url}") try: info = ydl.extract_info(url, download=True) nazev = info.get("title", url) if info else url print(f" [HOTOVO] {nazev}") stazeno += 1 except DownloadError as e: duvod = klasifikuj_chybu(str(e)) if duvod: print(f" [PRESKOCENO] {duvod}") preskoceno.append((url, duvod)) else: strucne = str(e).split("\n")[0] print(f" [CHYBA] {strucne}") chyby.append((url, strucne)) except Exception as e: # nečekané — taky nezhasnout celý běh print(f" [CHYBA] {e}") chyby.append((url, str(e))) print("\n=== SOUHRN ===") print(f" staženo: {stazeno}") print(f" přeskočeno: {len(preskoceno)}") for url, duvod in preskoceno: print(f" - {url} ({duvod})") if chyby: print(f" chyby: {len(chyby)}") for url, msg in chyby: print(f" - {url} ({msg})") return stazeno, preskoceno, chyby def main(): p = argparse.ArgumentParser( description="Stáhne videa přes yt-dlp; soukromá/nedostupná sám přeskočí.") p.add_argument("urls", nargs="*", help="URL videí (nebo nech prázdné a použij urls.txt)") p.add_argument("-o", "--out-dir", default=str(SKRIPT_DIR), help="výstupní adresář (výchozí: tento adresář)") p.add_argument("--cookies-from-browser", dest="cookies", help="prohlížeč pro cookies u videí za přihlášením (firefox/chrome/edge…)") a = p.parse_args() urls = nacti_urls(a.urls) if not urls: sys.exit("Nezadal jsi žádné URL. Předej je jako argumenty nebo do urls.txt.") stahni(urls, Path(a.out_dir), cookies_browser=a.cookies) if __name__ == "__main__": main()