edbb349c58
gallery-dl logs `[download][error] Failed to download <file>.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/<id> 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.
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""Unit tests for GalleryDLService._categorize_error.
|
|
|
|
Focus: making sure errors gallery-dl logs but recovers from (e.g., Patreon's
|
|
"Failed to extract campaign ID" that falls back to /cw/<vanity>) don't get
|
|
misclassified as real failures when the overall run was just archive skips.
|
|
"""
|
|
from unittest.mock import patch
|
|
|
|
from app.services.gallery_dl import ErrorType, GalleryDLService
|
|
|
|
|
|
def _make_service():
|
|
"""Build a service without touching the filesystem."""
|
|
with patch.object(GalleryDLService, "_load_base_config", return_value={}):
|
|
return GalleryDLService(rate_limit=1.0)
|
|
|
|
|
|
def test_patreon_campaign_id_error_with_skips_is_no_new_content():
|
|
"""gallery-dl logged the extractor error but recovered + skipped all → no_new_content."""
|
|
service = _make_service()
|
|
stderr = (
|
|
"[patreon][error] Failed to extract campaign ID\n"
|
|
"[patreon][info] campaign_id: 548223\n"
|
|
"[patreon][api] HEAD https://c10.patreonusercontent.com/4/.../img.jpg\n"
|
|
)
|
|
stdout = (
|
|
"# /data/downloads/ElArteDeVero/patreon/2024-01-01_1_title/01_img.jpg\n"
|
|
"# /data/downloads/ElArteDeVero/patreon/2024-01-02_2_title/01_img.jpg\n"
|
|
)
|
|
|
|
error_type, _message = service._categorize_error(1, stdout, stderr)
|
|
|
|
assert error_type == ErrorType.NO_NEW_CONTENT
|
|
|
|
|
|
def test_patreon_campaign_id_error_without_skips_still_fails():
|
|
"""Same error line but no skip activity → genuine failure, not masked."""
|
|
service = _make_service()
|
|
stderr = "[patreon][error] Failed to extract campaign ID\n"
|
|
stdout = ""
|
|
|
|
error_type, _message = service._categorize_error(1, stdout, stderr)
|
|
|
|
assert error_type != ErrorType.NO_NEW_CONTENT
|
|
|
|
|
|
def test_patreon_campaign_id_error_alongside_real_error_still_fails():
|
|
"""Campaign-ID error + a genuine auth error → auth error wins, not masked."""
|
|
service = _make_service()
|
|
stderr = (
|
|
"[patreon][error] Failed to extract campaign ID\n"
|
|
'[patreon][error] "GET /api/posts HTTP/1.1" 401 None\n'
|
|
)
|
|
stdout = "# /data/downloads/Foo/patreon/post1/img.jpg\n"
|
|
|
|
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/<id>)
|
|
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
|