Files
SubMiner/src/main/runtime/local-subtitle-selection.test.ts
T
sudacode 3a2d7a282d fix(jellyfin): show overlay, inject plugin, and fix stats title on playb
- Show visible overlay automatically during Jellyfin playback so subtitleStyle applies
- Inject bundled mpv plugin on auto-launch so keybindings work without overlay focus
- Group Jellyfin playback stats under item metadata (jellyfin://host/item/id) instead of stream URLs so episodes merge with matching local titles
- Mark ffsubsync unavailable in subsync modal for remote media paths
- Drain queued second-instance commands even when onReady throws
2026-05-22 00:29:14 -07:00

232 lines
7.7 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
createManagedLocalSubtitleSelectionRuntime,
resolveManagedLocalSubtitleSelection,
} from './local-subtitle-selection';
const mixedLanguageTrackList = [
{ type: 'sub', id: 1, lang: 'pt', title: '[Infinite]', external: false, selected: true },
{ type: 'sub', id: 2, lang: 'pt', title: '[Moshi Moshi]', external: false },
{ type: 'sub', id: 3, lang: 'en', title: '(Vivid)', external: false },
{ type: 'sub', id: 9, lang: 'en', title: 'English(US)', external: false },
{ type: 'sub', id: 11, lang: 'en', title: 'en.srt', external: true },
{ type: 'sub', id: 12, lang: 'ja', title: 'ja.srt', external: true },
];
const unlabeledExternalSidecarTrackList = [
{ type: 'sub', id: 1, lang: 'eng', title: 'English ASS', external: false, selected: true },
{ type: 'sub', id: 2, title: 'srt', external: true },
];
test('resolveManagedLocalSubtitleSelection prefers default Japanese primary and English secondary tracks', () => {
const result = resolveManagedLocalSubtitleSelection({
trackList: mixedLanguageTrackList,
primaryLanguages: [],
secondaryLanguages: [],
});
assert.equal(result.primaryTrackId, 12);
assert.equal(result.secondaryTrackId, 11);
});
test('resolveManagedLocalSubtitleSelection respects configured language overrides', () => {
const result = resolveManagedLocalSubtitleSelection({
trackList: mixedLanguageTrackList,
primaryLanguages: ['pt'],
secondaryLanguages: ['ja'],
});
assert.equal(result.primaryTrackId, 1);
assert.equal(result.secondaryTrackId, 12);
});
test('resolveManagedLocalSubtitleSelection promotes a single unlabeled external sidecar to primary', () => {
const result = resolveManagedLocalSubtitleSelection({
trackList: unlabeledExternalSidecarTrackList,
primaryLanguages: [],
secondaryLanguages: [],
});
assert.equal(result.primaryTrackId, 2);
assert.equal(result.secondaryTrackId, 1);
});
test('resolveManagedLocalSubtitleSelection does not guess between multiple unlabeled external sidecars', () => {
const result = resolveManagedLocalSubtitleSelection({
trackList: [
...unlabeledExternalSidecarTrackList,
{ type: 'sub', id: 3, title: 'subrip', external: true },
],
primaryLanguages: [],
secondaryLanguages: [],
});
assert.equal(result.primaryTrackId, null);
assert.equal(result.secondaryTrackId, 1);
});
test('managed local subtitle selection runtime applies preferred tracks once for a local media path', async () => {
const commands: Array<Array<string | number>> = [];
const scheduled: Array<() => void> = [];
const runtime = createManagedLocalSubtitleSelectionRuntime({
getCurrentMediaPath: () => '/videos/example.mkv',
getMpvClient: () =>
({
connected: true,
requestProperty: async (name: string) => {
if (name === 'track-list') {
return mixedLanguageTrackList;
}
throw new Error(`Unexpected property: ${name}`);
},
}) as never,
getPrimarySubtitleLanguages: () => [],
getSecondarySubtitleLanguages: () => [],
sendMpvCommand: (command) => {
commands.push(command);
},
schedule: (callback) => {
scheduled.push(callback);
return 1 as never;
},
clearScheduled: () => {},
});
runtime.handleMediaPathChange('/videos/example.mkv');
scheduled.shift()?.();
await new Promise((resolve) => setTimeout(resolve, 0));
runtime.handleSubtitleTrackListChange(mixedLanguageTrackList);
assert.deepEqual(commands, [
['set_property', 'sid', 12],
['set_property', 'secondary-sid', 11],
]);
});
test('managed local subtitle selection runtime promotes a single unlabeled external sidecar over embedded english', async () => {
const commands: Array<Array<string | number>> = [];
const scheduled: Array<() => void> = [];
const runtime = createManagedLocalSubtitleSelectionRuntime({
getCurrentMediaPath: () => '/videos/example.mkv',
getMpvClient: () =>
({
connected: true,
requestProperty: async (name: string) => {
if (name === 'track-list') {
return unlabeledExternalSidecarTrackList;
}
throw new Error(`Unexpected property: ${name}`);
},
}) as never,
getPrimarySubtitleLanguages: () => [],
getSecondarySubtitleLanguages: () => [],
sendMpvCommand: (command) => {
commands.push(command);
},
schedule: (callback) => {
scheduled.push(callback);
return 1 as never;
},
clearScheduled: () => {},
});
runtime.handleMediaPathChange('/videos/example.mkv');
scheduled.shift()?.();
await new Promise((resolve) => setTimeout(resolve, 0));
runtime.handleSubtitleTrackListChange(unlabeledExternalSidecarTrackList);
assert.deepEqual(commands, [
['set_property', 'sid', 2],
['set_property', 'secondary-sid', 1],
]);
});
test('managed local subtitle selection keeps waiting for primary after early secondary-only track list', () => {
const commands: Array<Array<string | number>> = [];
const runtime = createManagedLocalSubtitleSelectionRuntime({
getCurrentMediaPath: () => '/videos/example.mkv',
getMpvClient: () => null,
getPrimarySubtitleLanguages: () => [],
getSecondarySubtitleLanguages: () => [],
sendMpvCommand: (command) => {
commands.push(command);
},
schedule: () => 1 as never,
clearScheduled: () => {},
});
runtime.handleMediaPathChange('/videos/example.mkv');
runtime.handleSubtitleTrackListChange([
{ type: 'sub', id: 1, lang: 'eng', title: 'ASS', external: false },
{ type: 'sub', id: 2, lang: 'en', title: 'en.srt', external: true },
]);
runtime.handleSubtitleTrackListChange([
{ type: 'sub', id: 1, lang: 'eng', title: 'ASS', external: false },
{ type: 'sub', id: 2, lang: 'en', title: 'en.srt', external: true },
{ type: 'sub', id: 3, lang: 'ja', title: 'ja.srt', external: true },
]);
assert.deepEqual(commands, [
['set_property', 'secondary-sid', 2],
['set_property', 'sid', 3],
]);
});
test('managed local subtitle selection keeps pending refresh after early primary-only track list', async () => {
const commands: Array<Array<string | number>> = [];
const scheduled = new Map<number, () => void>();
let nextTimerId = 1;
const runtime = createManagedLocalSubtitleSelectionRuntime({
getCurrentMediaPath: () => '/videos/example.mkv',
getMpvClient: () =>
({
connected: true,
requestProperty: async (name: string) => {
if (name === 'track-list') {
return [
{ type: 'sub', id: 3, lang: 'ja', title: 'ja.srt', external: true },
{ type: 'sub', id: 4, lang: 'en', title: 'en.srt', external: true },
];
}
throw new Error(`Unexpected property: ${name}`);
},
}) as never,
getPrimarySubtitleLanguages: () => [],
getSecondarySubtitleLanguages: () => [],
sendMpvCommand: (command) => {
commands.push(command);
},
schedule: (callback) => {
const timerId = nextTimerId++;
scheduled.set(timerId, callback);
return timerId as never;
},
clearScheduled: (timer) => {
scheduled.delete(timer as never);
},
});
runtime.handleMediaPathChange('/videos/example.mkv');
runtime.handleSubtitleTrackListChange([
{ type: 'sub', id: 3, lang: 'ja', title: 'ja.srt', external: true },
]);
assert.deepEqual(commands, [['set_property', 'sid', 3]]);
assert.equal(scheduled.size, 1);
const refresh = [...scheduled.values()][0];
assert.ok(refresh);
refresh();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.deepEqual(commands, [
['set_property', 'sid', 3],
['set_property', 'secondary-sid', 4],
]);
});