fix: a YouTube stream refused after extraction is the gate, not a network fault
Measured on the isolated test stack the same night, on the very same video ~90 minutes after it was answering with the bot message: yt-dlp got the player response and picked its formats, and the CDN then refused the stream itself — "unable to download video data: HTTP Error 403: Forbidden". Signed out it did that on every attempt; with the owner's cookie attached the video downloaded in full. So the wall moves between the player response and the stream, and the text it wears there was costing the download twice. retriable: the format URLs belong to the client that minted them, so the next client set is a real second chance — but this text classifies as `network` and nothing in the YouTube marker list matched it, so the ladder ended the job on attempt 1 and never tried the fallback. catalog: `youtube.media_forbidden` for the cookie-less case, so the UI stops calling it a network error when nothing was wrong with the network, and names what actually gets past it. Scoped exactly as narrowly as the measurement: another host keeps the generic `network` category, and so does a download that already carried a cookie — telling a signed-in user to sign in is the failure the catalog exists to avoid. EN + HU copy.
This commit is contained in:
@@ -97,6 +97,13 @@ class YouTubeAdapter(SourceAdapter):
|
||||
# out below, so this is the safety net for a yt-dlp that renames a client or an admin who
|
||||
# configures both knobs with cookie-incapable clients only.
|
||||
"No player clients have been requested",
|
||||
# Measured 2026-09-16: a gated video whose EXTRACTION succeeds and whose media fetch is then
|
||||
# refused by the CDN ("unable to download video data: HTTP Error 403"). The wall moves
|
||||
# between the player response and the stream, so this is the same per-client flakiness one
|
||||
# step later — and it used to end the job on attempt 1, because the text classifies as
|
||||
# `network` and nothing here matched it. The format URLs belong to the client that minted
|
||||
# them, so the next client set is a real second chance.
|
||||
"unable to download video data",
|
||||
)
|
||||
# Measured against yt-dlp's own INNERTUBE_CLIENTS table (2026-09-16, on 2026.07.04 and
|
||||
# 2026.08.19): with account cookies attached yt-dlp REMOVES every client whose SUPPORTS_COOKIES
|
||||
|
||||
@@ -236,6 +236,19 @@ _CATALOG: tuple[_Entry, ...] = (
|
||||
detail="youtube.cookie_rejected",
|
||||
cookie=True,
|
||||
),
|
||||
# The same gate one step later: the player response comes back, and then the CDN refuses the
|
||||
# stream itself ("unable to download video data: HTTP Error 403"). Measured 2026-09-16 on the
|
||||
# very same video, ~90 minutes after it was answering with the bot message — signed out it
|
||||
# 403'd on every attempt; with the owner's cookie attached it downloaded in full (519 MB). So
|
||||
# the category this text carries generically, `network`, is a lie here: nothing was wrong with
|
||||
# the network. Scoped to a cookie-less YouTube download, where that is what we measured.
|
||||
_Entry(
|
||||
host="youtube",
|
||||
markers=("unable to download video data", "403"),
|
||||
code="login_required",
|
||||
detail="youtube.media_forbidden",
|
||||
cookie=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -115,6 +115,17 @@ def test_client_sets_and_retriable_are_per_source():
|
||||
# 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
|
||||
# The media fetch refused after a successful extraction: the format URLs belong to the client
|
||||
# that minted them, so the next client set is a real second chance (it used to end the job).
|
||||
assert (
|
||||
YouTubeAdapter().retriable(
|
||||
"ERROR: unable to download video data: HTTP Error 403: Forbidden"
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert (
|
||||
SourceAdapter().retriable("ERROR: unable to download video data: HTTP Error 403") is False
|
||||
)
|
||||
# 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()
|
||||
|
||||
@@ -119,6 +119,29 @@ def test_youtube_bot_wall_with_a_cookie_is_the_cookie_refused():
|
||||
)
|
||||
|
||||
|
||||
# Verbatim from the isolated test stack (2026-09-16), the SAME video ~90 minutes later: yt-dlp got
|
||||
# the player response and picked formats, and the CDN then refused the stream.
|
||||
_YT_MEDIA_403 = "ERROR: unable to download video data: HTTP Error 403: Forbidden"
|
||||
|
||||
|
||||
def test_youtube_media_403_is_the_wall_one_step_later_not_a_network_fault():
|
||||
# Generically this text is a network failure — and for every other host it stays one.
|
||||
assert classify(_YT_MEDIA_403) == "network"
|
||||
assert dlerrors.diagnose(_YT_MEDIA_403, url="https://vimeo.com/1") == ("network", None)
|
||||
# On a signed-out YouTube download it is the gate: measured, the same video downloaded in full
|
||||
# once the owner's cookie was attached. Calling that "network error" is the honest-states bug.
|
||||
assert dlerrors.diagnose(_YT_MEDIA_403, url=_YT) == (
|
||||
"login_required",
|
||||
"youtube.media_forbidden",
|
||||
)
|
||||
|
||||
|
||||
def test_youtube_media_403_with_a_cookie_is_not_claimed_as_the_gate():
|
||||
# Only the cookie-less case was measured. With a cookie the same text keeps its bare category
|
||||
# rather than telling a signed-in user to sign in.
|
||||
assert dlerrors.diagnose(_YT_MEDIA_403, url=_YT, cookie_attached=True) == ("network", None)
|
||||
|
||||
|
||||
def test_the_youtube_entries_never_match_another_site():
|
||||
# Host-scoped: the same words about a Facebook link stay the bare category.
|
||||
assert dlerrors.diagnose(
|
||||
|
||||
@@ -313,6 +313,10 @@
|
||||
"cookie_rejected": {
|
||||
"short": "YouTube didn't accept the cookies",
|
||||
"body": "YouTube rotates exported cookies as a security measure, so yours have probably expired. Export them again from a private browsing window, close that window without browsing on, paste them in under Settings → Account → Download authentication, then retry the download from the list."
|
||||
},
|
||||
"media_forbidden": {
|
||||
"short": "YouTube refused the video stream",
|
||||
"body": "YouTube described the video but then refused to hand over the file to a signed-out download. It comes and goes, so a retry from the list is worth one try — and adding your YouTube cookies under Settings → Account → Download authentication is what gets a stubborn one through."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +313,10 @@
|
||||
"cookie_rejected": {
|
||||
"short": "A YouTube nem fogadta el a cookie-kat",
|
||||
"body": "A YouTube biztonsági okból cserélgeti az exportált cookie-kat, ezért a tiéid valószínűleg lejártak. Exportáld őket újra egy privát böngészőablakból, majd zárd be azt az ablakot további böngészés nélkül, illeszd be a Beállítások → Fiók → Letöltés-hitelesítés alatt, majd a listában indítsd újra a letöltést."
|
||||
},
|
||||
"media_forbidden": {
|
||||
"short": "A YouTube megtagadta a videó adatfolyamát",
|
||||
"body": "A YouTube leírta a videót, de a fájlt már nem adta ki egy kijelentkezett letöltésnek. Ez jön-megy, ezért egy újrapróbálkozás a listából megér egy kísérletet — a makacs eseteken pedig a YouTube-cookie-k segítenek, a Beállítások → Fiók → Letöltés-hitelesítés alatt."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user