mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-28 16:49:50 -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);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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, {
|
||||
anilistId: selected.id,
|
||||
anilistId: seasonResolved ? selected.id : null,
|
||||
coverUrl,
|
||||
coverBlob,
|
||||
titleRomaji: selected.title?.romaji ?? null,
|
||||
titleEnglish: selected.title?.english ?? null,
|
||||
episodesTotal: selected.episodes ?? null,
|
||||
titleRomaji: seasonResolved ? (selected.title?.romaji ?? null) : null,
|
||||
titleEnglish: seasonResolved ? (selected.title?.english ?? null) : null,
|
||||
episodesTotal: seasonResolved ? (selected.episodes ?? null) : null,
|
||||
});
|
||||
|
||||
updateAnimeAnilistInfo(db, videoId, {
|
||||
anilistId: selected.id,
|
||||
titleRomaji: selected.title?.romaji ?? null,
|
||||
titleEnglish: selected.title?.english ?? null,
|
||||
titleNative: selected.title?.native ?? null,
|
||||
episodesTotal: selected.episodes ?? null,
|
||||
});
|
||||
if (seasonResolved) {
|
||||
updateAnimeAnilistInfo(db, videoId, {
|
||||
anilistId: selected.id,
|
||||
titleRomaji: selected.title?.romaji ?? null,
|
||||
titleEnglish: selected.title?.english ?? null,
|
||||
titleNative: selected.title?.native ?? null,
|
||||
episodesTotal: selected.episodes ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
selected.id,
|
||||
seasonResolved ? String(selected.id) : 'none (season unresolved)',
|
||||
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.
|
||||
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;
|
||||
|
||||
// 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 yearDelta =
|
||||
(airYear(a) ?? Number.MAX_SAFE_INTEGER) - (airYear(b) ?? Number.MAX_SAFE_INTEGER);
|
||||
const yearDelta = (airYear(a) ?? 0) - (airYear(b) ?? 0);
|
||||
if (yearDelta !== 0) return yearDelta;
|
||||
return a.id - b.id;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user