diff --git a/scripts/yt_download.py b/scripts/yt_download.py index 62dcde0..ecc92e6 100644 --- a/scripts/yt_download.py +++ b/scripts/yt_download.py @@ -9,10 +9,75 @@ import argparse import os import subprocess import sys +import time from pathlib import Path import json +YT_API_URL = os.environ.get("YT_API_URL", "https://yt.biba.live") +YT_API_TOKEN = os.environ.get("YT_API_TOKEN", "") + + +def fetch_cookies_from_api(cache_path="/tmp/yt_cookies_api.txt", max_age_seconds=300): + """Pull sveže YouTube cookies iz yt.biba.live API. + + Cache za 5 min (TTL) — če imamo nedavno fetched, uporabi local copy. + Vrne path do cookies file-a ali None če API ne deluje. + """ + if not YT_API_TOKEN: + return None # ni configured + + cache = Path(cache_path) + if cache.exists(): + age = time.time() - cache.stat().st_mtime + if age < max_age_seconds and cache.stat().st_size > 100: + return str(cache) + + try: + import urllib.request + req = urllib.request.Request( + f"{YT_API_URL}/cookies/raw", + headers={"Authorization": f"Bearer {YT_API_TOKEN}"}, + ) + with urllib.request.urlopen(req, timeout=10) as resp: + content = resp.read().decode("utf-8") + if len(content) < 100: + print(f"⚠ yt.biba.live vrnil prekratek cookies file ({len(content)} bytes)", file=sys.stderr) + return None + cache.write_text(content, encoding="utf-8") + print(f"🍪 Cookies fetched iz yt.biba.live ({len(content)} bytes)", file=sys.stderr) + return str(cache) + except Exception as e: + print(f"⚠ yt.biba.live cookies fetch failed: {e}", file=sys.stderr) + return None + + +def get_cookies_file(cookies_file=None): + """Vrne path do cookies file-a — prioriteta: + 1. eksplicitno podano (cookies_file param) + 2. yt.biba.live API (sveže, hourly refreshed) + 3. /data/cookies/youtube.txt (lokalni fallback) + 4. YT_COOKIES_FILE env + """ + if cookies_file and Path(cookies_file).exists(): + return cookies_file + + # Try API + api_cookies = fetch_cookies_from_api() + if api_cookies: + return api_cookies + + # Fallback to local + for candidate in [ + "/data/cookies/youtube.txt", + os.environ.get("YT_COOKIES_FILE", ""), + ]: + if candidate and Path(candidate).exists(): + return candidate + + return None + + def download(url, output, max_height=1080, format_str=None, cookies_file=None): """ Download YT video. Privzeto: best mp4 ≤1080p z audiotrackom. @@ -33,17 +98,8 @@ def download(url, output, max_height=1080, format_str=None, cookies_file=None): "-o", str(output), ] - # Auto-detect cookies file če ni eksplicitno podan - if cookies_file is None: - for candidate in [ - "/data/cookies/youtube.txt", - os.environ.get("YT_COOKIES_FILE", ""), - ]: - if candidate and Path(candidate).exists(): - cookies_file = candidate - break - - if cookies_file and Path(cookies_file).exists(): + cookies_file = get_cookies_file(cookies_file) + if cookies_file: cmd += ["--cookies", str(cookies_file)] print(f"🍪 Using cookies: {cookies_file}", file=sys.stderr) @@ -60,12 +116,8 @@ def download(url, output, max_height=1080, format_str=None, cookies_file=None): def get_info(url, cookies_file=None): """Vrni metadata brez prenosa.""" cmd = ["yt-dlp", "--dump-json", "--no-playlist"] - if cookies_file is None: - for candidate in ["/data/cookies/youtube.txt", os.environ.get("YT_COOKIES_FILE", "")]: - if candidate and Path(candidate).exists(): - cookies_file = candidate - break - if cookies_file and Path(cookies_file).exists(): + cookies_file = get_cookies_file(cookies_file) + if cookies_file: cmd += ["--cookies", str(cookies_file)] cmd.append(url) result = subprocess.run(cmd, capture_output=True, text=True) @@ -77,6 +129,11 @@ def get_info(url, cookies_file=None): def get_playlist(url, cookies_file=None): """Vrni listo videov v playlistu (samo metadata, ne pobere). + Strategija: + 1. Najprej probaj yt.biba.live API /download/playlist (centralizirano, + vedno sveže cookies + signature solver na strežniku). + 2. Fallback na lokalni yt-dlp --flat-playlist. + Returns: { "is_playlist": bool, @@ -86,19 +143,40 @@ def get_playlist(url, cookies_file=None): ... ] } - - Če URL ni playlist, vrne is_playlist=False in samo en item. """ + # ─── 1. Try yt.biba.live API ─── + if YT_API_TOKEN: + try: + import urllib.request + import urllib.parse + params = urllib.parse.urlencode({"url": url, "max_items": 200}) + req = urllib.request.Request( + f"{YT_API_URL}/download/playlist?{params}", + headers={"Authorization": f"Bearer {YT_API_TOKEN}"}, + ) + with urllib.request.urlopen(req, timeout=120) as resp: + api_data = json.loads(resp.read().decode("utf-8")) + + if api_data.get("ok"): + print(f"📋 Playlist resolved via yt.biba.live API: {api_data.get('count', 0)} items", file=sys.stderr) + return { + "is_playlist": api_data.get("is_playlist", False), + "playlist_title": api_data.get("playlist_title", ""), + "items": api_data.get("items", []), + } + else: + print(f"⚠ yt.biba.live API error: {api_data.get('error', 'unknown')[:200]}", file=sys.stderr) + # Fall through to local + except Exception as e: + print(f"⚠ yt.biba.live API failed: {e}, fallback to local yt-dlp", file=sys.stderr) + + # ─── 2. Fallback: lokalni yt-dlp ─── cmd = ["yt-dlp", "--flat-playlist", "--dump-json"] - if cookies_file is None: - for candidate in ["/data/cookies/youtube.txt", os.environ.get("YT_COOKIES_FILE", "")]: - if candidate and Path(candidate).exists(): - cookies_file = candidate - break - if cookies_file and Path(cookies_file).exists(): + cookies_file = get_cookies_file(cookies_file) + if cookies_file: cmd += ["--cookies", str(cookies_file)] cmd.append(url) - print(f"📋 Resolving playlist: {url}", file=sys.stderr) + print(f"📋 Resolving playlist (lokalno): {url}", file=sys.stderr) result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) if result.returncode != 0: print(f"❌ yt-dlp playlist napaka:\n{result.stderr[-1500:]}", file=sys.stderr)