Files
SubMiner/launcher/aniskip-metadata.test.ts

176 lines
5.5 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
inferAniSkipMetadataForFile,
buildSubminerScriptOpts,
parseAniSkipGuessitJson,
resolveAniSkipMetadataForFile,
} from './aniskip-metadata';
function makeMockResponse(payload: unknown): Response {
return {
ok: true,
status: 200,
json: async () => payload,
} as Response;
}
function normalizeFetchInput(input: string | URL | Request): string {
if (typeof input === 'string') return input;
if (input instanceof URL) return input.toString();
return input.url;
}
async function withMockFetch(
handler: (input: string | URL | Request) => Promise<Response>,
fn: () => Promise<void>,
): Promise<void> {
const original = globalThis.fetch;
(globalThis as { fetch: typeof fetch }).fetch = (async (input: string | URL | Request) => {
return handler(input);
}) as typeof fetch;
try {
await fn();
} finally {
(globalThis as { fetch: typeof fetch }).fetch = original;
}
}
test('parseAniSkipGuessitJson extracts title season and episode', () => {
const parsed = parseAniSkipGuessitJson(
JSON.stringify({ title: 'My Show', season: 2, episode: 7 }),
'/tmp/My.Show.S02E07.mkv',
);
assert.deepEqual(parsed, {
title: 'My Show',
season: 2,
episode: 7,
source: 'guessit',
malId: null,
introStart: null,
introEnd: null,
lookupStatus: 'lookup_failed',
});
});
test('parseAniSkipGuessitJson prefers series over episode title', () => {
const parsed = parseAniSkipGuessitJson(
JSON.stringify({
title: 'What Is This, a Picnic',
series: 'Solo Leveling',
season: 1,
episode: 10,
}),
'/tmp/S01E10-What.Is.This,.a.Picnic-Bluray-1080p.mkv',
);
assert.deepEqual(parsed, {
title: 'Solo Leveling',
season: 1,
episode: 10,
source: 'guessit',
malId: null,
introStart: null,
introEnd: null,
lookupStatus: 'lookup_failed',
});
});
test('inferAniSkipMetadataForFile falls back to filename title when guessit unavailable', () => {
const parsed = inferAniSkipMetadataForFile('/tmp/Another_Show_-_03.mkv', {
commandExists: () => false,
runGuessit: () => null,
});
assert.equal(parsed.title.length > 0, true);
assert.equal(parsed.source, 'fallback');
});
test('inferAniSkipMetadataForFile falls back to anime directory title when filename is episode-only', () => {
const parsed = inferAniSkipMetadataForFile(
'/truenas/jellyfin/anime/Solo Leveling/Season-1/S01E10-What.Is.This,.a.Picnic-Bluray-1080p.mkv',
{
commandExists: () => false,
runGuessit: () => null,
},
);
assert.equal(parsed.title, 'Solo Leveling');
assert.equal(parsed.season, 1);
assert.equal(parsed.episode, 10);
assert.equal(parsed.source, 'fallback');
});
test('resolveAniSkipMetadataForFile resolves MAL id and intro payload', async () => {
await withMockFetch(
async (input) => {
const url = normalizeFetchInput(input);
if (url.includes('myanimelist.net/search/prefix.json')) {
return makeMockResponse({
categories: [
{
items: [
{ id: '9876', name: 'Wrong Match' },
{ id: '1234', name: 'My Show' },
],
},
],
});
}
if (url.includes('api.aniskip.com/v1/skip-times/1234/7')) {
return makeMockResponse({
found: true,
results: [{ skip_type: 'op', interval: { start_time: 12.5, end_time: 54.2 } }],
});
}
throw new Error(`unexpected url: ${url}`);
},
async () => {
const resolved = await resolveAniSkipMetadataForFile('/media/Anime.My.Show.S01E07.mkv');
assert.equal(resolved.malId, 1234);
assert.equal(resolved.introStart, 12.5);
assert.equal(resolved.introEnd, 54.2);
assert.equal(resolved.lookupStatus, 'ready');
assert.equal(resolved.title, 'Anime My Show');
},
);
});
test('resolveAniSkipMetadataForFile emits missing_mal_id when MAL search misses', async () => {
await withMockFetch(
async () => makeMockResponse({ categories: [] }),
async () => {
const resolved = await resolveAniSkipMetadataForFile('/media/NopeShow.S01E03.mkv');
assert.equal(resolved.malId, null);
assert.equal(resolved.lookupStatus, 'missing_mal_id');
},
);
});
test('buildSubminerScriptOpts includes aniskip payload fields', () => {
const opts = buildSubminerScriptOpts('/tmp/SubMiner.AppImage', '/tmp/subminer.sock', {
title: "Frieren: Beyond Journey's End",
season: 1,
episode: 5,
source: 'guessit',
malId: 1234,
introStart: 30.5,
introEnd: 62,
lookupStatus: 'ready',
});
const payloadMatch = opts.match(/subminer-aniskip_payload=([^,]+)/);
assert.match(opts, /subminer-binary_path=\/tmp\/SubMiner\.AppImage/);
assert.match(opts, /subminer-socket_path=\/tmp\/subminer\.sock/);
assert.match(opts, /subminer-aniskip_title=Frieren: Beyond Journey's End/);
assert.match(opts, /subminer-aniskip_season=1/);
assert.match(opts, /subminer-aniskip_episode=5/);
assert.match(opts, /subminer-aniskip_mal_id=1234/);
assert.match(opts, /subminer-aniskip_intro_start=30.5/);
assert.match(opts, /subminer-aniskip_intro_end=62/);
assert.match(opts, /subminer-aniskip_lookup_status=ready/);
assert.ok(payloadMatch !== null);
const payload = JSON.parse(decodeURIComponent(payloadMatch[1]));
assert.equal(payload.found, true);
const first = payload.results?.[0];
assert.equal(first.skip_type, 'op');
assert.equal(first.interval.start_time, 30.5);
assert.equal(first.interval.end_time, 62);
});