fix(jellyfin): keep selecting subtitles when a track download fails (#272)

This commit is contained in:
2026-09-24 21:32:56 -07:00
committed by GitHub
parent 63eb228e4f
commit b24008237b
5 changed files with 91 additions and 18 deletions
@@ -471,6 +471,56 @@ test('preload jellyfin subtitles does not lock in a fallback japanese track duri
]);
});
test('preload jellyfin subtitles still selects remaining tracks when one download fails', async () => {
const commands: Array<Array<string | number>> = [];
const preload = createPreloadJellyfinExternalSubtitlesHandler(
makeDeps({
listJellyfinSubtitleTracks: async () => [
{ index: 0, language: 'eng', title: 'English', deliveryUrl: 'https://sub/eng.srt' },
{ index: 1, language: 'jpn', title: 'Japanese', deliveryUrl: 'https://sub/jpn.srt' },
{ index: 5, language: 'eng', title: 'English PGS', deliveryUrl: 'https://sub/pgs.srt' },
],
getMpvClient: () => ({
requestProperty: async () => [
{
type: 'sub',
id: 1,
lang: 'eng',
title: 'English',
external: true,
'external-filename': '/tmp/subminer-jellyfin-subtitles/0.srt',
},
{
type: 'sub',
id: 2,
lang: 'jpn',
title: 'Japanese',
external: true,
'external-filename': '/tmp/subminer-jellyfin-subtitles/1.srt',
},
],
}),
cacheSubtitleTrack: async (track) => {
if (track.index === 5) {
throw new Error('Failed to download Jellyfin subtitle (HTTP 400)');
}
return {
path: `/tmp/subminer-jellyfin-subtitles/${track.index}.srt`,
cleanupDir: '/tmp/subminer-jellyfin-subtitles',
};
},
sendMpvCommand: (command) => commands.push(command),
}),
);
await preload({ session, clientInfo, itemId: 'item-1' });
assert.deepEqual(setPropertyCommandsExceptTrackAutoSelection(commands), [
['set_property', 'sid', 2],
['set_property', 'secondary-sid', 1],
]);
});
test('preload jellyfin subtitles clears managed delay when no external tracks are available', async () => {
const commands: Array<Array<string | number>> = [];
const preload = createPreloadJellyfinExternalSubtitlesHandler(
+16 -16
View File
@@ -367,15 +367,22 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: {
// Download every track at once and add each to mpv as soon as it lands. Jellyfin has
// to extract embedded tracks from the container, so one slow track must not hold up
// the Japanese primary that annotations depend on.
// the Japanese primary that annotations depend on. A failed track is skipped rather
// than failing the whole preload, so the remaining tracks still get selected.
const cachedTracks: CachedExternalSubtitleTrack[] = [];
const downloads = new Map(
uniqueTracks.map((track) => [
track,
(async (): Promise<CachedExternalSubtitleTrack> => {
(async (): Promise<CachedExternalSubtitleTrack | null> => {
const labelBase = (track.title || track.language || '').trim();
const label = labelBase || `Jellyfin Subtitle ${track.index}`;
const cached = { ...(await deps.cacheSubtitleTrack(track)), source: track };
let cached: CachedExternalSubtitleTrack;
try {
cached = { ...(await deps.cacheSubtitleTrack(track)), source: track };
} catch (error) {
deps.logDebug(`Failed to download Jellyfin subtitle track ${track.index}`, error);
return null;
}
activeCacheDirs.add(cached.cleanupDir);
cachedTracks.push(cached);
deps.sendMpvCommand(['sub-add', cached.path, 'auto', label, track.language || '']);
@@ -383,20 +390,17 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: {
})(),
]),
);
// Settled up front so a failed download is not flagged as unhandled while the
// Japanese track is still being selected.
const allDownloads = Promise.allSettled(downloads.values());
const allDownloads = Promise.all(downloads.values());
try {
let subtitleTracks: MpvSubtitleTrack[] = [];
let japanesePrimaryId: number | null | undefined;
const preferredJapaneseSource = pickPreferredJapaneseSource(uniqueTracks);
const preferredJapaneseDownload = preferredJapaneseSource
? downloads.get(preferredJapaneseSource)
: undefined;
if (preferredJapaneseDownload) {
const preferredJapanese = await preferredJapaneseDownload;
const preferredJapanese = preferredJapaneseSource
? await downloads.get(preferredJapaneseSource)
: null;
if (preferredJapanese) {
await deps.wait(TRACK_SELECTION_INITIAL_WAIT_MS);
subtitleTracks =
(await waitForPreferredSubtitleTracks(deps, true, [preferredJapanese.path])) ?? [];
@@ -418,11 +422,7 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: {
}
}
const results = await allDownloads;
const failedDownload = results.find((result) => result.status === 'rejected');
if (failedDownload) {
throw failedDownload.reason;
}
await allDownloads;
const cachedPaths = cachedTracks.map((track) => track.path);
if (!hasExpectedExternalSubtitleTracks(subtitleTracks, cachedPaths)) {
await deps.wait(TRACK_SELECTION_INITIAL_WAIT_MS);