import assert from 'node:assert/strict'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import test from 'node:test'; import { createCharacterDictionaryRuntimeService } from '../character-dictionary-runtime'; import { buildCharacterDictionarySeriesKey } from './manual-selection'; function makeTempDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-character-dictionary-')); } test('getManualSelectionSnapshot waits for explicit search text before fetching candidates', async () => { const userDataPath = makeTempDir(); const originalFetch = globalThis.fetch; const searchTerms: string[] = []; globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => { const body = JSON.parse(String(init?.body ?? '{}')) as { variables?: { search?: string }; }; searchTerms.push(String(body.variables?.search ?? '')); return new Response( JSON.stringify({ data: { Page: { media: [ { id: 154587, episodes: 28, title: { romaji: 'Sousou no Frieren', english: 'Frieren: Beyond Journey’s End', native: '葬送のフリーレン', }, }, ], }, }, }), { status: 200, headers: { 'content-type': 'application/json' }, }, ); }) as typeof globalThis.fetch; try { const runtime = createCharacterDictionaryRuntimeService({ userDataPath, getCurrentMediaPath: () => '/tmp/[SubsPlease] Kage no Jitsuryokusha - 05.mkv', getCurrentMediaTitle: () => '[SubsPlease] Kage no Jitsuryokusha - 05.mkv', resolveMediaPathForJimaku: (mediaPath) => mediaPath, guessAnilistMediaInfo: async () => ({ title: 'Kage no Jitsuryokusha ni Naritakute!', season: null, episode: 5, source: 'guessit', }), now: () => 1_700_000_000_000, }); const initial = await runtime.getManualSelectionSnapshot(undefined, ''); assert.equal(initial.guessTitle, 'Kage no Jitsuryokusha ni Naritakute!'); assert.deepEqual(initial.candidates, []); assert.deepEqual(searchTerms, []); const searched = await runtime.getManualSelectionSnapshot(undefined, 'Frieren'); assert.deepEqual(searchTerms, ['Frieren']); assert.deepEqual(searched.candidates, [ { id: 154587, title: 'Frieren: Beyond Journey’s End', episodes: 28 }, ]); } finally { globalThis.fetch = originalFetch; } }); test('getManualSelectionSnapshot hydrates override episode count from searched candidates', async () => { const userDataPath = makeTempDir(); const overrideSeriesKey = buildCharacterDictionarySeriesKey({ mediaPath: '/tmp/KonoSuba - 01.mkv', mediaTitle: 'KonoSuba - 01.mkv', guess: { title: "KonoSuba - God's blessing on this wonderful world!", year: 2016, season: null, episode: 1, source: 'guessit', }, }); const overrideDir = path.join(userDataPath, 'character-dictionaries'); fs.mkdirSync(overrideDir, { recursive: true }); fs.writeFileSync( path.join(overrideDir, 'anilist-overrides.json'), JSON.stringify({ overrides: [ { seriesKey: overrideSeriesKey, mediaId: 21202, mediaTitle: "KONOSUBA -God's blessing on this wonderful world!", staleMediaIds: [], }, ], }), 'utf8', ); const originalFetch = globalThis.fetch; globalThis.fetch = (async (_input: string | URL | Request) => { return new Response( JSON.stringify({ data: { Page: { media: [ { id: 21202, episodes: 10, title: { romaji: 'Kono Subarashii Sekai ni Shukufuku wo!', english: "KONOSUBA -God's blessing on this wonderful world!", native: 'この素晴らしい世界に祝福を!', }, }, ], }, }, }), { status: 200, headers: { 'content-type': 'application/json' }, }, ); }) as typeof globalThis.fetch; try { const runtime = createCharacterDictionaryRuntimeService({ userDataPath, getCurrentMediaPath: () => '/tmp/KonoSuba - 01.mkv', getCurrentMediaTitle: () => 'KonoSuba - 01.mkv', resolveMediaPathForJimaku: (mediaPath) => mediaPath, guessAnilistMediaInfo: async () => ({ title: "KonoSuba - God's blessing on this wonderful world!", year: 2016, season: null, episode: 1, source: 'guessit', }), now: () => 1_700_000_000_000, }); const snapshot = await runtime.getManualSelectionSnapshot(undefined, 'KonoSuba'); assert.deepEqual(snapshot.override, { id: 21202, title: "KONOSUBA -God's blessing on this wonderful world!", episodes: 10, }); } finally { globalThis.fetch = originalFetch; } }); test('resolvePinnedMediaId returns the manual override for the current season', async () => { const userDataPath = makeTempDir(); const seasonThreePath = '/anime/Oregairu/My Teen Romantic Comedy SNAFU (2013) - S03E01.mkv'; const guess = { title: 'My Teen Romantic Comedy SNAFU', year: 2013, season: 3, episode: 1, source: 'guessit' as const, }; const runtime = createCharacterDictionaryRuntimeService({ userDataPath, getCurrentMediaPath: () => seasonThreePath, getCurrentMediaTitle: () => null, resolveMediaPathForJimaku: (mediaPath) => mediaPath, guessAnilistMediaInfo: async () => guess, now: () => 1_700_000_000_000, }); assert.equal( await runtime.resolvePinnedMediaId({ mediaPath: seasonThreePath, mediaTitle: null, guess, }), null, ); const overridesPath = path.join(userDataPath, 'character-dictionaries', 'anilist-overrides.json'); fs.mkdirSync(path.dirname(overridesPath), { recursive: true }); fs.writeFileSync( overridesPath, JSON.stringify({ overrides: [ { seriesKey: buildCharacterDictionarySeriesKey({ mediaPath: seasonThreePath, mediaTitle: null, guess, }), mediaId: 108489, mediaTitle: 'My Teen Romantic Comedy SNAFU Climax!', staleMediaIds: [], }, ], }), 'utf8', ); assert.equal( await runtime.resolvePinnedMediaId({ mediaPath: seasonThreePath, mediaTitle: null, guess, }), 108489, ); // The season 1 file in the same folder must not inherit the season 3 override. assert.equal( await runtime.resolvePinnedMediaId({ mediaPath: '/anime/Oregairu/My Teen Romantic Comedy SNAFU (2013) - S01E01.mkv', mediaTitle: null, guess: { ...guess, season: 1 }, }), null, ); });