From 4c42a15fa1a1d9b474a866340baa15052689dede Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 6 Jun 2026 23:10:46 -0400 Subject: [PATCH] fix(patreon): a missing media file_name is a URL-basename fallback, not API drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native client treated a gallery image without `file_name` as schema drift and raised "Patreon API changed — ingester needs update", failing the whole walk (operator-flagged 2026-06-07: BlenderKnight post 73665615, kind=images). But the resource had a valid URL, and the code already derives a filename from the URL basename right below the raise — the same fallback gallery-dl uses. Patreon legitimately serves some images without file_name, so this isn't drift. Drop the require_file_name gate from _media_item: file_name is now optional for every kind (images/attachments/postfile), falling back to the URL basename. Genuine drift still raises — no resolvable URL, or a media id referenced by a relationship but absent from `included`. Test updated to assert the fallback. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/services/patreon_client.py | 20 +++++++------------- tests/test_patreon_client.py | 13 ++++++++----- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/backend/app/services/patreon_client.py b/backend/app/services/patreon_client.py index 98d8134..b896c4b 100644 --- a/backend/app/services/patreon_client.py +++ b/backend/app/services/patreon_client.py @@ -405,21 +405,19 @@ class PatreonClient: return candidate return None - def _media_item( - self, attrs: dict, kind: str, post_id: str, *, require_file_name: bool - ) -> MediaItem: + def _media_item(self, attrs: dict, kind: str, post_id: str) -> MediaItem: url = self._media_url(attrs) if not url: raise PatreonDriftError( f"Patreon media (post {post_id}, kind={kind}) had no resolvable URL " f"(no download_url / image_urls)" ) + # file_name is OPTIONAL: Patreon legitimately serves some gallery images + # without it (operator-flagged 2026-06-07, BlenderKnight post 73665615), + # and the URL basename is a fine fallback — the same thing gallery-dl + # uses. A genuine schema change shows up as no URL (above) or a media id + # absent from `included` (caller), not a missing name. file_name = attrs.get("file_name") - if require_file_name and not (isinstance(file_name, str) and file_name): - raise PatreonDriftError( - f"Patreon media resource (post {post_id}, kind={kind}) missing " - f"'file_name'" - ) filename = file_name if isinstance(file_name, str) and file_name else _basename_from_url(url) return MediaItem( url=url, @@ -452,11 +450,7 @@ class PatreonClient: f"Patreon post {post_id} references media {mid} " f"({rel_name}) not present in 'included'" ) - items.append( - self._media_item( - media_attrs, kind, post_id, require_file_name=True - ) - ) + items.append(self._media_item(media_attrs, kind, post_id)) # 1. gallery images _resolve_rel("images", "images") diff --git a/tests/test_patreon_client.py b/tests/test_patreon_client.py index 0426147..fe46b91 100644 --- a/tests/test_patreon_client.py +++ b/tests/test_patreon_client.py @@ -159,9 +159,10 @@ def test_non_list_data_raises_drift(client): client._validate_response({"data": {"id": "1"}}) -def test_media_missing_file_name_raises_drift(client): - # A media resource referenced by a gallery relationship but lacking - # file_name is drift from the contract. +def test_media_missing_file_name_falls_back_to_url_basename(client): + # A gallery image with a valid URL but no file_name is NOT drift — Patreon + # serves these (operator-flagged 2026-06-07, BlenderKnight post 73665615); + # fall back to the URL basename like gallery-dl does. post = { "id": "2000", "type": "post", @@ -175,8 +176,10 @@ def test_media_missing_file_name_raises_drift(client): + "/x.jpg" } } - with pytest.raises(PatreonDriftError): - client.extract_media(post, index) + items = client.extract_media(post, index) + assert len(items) == 1 + assert items[0].filename == "x.jpg" + assert items[0].kind == "images" def test_media_referenced_but_absent_raises_drift(client):