fix(jellyfin): stop inferring subtitle delays

This commit is contained in:
2026-08-30 21:03:25 -07:00
parent 2ad491e95c
commit bb611588ed
12 changed files with 9 additions and 697 deletions
@@ -32,14 +32,6 @@ function makeDeps(overrides: {
cleanupCachedSubtitles?: Parameters<
typeof createPreloadJellyfinExternalSubtitlesHandler
>[0]['cleanupCachedSubtitles'];
getSavedSubtitleDelay?: Parameters<
typeof createPreloadJellyfinExternalSubtitlesHandler
>[0]['getSavedSubtitleDelay'];
setActiveSubtitleDelayKey?: Parameters<
typeof createPreloadJellyfinExternalSubtitlesHandler
>[0]['setActiveSubtitleDelayKey'];
loadSubtitleSourceText?: (source: string) => Promise<string>;
saveSubtitleDelay?: (itemId: string, streamIndex: number, delaySeconds: number) => void;
initSubtitlePrefetch?: Parameters<
typeof createPreloadJellyfinExternalSubtitlesHandler
>[0]['initSubtitlePrefetch'];
@@ -57,10 +49,6 @@ function makeDeps(overrides: {
cleanupDir: '/tmp/subminer-jellyfin-subtitles',
})),
cleanupCachedSubtitles: overrides.cleanupCachedSubtitles ?? (() => {}),
getSavedSubtitleDelay: overrides.getSavedSubtitleDelay,
setActiveSubtitleDelayKey: overrides.setActiveSubtitleDelayKey,
loadSubtitleSourceText: overrides.loadSubtitleSourceText,
saveSubtitleDelay: overrides.saveSubtitleDelay,
initSubtitlePrefetch: overrides.initSubtitlePrefetch,
logDebug: overrides.logDebug ?? (() => {}),
};
@@ -377,20 +365,17 @@ test('preload jellyfin subtitles waits for delayed external japanese track inste
test('preload jellyfin subtitles clears managed delay when no external tracks are available', async () => {
const commands: Array<Array<string | number>> = [];
const activeDelayKeys: Array<unknown> = [];
const preload = createPreloadJellyfinExternalSubtitlesHandler(
makeDeps({
listJellyfinSubtitleTracks: async () => [
{ index: 0, language: 'jpn', title: 'Embedded Japanese' },
],
sendMpvCommand: (command) => commands.push(command),
setActiveSubtitleDelayKey: (key) => activeDelayKeys.push(key),
}),
);
await preload({ session, clientInfo, itemId: 'item-1' });
assert.deepEqual(activeDelayKeys, [null]);
assert.deepEqual(commands, [['set_property', 'sub-delay', 0]]);
});
@@ -461,42 +446,7 @@ test('preload jellyfin subtitles prefers Jellyfin default and embedded japanese
]);
});
test('preload jellyfin subtitles applies saved delay for selected japanese stream', async () => {
const commands: Array<Array<string | number>> = [];
const activeKeys: Array<{ itemId: string; streamIndex: number } | null> = [];
const preload = createPreloadJellyfinExternalSubtitlesHandler(
makeDeps({
listJellyfinSubtitleTracks: async () => [
{ index: 3, language: 'jpn', title: 'Japanese', deliveryUrl: 'https://sub/jpn.srt' },
],
getMpvClient: () => ({
requestProperty: async () => [
{
type: 'sub',
id: 11,
lang: 'jpn',
title: 'Japanese',
external: true,
'external-filename': '/tmp/subminer-jellyfin-subtitles/3.srt',
},
],
}),
sendMpvCommand: (command) => commands.push(command),
getSavedSubtitleDelay: (_itemId, streamIndex) => (streamIndex === 3 ? 1.25 : null),
setActiveSubtitleDelayKey: (key) => activeKeys.push(key),
}),
);
await preload({ session, clientInfo, itemId: 'item-9' });
assert.deepEqual(setPropertyCommandsExceptTrackAutoSelection(commands), [
['set_property', 'sub-delay', 1.25],
['set_property', 'sid', 11],
]);
assert.deepEqual(activeKeys, [{ itemId: 'item-9', streamIndex: 3 }]);
});
test('preload jellyfin subtitles applies saved delay before selecting japanese stream', async () => {
test('preload jellyfin subtitles resets delay before selecting japanese stream', async () => {
const commands: Array<Array<string | number>> = [];
const preload = createPreloadJellyfinExternalSubtitlesHandler(
makeDeps({
@@ -516,14 +466,13 @@ test('preload jellyfin subtitles applies saved delay before selecting japanese s
],
}),
sendMpvCommand: (command) => commands.push(command),
getSavedSubtitleDelay: () => 1.25,
}),
);
await preload({ session, clientInfo, itemId: 'item-9' });
const delayIndex = commands.findIndex(
(command) => command[0] === 'set_property' && command[1] === 'sub-delay' && command[2] === 1.25,
(command) => command[0] === 'set_property' && command[1] === 'sub-delay' && command[2] === 0,
);
const selectedSidIndex = commands.findIndex(
(command) => command[0] === 'set_property' && command[1] === 'sid' && command[2] === 11,
@@ -533,143 +482,6 @@ test('preload jellyfin subtitles applies saved delay before selecting japanese s
assert.ok(delayIndex < selectedSidIndex);
});
test('preload jellyfin subtitles auto-aligns late japanese track from english reference', async () => {
const commands: Array<Array<string | number>> = [];
const savedDelays: Array<{ itemId: string; streamIndex: number; delaySeconds: number }> = [];
const primarySrt = `1
00:00:34,935 --> 00:00:36,937
Japanese 1
2
00:00:36,937 --> 00:00:41,441
Japanese 2
3
00:00:41,441 --> 00:00:45,279
Japanese 3
4
00:00:45,279 --> 00:00:48,115
Japanese 4
5
00:00:48,115 --> 00:00:52,286
Japanese 5
6
00:00:52,286 --> 00:00:54,955
Japanese 6
7
00:00:54,955 --> 00:00:59,793
Japanese 7
8
00:00:59,793 --> 00:01:03,630
Japanese 8
9
00:01:03,630 --> 00:01:07,634
Japanese 9
10
00:01:07,634 --> 00:01:13,040
Japanese 10
11
00:01:16,643 --> 00:01:20,814
Japanese 11
12
00:01:20,814 --> 00:01:23,116
Japanese 12
13
00:01:27,988 --> 00:01:30,991
Japanese 13
14
00:01:30,991 --> 00:01:34,094
Japanese 14
15
00:01:34,094 --> 00:01:37,097
Japanese 15
16
00:01:37,097 --> 00:01:39,100
Japanese 16
`;
const referenceAss = `[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:03.46,0:00:08.73,Default,,0,0,0,,English 1
Dialogue: 0,0:00:09.48,0:00:13.61,Default,,0,0,0,,English 2
Dialogue: 0,0:00:13.61,0:00:19.64,Default,,0,0,0,,English 3
Dialogue: 0,0:00:21.40,0:00:27.32,Default,,0,0,0,,English 4
Dialogue: 0,0:00:28.16,0:00:31.75,Default,,0,0,0,,English 5
Dialogue: 0,0:00:32.06,0:00:34.52,Default,,0,0,0,,English 6
Dialogue: 0,0:00:35.93,0:00:40.57,Default,,0,0,0,,English 7
Dialogue: 0,0:00:45.10,0:00:51.01,Default,,0,0,0,,English 8
Dialogue: 0,0:00:56.57,0:00:59.12,Default,,0,0,0,,English 9
Dialogue: 0,0:00:59.68,0:01:02.44,Default,,0,0,0,,English 10
Dialogue: 0,0:01:02.44,0:01:05.56,Default,,0,0,0,,English 11
Dialogue: 0,0:01:05.56,0:01:06.87,Default,,0,0,0,,English 12
`;
const preload = createPreloadJellyfinExternalSubtitlesHandler(
makeDeps({
listJellyfinSubtitleTracks: async () => [
{ index: 0, language: 'jpn', title: 'Japanese', deliveryUrl: 'https://sub/jpn.srt' },
{ index: 4, language: 'eng', title: 'English', deliveryUrl: 'https://sub/eng.ass' },
],
getMpvClient: () => ({
requestProperty: async () => [
{
type: 'sub',
id: 10,
lang: 'jpn',
title: 'Japanese',
external: true,
'external-filename': '/tmp/subminer-jellyfin-subtitles/0.srt',
},
{
type: 'sub',
id: 12,
lang: 'eng',
title: 'English',
external: true,
'external-filename': '/tmp/subminer-jellyfin-subtitles/4.ass',
},
],
}),
sendMpvCommand: (command) => commands.push(command),
cacheSubtitleTrack: async (track) => ({
path: `/tmp/subminer-jellyfin-subtitles/${track.index}.${track.index === 4 ? 'ass' : 'srt'}`,
cleanupDir: '/tmp/subminer-jellyfin-subtitles',
}),
getSavedSubtitleDelay: () => null,
loadSubtitleSourceText: async (source) =>
source.endsWith('.ass') ? referenceAss : primarySrt,
saveSubtitleDelay: (itemId, streamIndex, delaySeconds) => {
savedDelays.push({ itemId, streamIndex, delaySeconds });
},
}),
);
await preload({ session, clientInfo, itemId: 'item-9' });
const delayCommand = commands.find(
(command) => command[0] === 'set_property' && command[1] === 'sub-delay',
);
assert.ok(delayCommand);
const delaySeconds = delayCommand[2];
if (typeof delaySeconds !== 'number') {
assert.fail('Expected numeric subtitle delay.');
}
assert.ok(delaySeconds > -32);
assert.ok(delaySeconds < -31);
assert.deepEqual(savedDelays, [{ itemId: 'item-9', streamIndex: 0, delaySeconds }]);
});
test('preload jellyfin subtitles accepts numeric string mpv track ids', async () => {
const commands: Array<Array<string | number>> = [];
const preload = createPreloadJellyfinExternalSubtitlesHandler(