fix(downloads): android_vr primary + POT web fallback (fixes intermittent DRM/bot errors)

YouTube returns flaky per-client responses: the web clients intermittently mislabel normal
videos as 'DRM protected' (like tv did), and android_vr can get bot-flagged under heavy load.
Neither client alone is reliable. So:
- PRIMARY player client = android_vr: high-quality DASH (399+251), no PO token / Deno needed,
  reliable for normal use
- on a bot/DRM/format failure the worker retries with the FALLBACK set (web_safari,web,android
  + POT sidecar + Deno) — bypasses bot-detection when android_vr is flagged
- both client sets admin-tunable (DOWNLOAD_PLAYER_CLIENTS[_FALLBACK])

Verified: the previously DRM-failing video now downloads steadily via android_vr, no DRM error.
This commit is contained in:
npeter83 2026-07-03 23:01:09 +02:00
parent 21c5508889
commit 8588fa104b
2 changed files with 61 additions and 18 deletions

View file

@ -142,14 +142,25 @@ class Settings(BaseSettings):
# 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,android"
# yt-dlp player clients (comma-separated). PRIMARY = android_vr: high-quality DASH, no PO
# token / JS runtime needed, reliable for normal use. On a bot/DRM/format failure (YouTube's
# per-client responses are flaky, and android_vr can get bot-flagged under heavy load) the
# worker retries with the FALLBACK set — the POT-capable web clients (web needs the token +
# Deno) plus android for audio. Both admin-tunable when YouTube shifts what works.
download_player_clients: str = "android_vr"
download_player_clients_fallback: str = "web_safari,web,android"
@staticmethod
def _clients(raw: str) -> list[str]:
return [c.strip() for c in raw.split(",") if c.strip()]
@property
def player_client_list(self) -> list[str]:
return [c.strip() for c in self.download_player_clients.split(",") if c.strip()]
return self._clients(self.download_player_clients)
@property
def player_client_fallback_list(self) -> list[str]:
return self._clients(self.download_player_clients_fallback)
# 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.