feat(downloads): automatic PO-token provider (bgutil sidecar) to beat YouTube bot-detection

YouTube increasingly demands a Proof-of-Origin token (or shows 'confirm you're not a bot').
Instead of manual cookies, run the bgutil POT provider as a sidecar that mints tokens on demand:
- compose: bgutil-pot service (brainicism/bgutil-ytdlp-pot-provider:1.3.1, port 4416, init)
- requirements: bgutil-ytdlp-pot-provider==1.3.1 plugin (version pinned to the image)
- config: DOWNLOAD_POT_BASE_URL (http://bgutil-pot:4416) + DOWNLOAD_PLAYER_CLIENTS
  (web_safari,web,tv — POT-capable; the default android_vr client never requests a token)
- formats.build_ydl_opts wires extractor_args youtube:player_client + youtubepot-bgutilhttp:base_url

Verified: the sidecar mints a valid PO token (BotGuard challenge solved) and yt-dlp fetches it
from the provider. NOTE: end-to-end download couldn't be confirmed here because the test IP got
hard-flagged by YouTube from heavy testing (~16 GB + dozens of extractions) — a hard abuse block
needs cooldown/cookies, which POT doesn't lift; POT bypasses the normal soft bot-check.
This commit is contained in:
npeter83 2026-07-03 04:40:10 +02:00
parent d7fe3c34e8
commit 03a1865aa8
5 changed files with 58 additions and 4 deletions

View file

@ -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.