fix: a YouTube video behind the bot wall is downloadable with cookies
A YouTube link can fail with "Sign in to confirm you're not a bot" while every other YouTube download works — YouTube gates INDIVIDUAL videos behind a signed-in session. Measured on prod job 273: all twelve of yt-dlp's player clients fail on that one video while its neighbours download fine, the bgutil PO-token sidecar is live and answering, and the newest yt-dlp fails identically. So the download is not broken by our stack or a stale yt-dlp, and no retry, client switch or upgrade gets past it. A cookie is the only way in. The failure copy already sends the user to Settings -> Account -> Download authentication, but YouTube was not in the cookie host registry, so that dropdown could not help them: the app asked for something it did not offer. credentials: YouTube joins SUPPORTED_HOSTS (youtube.com, youtu.be, youtube-nocookie.com). The Settings dropdown, the SEC-12 host scope and the private-asset cache salt all derive from the row, so that is the whole wiring. adapters: a signed-in YouTube download runs only cookie-capable player clients. Measured against yt-dlp's own INNERTUBE_CLIENTS table: with account cookies attached yt-dlp REMOVES every client whose SUPPORTS_COOKIES is False, and a set it empties that way raises "No player clients have been requested" — which is not a per-client flake it retries past. Our primary set is android_vr, one of those, so without this the cookie would have failed the download on attempt 1 and never reached the POT-backed web fallback it exists for. A test pins the client list against the installed yt-dlp so a version bump turns the gate red instead of silently un-authing every YouTube download, and the empty-set message joins the retriable markers as the second line of defence. errors: catalog entries for the wall with and without a cookie, so a user who has already added cookies is told they were rejected (YouTube rotates exported ones) rather than to add some. The marker is quote-agnostic: YouTube writes the apostrophe as U+2019, so "you're not a bot" with a straight quote never matches the real text. EN + HU copy.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Per-source adapter selection + the source-specific policy each one carries. These lock the
|
||||
behaviour that used to live inline in worker.build_uploader_url / backfill._should_refetch."""
|
||||
|
||||
from app.config import settings
|
||||
from app.titles import normalize_title
|
||||
from app.downloads.adapters import (
|
||||
FacebookAdapter,
|
||||
@@ -111,6 +112,9 @@ def test_client_sets_and_retriable_are_per_source():
|
||||
assert SourceAdapter().retriable("Sign in to confirm you're not a bot") is False
|
||||
assert YouTubeAdapter().retriable("Sign in to confirm you're not a bot") is True
|
||||
assert YouTubeAdapter().retriable("some unrelated network blip") is False
|
||||
# The empty-set error of a cookie-incapable client list (see _COOKIE_CAPABLE): retriable, so an
|
||||
# admin who configures one still gets the next set tried instead of a hard failure.
|
||||
assert YouTubeAdapter().retriable("ERROR: No player clients have been requested") is True
|
||||
# Facebook: a second, Chrome-impersonating attempt only WITH the user's cookie, and only behind
|
||||
# the parse wall (without a cookie that wall is the login wall — no disguise gets past it).
|
||||
fb = FacebookAdapter()
|
||||
@@ -121,6 +125,37 @@ def test_client_sets_and_retriable_are_per_source():
|
||||
assert fb.retriable("ERROR: [facebook] 1: No video formats found!") is False
|
||||
|
||||
|
||||
def test_a_signed_in_youtube_download_only_runs_cookie_capable_clients(monkeypatch):
|
||||
yt = YouTubeAdapter()
|
||||
monkeypatch.setattr(settings, "download_player_clients", "android_vr")
|
||||
monkeypatch.setattr(settings, "download_player_clients_fallback", "web_safari,web,android")
|
||||
# Signed out: the admin's ladder exactly as configured.
|
||||
assert yt.client_sets() == [["android_vr"], ["web_safari", "web", "android"]]
|
||||
# Signed in: yt-dlp REMOVES every client that takes no cookies, and a set it empties that way
|
||||
# raises "No player clients have been requested" — which would fail the download on attempt 1,
|
||||
# before the POT-backed web fallback the cookie exists for. So the adapter never asks for one:
|
||||
# the android_vr-only set is gone and `android` is dropped out of the fallback.
|
||||
assert yt.client_sets(authed=True) == [["web_safari", "web"]]
|
||||
|
||||
|
||||
def test_a_signed_in_download_still_has_clients_when_the_admin_configured_none(monkeypatch):
|
||||
# Both knobs cookie-incapable: fall back to the POT-backed web pair rather than no attempt at all.
|
||||
monkeypatch.setattr(settings, "download_player_clients", "android_vr")
|
||||
monkeypatch.setattr(settings, "download_player_clients_fallback", "android,ios")
|
||||
assert YouTubeAdapter().client_sets(authed=True) == [["web_safari", "web"]]
|
||||
|
||||
|
||||
def test_the_cookie_capable_clients_match_the_installed_ytdlp():
|
||||
# The list is yt-dlp's, not ours (INNERTUBE_CLIENTS[...]["SUPPORTS_COOKIES"]). Pinned here so a
|
||||
# yt-dlp bump that renames or re-flags a client turns the gate red, instead of silently sending
|
||||
# a signed-in download to a client that drops the cookie.
|
||||
from yt_dlp.extractor.youtube._base import INNERTUBE_CLIENTS
|
||||
|
||||
live = {k for k, v in INNERTUBE_CLIENTS.items() if v.get("SUPPORTS_COOKIES")}
|
||||
assert live == set(YouTubeAdapter._COOKIE_CAPABLE)
|
||||
assert set(YouTubeAdapter._AUTHED_DEFAULT) <= live
|
||||
|
||||
|
||||
def test_youtube_ydl_opts_extra_carries_player_client_and_pot():
|
||||
ea = YouTubeAdapter().ydl_opts_extra(["android_vr"])["extractor_args"]
|
||||
assert ea["youtube"]["player_client"] == ["android_vr"]
|
||||
|
||||
Reference in New Issue
Block a user