The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
236 lines
12 KiB
Python
236 lines
12 KiB
Python
"""Category classification of raw yt-dlp / ffmpeg download-failure strings."""
|
||
|
||
from app.downloads import adapters
|
||
from app.downloads import errors as dlerrors
|
||
from app.downloads.errors import classify
|
||
|
||
|
||
def test_real_ytdlp_messages_map_to_categories():
|
||
# Verbatim-shaped yt-dlp / ffmpeg output → the category the UI explains.
|
||
cases = {
|
||
"ERROR: [youtube] xxxxxxxxxxx: Video unavailable": "unavailable",
|
||
"ERROR: [youtube] abc: Private video. Sign in if you've been granted access": "unavailable",
|
||
"ERROR: [youtube] abc: This video is no longer available because the uploader has closed their "
|
||
"account": "unavailable",
|
||
"ERROR: [youtube] abc: Sign in to confirm your age. This video may be inappropriate": "login_required",
|
||
"ERROR: [youtube] abc: Sign in to confirm you're not a bot": "login_required",
|
||
"ERROR: [youtube] abc: Join this channel to get access to members-only content": "login_required",
|
||
"ERROR: [Reddit] 1vi1x56: Account authentication is required.": "login_required",
|
||
# Closed-group / private-post auth walls: yt-dlp either says to sign in or suggests cookies.
|
||
"ERROR: [facebook] 123: You must be logged in to see this post": "login_required",
|
||
"ERROR: [facebook] 123: Login required. Use --cookies for the authentication.": "login_required",
|
||
"ERROR: [instagram] abc: This content is only available for registered users": "login_required",
|
||
"ERROR: [youtube] abc: Sign in to confirm... Use --cookies-from-browser or --cookies": "login_required",
|
||
# A site yt-dlp has no extractor for at all — distinct from an auth wall, still non-retryable.
|
||
"ERROR: Unsupported URL: https://example.com/whatever": "unsupported_host",
|
||
"ERROR: [youtube] abc: The uploader has not made this video available in your country": "geo_blocked",
|
||
"ERROR: [youtube] abc: Requested format is not available": "format_unavailable",
|
||
"ERROR: [youtube] abc: Only images are available for download": "format_unavailable",
|
||
"ERROR: [generic] This video is DRM protected": "drm",
|
||
"ffmpeg failed (1): Conversion failed!": "postprocess",
|
||
"The source download is no longer available.": "source_missing",
|
||
"The source file is missing.": "source_missing",
|
||
"ERROR: unable to download video data: HTTP Error 403: Forbidden": "network",
|
||
"ERROR: Unable to download webpage: <urlopen error timed out>": "network",
|
||
}
|
||
for msg, expected in cases.items():
|
||
assert classify(msg) == expected, f"{msg!r} → {classify(msg)!r}, expected {expected!r}"
|
||
|
||
|
||
def test_unmatched_and_empty_are_unknown():
|
||
assert classify("some totally novel failure text") == "unknown"
|
||
assert classify("") == "unknown"
|
||
assert classify(None) == "unknown"
|
||
|
||
|
||
def test_matching_is_case_insensitive():
|
||
assert classify("VIDEO UNAVAILABLE") == "unavailable"
|
||
assert classify("drm protected") == "drm"
|
||
|
||
|
||
def test_login_beats_generic_unavailable_when_both_present():
|
||
# A members-only video also reads as "unavailable" to a signed-out client; the sign-in cause is
|
||
# the more useful, more specific one, so it must win (rule order guards this).
|
||
msg = (
|
||
"ERROR: [youtube] abc: Join this channel to get access to members-only content. "
|
||
"This video is unavailable"
|
||
)
|
||
assert classify(msg) == "login_required"
|
||
|
||
|
||
def test_our_own_worker_refusals_classify_as_themselves():
|
||
# The worker raises these itself (not yt-dlp), and the rule that catches each one matches only a
|
||
# fragment of the text. Pinning message and rule together is what stops an edit to the wording
|
||
# from silently orphaning the category — the UI would then fall back to "Download failed".
|
||
assert classify(dlerrors.QUOTA_FULL_MESSAGE) == "quota_exceeded"
|
||
|
||
|
||
def test_the_redacted_network_text_classifies_back_to_network():
|
||
# The redacted text is also what a failed asset keeps, and a second job that meets that asset is
|
||
# diagnosed from it — without its own rule it would read "Download failed".
|
||
assert classify(dlerrors.NETWORK_REDACTED_MESSAGE) == "network"
|
||
|
||
|
||
# Verbatim yt-dlp 2026.07.04 output from prod jobs, measured on dev 2026-09-13.
|
||
_FB_PARSE = (
|
||
"ERROR: [facebook] 1736360027626346: Cannot parse data; please report this issue on "
|
||
"https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template."
|
||
)
|
||
_FB_NO_FORMATS = (
|
||
"ERROR: [facebook] 1755988225525547: No video formats found!; please report this issue on "
|
||
"https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template."
|
||
)
|
||
_REEL = "https://www.facebook.com/reel/1736360027626346/?fs=e&fs=e"
|
||
_GROUP_POST = "https://www.facebook.com/groups/983554479435596/permalink/1755988225525547/?"
|
||
|
||
|
||
# Verbatim from prod job 273 (2026-09-15). YouTube writes the apostrophe as U+2019, which is why
|
||
# neither the rule nor the catalog entry may spell out "you’re" with a straight quote.
|
||
_YT_BOT_WALL = (
|
||
"ERROR: [youtube] 7JPmS_bWjhI: Sign in to confirm you’re not a bot. Use "
|
||
"--cookies-from-browser or --cookies for the authentication. See "
|
||
"https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp for how to manually "
|
||
"pass cookies."
|
||
)
|
||
_YT = "https://www.youtube.com/watch?v=7JPmS_bWjhI"
|
||
|
||
|
||
def test_youtube_bot_wall_is_that_video_own_signed_out_gate():
|
||
# Measured on prod: all 12 player clients fail on this video while its neighbours download, the
|
||
# POT sidecar answers, and the newest yt-dlp fails identically — a cookie is the only way in.
|
||
assert classify(_YT_BOT_WALL) == "login_required"
|
||
for url in (
|
||
_YT,
|
||
"https://youtu.be/7JPmS_bWjhI",
|
||
"https://www.youtube-nocookie.com/embed/7JPmS_bWjhI",
|
||
"https://music.youtube.com/watch?v=7JPmS_bWjhI",
|
||
):
|
||
assert dlerrors.diagnose(_YT_BOT_WALL, url=url) == (
|
||
"login_required",
|
||
"youtube.bot_wall",
|
||
), url
|
||
|
||
|
||
def test_youtube_bot_wall_with_a_cookie_is_the_cookie_refused():
|
||
# Telling a user who already gave a cookie to add one is what this entry exists to avoid.
|
||
assert dlerrors.diagnose(_YT_BOT_WALL, url=_YT, cookie_attached=True) == (
|
||
"login_required",
|
||
"youtube.cookie_rejected",
|
||
)
|
||
|
||
|
||
# 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(
|
||
"ERROR: [facebook] 1: Sign in to confirm you’re not a bot",
|
||
url="https://www.facebook.com/reel/1/",
|
||
) == ("login_required", None)
|
||
|
||
|
||
def test_facebook_cannot_parse_data_is_the_login_wall_without_a_cookie():
|
||
for url in (
|
||
_REEL,
|
||
_GROUP_POST,
|
||
"https://fb.watch/Ia_iJYI2Vw/?fs=e",
|
||
"https://m.facebook.com/x",
|
||
):
|
||
assert dlerrors.diagnose(_FB_PARSE, url=url) == ("login_required", "facebook.login_wall")
|
||
|
||
|
||
def test_facebook_cannot_parse_data_with_a_cookie_is_the_cookie_refused():
|
||
# Telling a user who already gave a cookie to add one is the failure this entry exists to avoid.
|
||
assert dlerrors.diagnose(_FB_PARSE, url=_REEL, cookie_attached=True) == (
|
||
"login_required",
|
||
"facebook.cookie_rejected",
|
||
)
|
||
|
||
|
||
def test_a_facebook_group_post_without_formats_names_the_link():
|
||
# Without a cookie yt-dlp is given the post link itself (measured, prod jobs 66 and 145).
|
||
assert dlerrors.diagnose(_FB_NO_FORMATS, url=_GROUP_POST) == (
|
||
"format_unavailable",
|
||
"facebook.group_post_video",
|
||
)
|
||
# With one it is only ever given the post's VIDEO link, so the text is about that video: the adapter
|
||
# found it, and "this post has no video" would be false.
|
||
assert dlerrors.diagnose(_FB_NO_FORMATS, url=_GROUP_POST, cookie_attached=True) == (
|
||
"format_unavailable",
|
||
None,
|
||
)
|
||
# A reel, or a group link that is not a post, is another failure: only the category.
|
||
for url in (_REEL, "https://www.facebook.com/groups/983554479435596/"):
|
||
assert dlerrors.diagnose(_FB_NO_FORMATS, url=url) == ("format_unavailable", None), url
|
||
|
||
|
||
def test_our_post_not_shown_verdict_is_the_refused_cookie():
|
||
# The adapter's text when a signed-in read of the post got a page without the post (the login page).
|
||
msg = adapters.POST_NOT_SHOWN_MESSAGE
|
||
assert classify(msg) == "login_required"
|
||
assert dlerrors.diagnose(msg, url=_GROUP_POST, cookie_attached=True) == (
|
||
"login_required",
|
||
"facebook.cookie_rejected",
|
||
)
|
||
|
||
|
||
def test_our_post_without_a_video_verdict_names_the_group_link():
|
||
# The adapter's own text when the post page holds no single video: the message and its rule and
|
||
# catalog entry are pinned together, or an edit to the wording orphans the copy.
|
||
msg = adapters.POST_WITHOUT_VIDEO_MESSAGE
|
||
assert classify(msg) == "format_unavailable"
|
||
assert dlerrors.diagnose(msg, url=_GROUP_POST, cookie_attached=True) == (
|
||
"format_unavailable",
|
||
"facebook.group_post_video",
|
||
)
|
||
assert dlerrors.diagnose(msg, url=_REEL, cookie_attached=True) == ("format_unavailable", None)
|
||
|
||
|
||
def test_a_catalog_entry_never_leaves_its_host():
|
||
# "Cannot parse data" is yt-dlp's wording for a broken extractor on every site.
|
||
other = _FB_PARSE.replace("[facebook]", "[vimeo]")
|
||
assert dlerrors.diagnose(other, url="https://vimeo.com/1") == ("unknown", None)
|
||
# The Facebook text for a link that is not Facebook's (a look-alike host) is not the Facebook wall.
|
||
assert dlerrors.diagnose(_FB_PARSE, url="https://facebook.com.evil/reel/1") == ("unknown", None)
|
||
# No link known: the category only.
|
||
assert dlerrors.diagnose(_FB_PARSE) == ("unknown", None)
|
||
|
||
|
||
def test_diagnose_falls_back_to_the_category():
|
||
msg = "ERROR: [facebook] 123: You must be logged in to see this post"
|
||
assert dlerrors.diagnose(msg, url=_REEL) == ("login_required", None)
|
||
assert dlerrors.diagnose(None, url=_REEL) == ("unknown", None)
|
||
|
||
|
||
def test_a_url_jobs_network_failure_is_redacted_but_youtube_keeps_its_text():
|
||
# SEC-12: the raw transport text for a user-chosen host:port is a port-scanner readout, so it is
|
||
# replaced for `url` jobs. Everything else must survive verbatim, or a real diagnosis is lost.
|
||
raw = "ERROR: Unable to download webpage: <urlopen error [Errno 111] Connection refused>"
|
||
assert dlerrors.user_error_text("url", raw, "network") == dlerrors.NETWORK_REDACTED_MESSAGE
|
||
# Same failure, but the destination was never user-chosen — nothing to leak, keep the detail.
|
||
assert dlerrors.user_error_text("youtube", raw, "network") == raw
|
||
# A user-chosen host, but a category that says something real about the CONTENT, not the network.
|
||
login = "ERROR: [facebook] 123: You must be logged in to see this post"
|
||
assert dlerrors.user_error_text("url", login, "login_required") == login
|