mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-03 06:22:41 -08:00
176 lines
5.5 KiB
TypeScript
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);
|
|
});
|