diff --git a/backend/app/config.py b/backend/app/config.py index e521bcd..2e99e35 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -138,6 +138,18 @@ class Settings(BaseSettings): # Whether THIS process runs the download worker loop. On for the dedicated worker # container, off for the API (which keeps the scheduler). Mirrors scheduler_enabled. worker_enabled: bool = False + # Base URL of the bgutil PO-token provider sidecar. When set, the download worker asks it + # for YouTube Proof-of-Origin tokens (bypasses bot-detection + unlocks high-quality + # formats). Empty disables it (yt-dlp still works, but may hit "confirm you're not a bot"). + download_pot_base_url: str = "http://bgutil-pot:4416" + # yt-dlp player clients to use (comma-separated). POT-capable clients (web_safari/web) must + # be used for the PO token to actually apply; the default clients (android_vr) don't request + # one. Empty = yt-dlp's own default. Admin-tunable when YouTube shifts what works. + download_player_clients: str = "web_safari,web,tv" + + @property + def player_client_list(self) -> list[str]: + return [c.strip() for c in self.download_player_clients.split(",") if c.strip()] # How many downloads the worker runs concurrently (claimed via FOR UPDATE SKIP LOCKED). download_worker_concurrency: int = 2 # How often (empty pending queue) the worker polls for a new job, in seconds. diff --git a/backend/app/downloads/formats.py b/backend/app/downloads/formats.py index 02e1f71..0fb927f 100644 --- a/backend/app/downloads/formats.py +++ b/backend/app/downloads/formats.py @@ -81,8 +81,17 @@ def target_ext(spec: dict) -> str: return s["container"] or "mp4" -def build_ydl_opts(spec: dict, outtmpl: str, progress_hook) -> dict: - """Assemble yt-dlp options for a single download to `outtmpl` (a full path template).""" +def build_ydl_opts( + spec: dict, + outtmpl: str, + progress_hook, + pot_base_url: str | None = None, + player_clients: list[str] | None = None, +) -> dict: + """Assemble yt-dlp options for a single download to `outtmpl` (a full path template). + + `pot_base_url` points the bgutil PO-token provider plugin at the sidecar server; + `player_clients` picks which YouTube clients to use (POT-capable ones like web use the token).""" s = normalize(spec) h = s["max_height"] opts: dict = { @@ -107,6 +116,17 @@ def build_ydl_opts(spec: dict, outtmpl: str, progress_hook) -> dict: "socket_timeout": 30, } + # Extractor args: which YouTube clients to use + where the bgutil PO-token sidecar lives + # (== --extractor-args "youtube:player_client=…;youtubepot-bgutilhttp:base_url=…"). Values + # are lists in the Python API. + ea: dict = {} + if player_clients: + ea["youtube"] = {"player_client": player_clients} + if pot_base_url: + ea["youtubepot-bgutilhttp"] = {"base_url": [pot_base_url]} + if ea: + opts["extractor_args"] = ea + if s["mode"] == "a": opts["format"] = "bestaudio/best" opts["postprocessors"].append( diff --git a/backend/app/worker.py b/backend/app/worker.py index 1475f1b..57db81e 100644 --- a/backend/app/worker.py +++ b/backend/app/worker.py @@ -293,7 +293,13 @@ def _download(job_id: int, asset_id: int, source_kind: str, source_ref: str, spe staging.mkdir(parents=True, exist_ok=True) outtmpl = str(staging / "%(id)s.%(ext)s") - opts = formats.build_ydl_opts(spec, outtmpl, _make_progress_hook(job_id, asset_id)) + opts = formats.build_ydl_opts( + spec, + outtmpl, + _make_progress_hook(job_id, asset_id), + settings.download_pot_base_url, + settings.player_client_list, + ) opts["postprocessor_hooks"] = [_make_pp_hook(job_id)] url = service.source_url(source_kind, source_ref) diff --git a/backend/requirements.txt b/backend/requirements.txt index 2e293ce..2e9563c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -16,4 +16,8 @@ argon2-cffi>=23.1,<26 # Download center: yt-dlp does the extraction/download; ffmpeg (installed at the OS level in # the Dockerfile) does the merge + postprocessing. yt-dlp is release-often (YouTube changes), # so keep the floor loose and bump it when extraction breaks. -yt-dlp>=2025.1 +yt-dlp>=2025.5.22 +# PO Token provider plugin: talks to the bgutil sidecar (brainicism/bgutil-ytdlp-pot-provider) +# to mint YouTube Proof-of-Origin tokens on demand, bypassing "confirm you're not a bot" and +# unlocking high-quality formats. Pin to match the sidecar image tag (versions must align). +bgutil-ytdlp-pot-provider==1.3.1 diff --git a/docker-compose.localdev.yml b/docker-compose.localdev.yml index 2cac4c0..48b06ff 100644 --- a/docker-compose.localdev.yml +++ b/docker-compose.localdev.yml @@ -81,11 +81,23 @@ services: depends_on: db: condition: service_healthy + bgutil-pot: + condition: service_started security_opt: - apparmor:unconfined volumes: - ./downloads:/downloads restart: unless-stopped + # PO-token provider: mints YouTube Proof-of-Origin tokens on demand so the worker bypasses + # bot-detection and gets high-quality formats. The worker reaches it at http://bgutil-pot:4416 + # (DOWNLOAD_POT_BASE_URL). Pin the tag to match the plugin version in requirements.txt. + bgutil-pot: + image: brainicism/bgutil-ytdlp-pot-provider:1.3.1 + init: true + security_opt: + - apparmor:unconfined + restart: unless-stopped + volumes: pgdata: