Failed downloads stored the raw yt-dlp/ffmpeg text and dumped it as a truncated red one-liner in the queue. Classify the failure into a category (unavailable / login_required / geo_blocked / format_unavailable / drm / postprocess / source_missing / network / unknown) and surface it properly: - backend: app/downloads/errors.classify maps the raw message to a category code; the worker stores it in the new download_jobs.error_code (migration 0061) at both failure sites; resume clears it; the serializer exposes it. Raw text kept as technical detail. - frontend: the queue row shows a clean localized reason + a "details" link opening a modal with the explanation, the raw text behind a disclosure, and a Retry only for transient categories (network/postprocess/unknown). - tests: classify() unit coverage over verbatim yt-dlp/ffmpeg message shapes.
48 lines
2.5 KiB
Python
48 lines
2.5 KiB
Python
"""Category classification of raw yt-dlp / ffmpeg download-failure strings (S2)."""
|
|
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: [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"
|