mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-30 19:21:32 -07:00
fix(anilist): stop persisting wrong-season data on unresolved seasons
Two cases where an unresolved season still produced a confident wrong answer: - Cover art warned but then wrote the season 1 anilistId and episodesTotal via upsertCoverArt/updateAnimeAnilistInfo. Since a cached coverBlob short-circuits the next call, that metadata never re-resolved. Keep the artwork as a stand-in but withhold the id, titles and episode count when the season is unresolved. - pickByAirOrder sorted entries with no air year last, shifting every later season by one while still reporting seasonResolved. Decline instead when any franchise entry lacks a year.
This commit is contained in:
@@ -356,3 +356,78 @@ test('fetchIfMissing falls back to internal parser when guessit throws', async (
|
|||||||
cleanupDbPath(dbPath);
|
cleanupDbPath(dbPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('fetchIfMissing keeps the cover but withholds metadata when the season is unresolved', async () => {
|
||||||
|
const dbPath = makeDbPath();
|
||||||
|
const db = new Database(dbPath);
|
||||||
|
ensureSchema(db);
|
||||||
|
const videoId = getOrCreateVideoRecord(db, 'local:/tmp/cover-fetcher-unresolved.mkv', {
|
||||||
|
canonicalTitle: 'Unresolved Show (2013) - S03E01 - Something [1080p].mkv',
|
||||||
|
sourcePath: '/tmp/cover-fetcher-unresolved.mkv',
|
||||||
|
sourceType: SOURCE_TYPE_LOCAL,
|
||||||
|
sourceUrl: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
||||||
|
if (!url.includes('graphql')) {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(Buffer.from('01020304'), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'image/png' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.parse(String(init?.body ?? '{}')) as {
|
||||||
|
variables?: { search?: string; id?: number };
|
||||||
|
};
|
||||||
|
if (typeof payload.variables?.id === 'number') {
|
||||||
|
// No sequel edges, so season 3 cannot be reached from the season 1 anchor.
|
||||||
|
return Promise.resolve(createJsonResponse({ data: { Media: { relations: { edges: [] } } } }));
|
||||||
|
}
|
||||||
|
return Promise.resolve(
|
||||||
|
createJsonResponse({
|
||||||
|
data: {
|
||||||
|
Page: {
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
id: 55,
|
||||||
|
episodes: 13,
|
||||||
|
format: 'TV',
|
||||||
|
seasonYear: 2013,
|
||||||
|
coverImage: { large: 'https://images.test/s1.jpg', medium: null },
|
||||||
|
title: { romaji: 'Unresolved Show', english: 'Unresolved Show', native: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}) as typeof fetch;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetcher = createCoverArtFetcher(
|
||||||
|
{ acquire: async () => {}, recordResponse: () => {} },
|
||||||
|
console,
|
||||||
|
{
|
||||||
|
runGuessit: async () =>
|
||||||
|
JSON.stringify({ title: 'Unresolved Show', season: 3, episode: 1, year: 2013 }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetched = await fetcher.fetchIfMissing(db, videoId, 'Unresolved Show');
|
||||||
|
const stored = getCoverArt(db, videoId);
|
||||||
|
|
||||||
|
assert.equal(fetched, true);
|
||||||
|
// Artwork is a reasonable stand-in; the season 1 identity and episode count are not.
|
||||||
|
assert.ok(stored?.coverBlob);
|
||||||
|
assert.equal(stored?.anilistId, null);
|
||||||
|
assert.equal(stored?.episodesTotal, null);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
db.close();
|
||||||
|
cleanupDbPath(dbPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -299,27 +299,34 @@ export function createCoverArtFetcher(
|
|||||||
coverBlob = await downloadImage(coverUrl);
|
coverBlob = await downloadImage(coverUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An unresolved season means `selected` is the season 1 entry. Its artwork is a
|
||||||
|
// reasonable stand-in, but its id and episode count are not: they feed anime-level
|
||||||
|
// metadata that would otherwise be cached wrong and never re-resolved.
|
||||||
|
const seasonResolved = resolution?.seasonResolved !== false;
|
||||||
|
|
||||||
upsertCoverArt(db, videoId, {
|
upsertCoverArt(db, videoId, {
|
||||||
anilistId: selected.id,
|
anilistId: seasonResolved ? selected.id : null,
|
||||||
coverUrl,
|
coverUrl,
|
||||||
coverBlob,
|
coverBlob,
|
||||||
titleRomaji: selected.title?.romaji ?? null,
|
titleRomaji: seasonResolved ? (selected.title?.romaji ?? null) : null,
|
||||||
titleEnglish: selected.title?.english ?? null,
|
titleEnglish: seasonResolved ? (selected.title?.english ?? null) : null,
|
||||||
episodesTotal: selected.episodes ?? null,
|
episodesTotal: seasonResolved ? (selected.episodes ?? null) : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateAnimeAnilistInfo(db, videoId, {
|
if (seasonResolved) {
|
||||||
anilistId: selected.id,
|
updateAnimeAnilistInfo(db, videoId, {
|
||||||
titleRomaji: selected.title?.romaji ?? null,
|
anilistId: selected.id,
|
||||||
titleEnglish: selected.title?.english ?? null,
|
titleRomaji: selected.title?.romaji ?? null,
|
||||||
titleNative: selected.title?.native ?? null,
|
titleEnglish: selected.title?.english ?? null,
|
||||||
episodesTotal: selected.episodes ?? null,
|
titleNative: selected.title?.native ?? null,
|
||||||
});
|
episodesTotal: selected.episodes ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'cover-art: cached art for videoId=%d anilistId=%d title="%s"',
|
'cover-art: cached art for videoId=%d anilistId=%s title="%s"',
|
||||||
videoId,
|
videoId,
|
||||||
selected.id,
|
seasonResolved ? String(selected.id) : 'none (season unresolved)',
|
||||||
selected.title?.romaji ?? searchBase,
|
selected.title?.romaji ?? searchBase,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -287,3 +287,21 @@ test('refuses a season beyond the sequel-hop cap instead of returning a partial
|
|||||||
// Declines before spending any relation requests.
|
// Declines before spending any relation requests.
|
||||||
assert.deepEqual(relationLookups, []);
|
assert.deepEqual(relationLookups, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('air-order fallback declines when a franchise entry has no air year', async () => {
|
||||||
|
// The middle season has no year, so ordering it would shift every later season by one.
|
||||||
|
const { execute } = createExecutor(
|
||||||
|
[
|
||||||
|
{ id: 1, episodes: 12, format: 'TV', seasonYear: 2013, title: { english: 'Show' } },
|
||||||
|
{ id: 2, episodes: 12, format: 'TV', title: { english: 'Show Zoku' } },
|
||||||
|
{ id: 3, episodes: 12, format: 'TV', seasonYear: 2020, title: { english: 'Show Kan' } },
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await resolveAnilistSeasonMedia({ title: 'Show', season: 2 }, { execute });
|
||||||
|
|
||||||
|
assert.equal(result?.id, 1);
|
||||||
|
assert.equal(result?.seasonResolved, false);
|
||||||
|
assert.equal(result?.via, 'anchor');
|
||||||
|
});
|
||||||
|
|||||||
@@ -325,9 +325,12 @@ export function pickByAirOrder(
|
|||||||
});
|
});
|
||||||
if (franchise.length < season) return null;
|
if (franchise.length < season) return null;
|
||||||
|
|
||||||
|
// Ordering by air date is only meaningful when every entry has one: an unknown year
|
||||||
|
// would sort last and shift the season index while still reporting a resolved match.
|
||||||
|
if (franchise.some((entry) => airYear(entry) === null)) return null;
|
||||||
|
|
||||||
const ordered = [...franchise].sort((a, b) => {
|
const ordered = [...franchise].sort((a, b) => {
|
||||||
const yearDelta =
|
const yearDelta = (airYear(a) ?? 0) - (airYear(b) ?? 0);
|
||||||
(airYear(a) ?? Number.MAX_SAFE_INTEGER) - (airYear(b) ?? Number.MAX_SAFE_INTEGER);
|
|
||||||
if (yearDelta !== 0) return yearDelta;
|
if (yearDelta !== 0) return yearDelta;
|
||||||
return a.id - b.id;
|
return a.id - b.id;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user