mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
fix(anilist): add missing list entry on confident match (#305)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Ivan Melck <ivan@spec-kitty.ai>
This commit is contained in:
4 files changed
+139
-7
No files matched your search
@@ -401,7 +401,83 @@ test('updateAnilistPostWatchProgress skips when progress already reached', async
|
||||
}
|
||||
});
|
||||
|
||||
test('updateAnilistPostWatchProgress returns non-retryable error when media is not planning or watching', async () => {
|
||||
test('updateAnilistPostWatchProgress adds an exactly matched show missing from the list', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let call = 0;
|
||||
let saveVariables: Record<string, unknown> | undefined;
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
call += 1;
|
||||
if (call === 1) {
|
||||
return createJsonResponse({
|
||||
data: {
|
||||
Page: {
|
||||
media: [{ id: 33, episodes: 12, title: { english: 'New Show' } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (call === 2) {
|
||||
return createJsonResponse({
|
||||
data: {
|
||||
Media: { id: 33, mediaListEntry: null },
|
||||
},
|
||||
});
|
||||
}
|
||||
saveVariables = (JSON.parse(String(init?.body)) as { variables?: Record<string, unknown> })
|
||||
.variables;
|
||||
return createJsonResponse({
|
||||
data: { SaveMediaListEntry: { progress: 1, status: 'CURRENT' } },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await updateAnilistPostWatchProgress('token', 'New Show', 1);
|
||||
assert.equal(result.status, 'updated');
|
||||
assert.match(result.message, /added "New Show" to Watching at episode 1/i);
|
||||
assert.deepEqual(saveVariables, { mediaId: 33, progress: 1, status: 'CURRENT' });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('updateAnilistPostWatchProgress adds a pinned media id missing from the list', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let call = 0;
|
||||
let saveVariables: Record<string, unknown> | undefined;
|
||||
globalThis.fetch = (async (_input, init) => {
|
||||
call += 1;
|
||||
if (call === 1) {
|
||||
return createJsonResponse({
|
||||
data: {
|
||||
Media: {
|
||||
id: 108489,
|
||||
episodes: 12,
|
||||
title: { english: 'Pinned Show' },
|
||||
mediaListEntry: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
saveVariables = (JSON.parse(String(init?.body)) as { variables?: Record<string, unknown> })
|
||||
.variables;
|
||||
return createJsonResponse({
|
||||
data: { SaveMediaListEntry: { progress: 12, status: 'COMPLETED' } },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await updateAnilistPostWatchProgress('token', 'Wrong Guess', 12, {
|
||||
mediaId: 108489,
|
||||
});
|
||||
assert.equal(result.status, 'updated');
|
||||
assert.match(result.message, /added "Pinned Show" at episode 12 and marked it completed/i);
|
||||
assert.deepEqual(saveVariables, { mediaId: 108489, progress: 12, status: 'COMPLETED' });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('updateAnilistPostWatchProgress does not add a loosely matched show missing from the list', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let call = 0;
|
||||
globalThis.fetch = (async () => {
|
||||
@@ -410,7 +486,7 @@ test('updateAnilistPostWatchProgress returns non-retryable error when media is n
|
||||
return createJsonResponse({
|
||||
data: {
|
||||
Page: {
|
||||
media: [{ id: 33, episodes: 12, title: { english: 'Missing Show' } }],
|
||||
media: [{ id: 33, episodes: 12, title: { english: 'Missing Show Abridged Edition' } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -427,6 +503,39 @@ test('updateAnilistPostWatchProgress returns non-retryable error when media is n
|
||||
assert.equal(result.status, 'error');
|
||||
assert.equal(result.retryable, false);
|
||||
assert.match(result.message, /not in your AniList Planning or Watching list/i);
|
||||
assert.equal(call, 2);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('updateAnilistPostWatchProgress does not add the season 1 entry when the title carries an unparsed season marker', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let call = 0;
|
||||
globalThis.fetch = (async () => {
|
||||
call += 1;
|
||||
if (call === 1) {
|
||||
return createJsonResponse({
|
||||
data: {
|
||||
Page: {
|
||||
media: [{ id: 33, episodes: 12, format: 'TV', title: { english: 'Demo Show' } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return createJsonResponse({
|
||||
data: {
|
||||
Media: { id: 33, mediaListEntry: null },
|
||||
},
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await updateAnilistPostWatchProgress('token', 'Demo Show S2', 3);
|
||||
assert.equal(result.status, 'error');
|
||||
assert.equal(result.retryable, false);
|
||||
assert.match(result.message, /not in your AniList Planning or Watching list/i);
|
||||
assert.equal(call, 2);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as path from 'path';
|
||||
import { parseMediaInfo } from '../../../jimaku/utils';
|
||||
import { resolveMediaLookupTarget, sanitizeMediaTitle } from '../../../shared/media-identity';
|
||||
import type { AnilistRateLimiter } from './rate-limiter';
|
||||
import { resolveAnilistSeasonMedia } from './season-resolver';
|
||||
import { resolveAnilistSeasonMedia, stripSeasonSuffix } from './season-resolver';
|
||||
|
||||
const ANILIST_GRAPHQL_URL = 'https://graphql.anilist.co';
|
||||
|
||||
@@ -273,6 +273,9 @@ export async function updateAnilistPostWatchProgress(
|
||||
let mediaId = pinnedMediaId;
|
||||
let resolvedTitle: string | null = null;
|
||||
let resolvedEpisodes: number | null = null;
|
||||
// A missing list entry is only created when we trust the match; otherwise a fuzzy
|
||||
// search hit would add an unrelated show to the user's list.
|
||||
let confidentMatch = pinnedMediaId !== null;
|
||||
|
||||
if (mediaId === null) {
|
||||
let resolution: Awaited<ReturnType<typeof resolveAnilistSeasonMedia>>;
|
||||
@@ -312,6 +315,11 @@ export async function updateAnilistPostWatchProgress(
|
||||
mediaId = resolution.id;
|
||||
resolvedTitle = resolution.title;
|
||||
resolvedEpisodes = resolution.episodes;
|
||||
// With no parsed season, an exact match on a season-stripped title ("Show S2" ->
|
||||
// "Show") only certifies the season 1 anchor, not the entry being watched.
|
||||
const seasonMarkerStripped = stripSeasonSuffix(title).trim() !== title.trim();
|
||||
confidentMatch =
|
||||
resolution.exactTitleMatch && (resolution.requestedSeason !== null || !seasonMarkerStripped);
|
||||
}
|
||||
|
||||
const entryResponse = await anilistGraphQl<AnilistMediaEntryData>(
|
||||
@@ -358,7 +366,8 @@ export async function updateAnilistPostWatchProgress(
|
||||
: null);
|
||||
|
||||
const entry = entryMedia?.mediaListEntry ?? null;
|
||||
if (!entry || !isUpdateableListStatus(entry.status)) {
|
||||
const createEntry = !entry && confidentMatch;
|
||||
if (!createEntry && (!entry || !isUpdateableListStatus(entry.status))) {
|
||||
return {
|
||||
status: 'error',
|
||||
retryable: false,
|
||||
@@ -366,7 +375,7 @@ export async function updateAnilistPostWatchProgress(
|
||||
};
|
||||
}
|
||||
|
||||
const currentProgress = entry.progress ?? 0;
|
||||
const currentProgress = entry?.progress ?? 0;
|
||||
const shouldMarkCompleted = isKnownFinalEpisode(pickedEpisodes, episode);
|
||||
if (typeof currentProgress === 'number' && currentProgress >= episode && !shouldMarkCompleted) {
|
||||
return {
|
||||
@@ -397,6 +406,15 @@ export async function updateAnilistPostWatchProgress(
|
||||
return { status: 'error', message: `AniList update failed: ${saveError}` };
|
||||
}
|
||||
|
||||
if (createEntry) {
|
||||
return {
|
||||
status: 'updated',
|
||||
message: shouldMarkCompleted
|
||||
? `AniList added "${pickedTitle}" at episode ${episode} and marked it completed.`
|
||||
: `AniList added "${pickedTitle}" to Watching at episode ${episode}.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'updated',
|
||||
message: shouldMarkCompleted
|
||||
|
||||
Reference in new issue
Block a user