From 59c305e023dace81f00915fd796d0cb2937ae439 Mon Sep 17 00:00:00 2001 From: sudacode Date: Tue, 28 Jul 2026 02:27:36 -0700 Subject: [PATCH] fix(anilist): stop requeueing searches that matched nothing A well-formed search returning zero media is deterministic for that title, so the retry queue just burns rate limit against it. Transient failures (network, GraphQL errors, malformed data) throw inside the query executor and are still caught as retryable, so only genuine no-matches become non-retryable. --- .../services/anilist/anilist-updater.test.ts | 30 +++++++++++++++++++ src/core/services/anilist/anilist-updater.ts | 8 ++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/core/services/anilist/anilist-updater.test.ts b/src/core/services/anilist/anilist-updater.test.ts index 3b99e1c4..d5e961f9 100644 --- a/src/core/services/anilist/anilist-updater.test.ts +++ b/src/core/services/anilist/anilist-updater.test.ts @@ -605,3 +605,33 @@ test('updateAnilistPostWatchProgress returns error when search fails', async () globalThis.fetch = originalFetch; } }); + +test('updateAnilistPostWatchProgress does not requeue a search that matched nothing', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => + createJsonResponse({ data: { Page: { media: [] } } })) as typeof fetch; + + try { + const result = await updateAnilistPostWatchProgress('token', 'Unknown Show', 3); + assert.equal(result.status, 'error'); + assert.equal(result.retryable, false); + assert.match(result.message, /no matches/i); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('updateAnilistPostWatchProgress still allows retry when the search itself fails', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => + createJsonResponse({ errors: [{ message: 'upstream exploded' }] })) as typeof fetch; + + try { + const result = await updateAnilistPostWatchProgress('token', 'Demo Show', 3); + assert.equal(result.status, 'error'); + assert.notEqual(result.retryable, false); + assert.match(result.message, /search failed/i); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/src/core/services/anilist/anilist-updater.ts b/src/core/services/anilist/anilist-updater.ts index bbb6da8f..21149fc2 100644 --- a/src/core/services/anilist/anilist-updater.ts +++ b/src/core/services/anilist/anilist-updater.ts @@ -305,7 +305,13 @@ export async function updateAnilistPostWatchProgress( } if (!resolution) { - return { status: 'error', message: 'AniList search returned no matches.' }; + // A well-formed search that matched nothing is deterministic for this title, so + // requeueing it just burns rate limit. Transient failures throw and are caught above. + return { + status: 'error', + retryable: false, + message: 'AniList search returned no matches.', + }; } if (!resolution.seasonResolved) { // Updating the season 1 entry here is worse than not updating at all.