From edbb349c583fdcf04206e5559a4ded0c1b31be29 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 19 Apr 2026 14:07:50 -0400 Subject: [PATCH] fix(downloads): don't classify per-item download 404s as source-level Not Found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gallery-dl logs `[download][error] Failed to download .part` when a single post attachment's media URL expires or is deleted, then recovers and proceeds with the rest of the run. Our error categorizer was letting a urllib3 debug line like `"HEAD /media-u/v3/ HTTP/1.1" 404 0` match the NOT_FOUND_PATTERNS list, flagging the whole source as deleted even though gallery-dl kept downloading subsequent items successfully. Adds "failed to download" to the per-item allowlist so the mask flips when skip activity is present, matching the shape of the campaign-ID fix. Total-failure runs still classify as failures. Also renames the Download Details modal's "Error Log" panel to "Verbose Log (stderr)" — gallery-dl's `-v` flag sends all logging (including [debug] lines) to stderr, so the original label was misleading. Drops the blanket red text color for the same reason. --- backend/app/services/gallery_dl.py | 4 ++ .../services/test_gallery_dl_categorize.py | 37 +++++++++++++++++++ frontend/src/views/Downloads.vue | 4 +- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/backend/app/services/gallery_dl.py b/backend/app/services/gallery_dl.py index 8b70954..cf78560 100644 --- a/backend/app/services/gallery_dl.py +++ b/backend/app/services/gallery_dl.py @@ -441,11 +441,15 @@ class GalleryDLService: # - "failed to extract campaign id": logged when the vanity-URL HTML path # fails, but gallery-dl falls back to the /cw/ redirect and # recovers the campaign_id on its own. + # - "failed to download": per-item download failure (e.g., a single + # Patreon attachment whose media URL expired / was deleted). The + # downloader logs [download][error] and moves to the next item. if has_actual_error and (skip_line_count > 0 or has_skip_text): per_item_patterns = [ "not allowed to view", "unable to get post", "failed to extract campaign id", + "failed to download", ] error_lines = [ line for line in combined.split('\n') diff --git a/backend/tests/services/test_gallery_dl_categorize.py b/backend/tests/services/test_gallery_dl_categorize.py index 6740dbe..0cbaaa1 100644 --- a/backend/tests/services/test_gallery_dl_categorize.py +++ b/backend/tests/services/test_gallery_dl_categorize.py @@ -56,3 +56,40 @@ def test_patreon_campaign_id_error_alongside_real_error_still_fails(): error_type, _message = service._categorize_error(1, stdout, stderr) assert error_type == ErrorType.AUTH_ERROR + + +def test_patreon_per_item_download_404_with_skips_is_no_new_content(): + """Per-item 'Failed to download' + urllib3 404 HEAD + skips → no_new_content. + + Real-world trigger: a Patreon post's media URL (e.g., /media-u/v3/) + expired or was deleted. gallery-dl logs [download][error] and moves on; + other items skip cleanly. We shouldn't classify the whole source NOT_FOUND + just because one attachment 404'd. + """ + service = _make_service() + stderr = ( + "[download][error] Failed to download 02_46004845.part\n" + '[urllib3.connectionpool][debug] "HEAD /media-u/v3/46004845 HTTP/1.1" 404 0\n' + "[downloader.http][warning] '404 OK' for 'https://www.patreon.com/media-u/v3/46004845'\n" + "[download][error] Failed to download 03_46004845.part\n" + "[patreon][debug] skipping https://c10.patreonusercontent.com/4/patreon-media/p/post/30953341/.../1.png (abc image_large)\n" + ) + stdout = "" + + error_type, _message = service._categorize_error(1, stdout, stderr) + + assert error_type == ErrorType.NO_NEW_CONTENT + + +def test_patreon_per_item_download_failure_without_skips_still_fails(): + """Same 'Failed to download' error but no skip activity → real failure.""" + service = _make_service() + stderr = ( + "[download][error] Failed to download 02_46004845.part\n" + '[urllib3.connectionpool][debug] "HEAD /media-u/v3/46004845 HTTP/1.1" 404 0\n' + ) + stdout = "" + + error_type, _message = service._categorize_error(1, stdout, stderr) + + assert error_type != ErrorType.NO_NEW_CONTENT diff --git a/frontend/src/views/Downloads.vue b/frontend/src/views/Downloads.vue index 6844362..dfeb02e 100644 --- a/frontend/src/views/Downloads.vue +++ b/frontend/src/views/Downloads.vue @@ -334,9 +334,9 @@ - Error Log + Verbose Log (stderr) -
{{ selectedDownload.metadata.stderr }}
+
{{ selectedDownload.metadata.stderr }}