diff --git a/src/main/runtime/jellyfin-playback-launch.test.ts b/src/main/runtime/jellyfin-playback-launch.test.ts index a8c905e4..3529ed72 100644 --- a/src/main/runtime/jellyfin-playback-launch.test.ts +++ b/src/main/runtime/jellyfin-playback-launch.test.ts @@ -267,3 +267,46 @@ test('playback handler does not let stats metadata failures block playback start assert.deepEqual(commands[1], ['loadfile', 'https://stream.example/video.m3u8', 'replace']); }); + +test('playback handler does not let media title failures block playback startup', async () => { + const commands: Array> = []; + const handler = createPlayJellyfinItemInMpvHandler({ + ensureMpvConnectedForPlayback: async () => true, + getMpvClient: () => ({ connected: true, send: () => {} }), + resolvePlaybackPlan: async () => ({ + url: 'https://stream.example/video.m3u8', + mode: 'direct', + title: 'Episode 4', + itemTitle: 'Episode 4', + seriesTitle: null, + seasonNumber: null, + episodeNumber: null, + startTimeTicks: 0, + audioStreamIndex: null, + subtitleStreamIndex: null, + }), + applyJellyfinMpvDefaults: () => {}, + showVisibleOverlay: () => {}, + sendMpvCommand: (command) => commands.push(command), + armQuitOnDisconnect: () => {}, + schedule: () => {}, + convertTicksToSeconds: (ticks) => ticks / 10_000_000, + preloadExternalSubtitles: () => {}, + setActivePlayback: () => {}, + setLastProgressAtMs: () => {}, + reportPlaying: () => {}, + showMpvOsd: () => {}, + updateCurrentMediaTitle: () => { + throw new Error('title state unavailable'); + }, + }); + + await handler({ + session: baseSession, + clientInfo: baseClientInfo, + jellyfinConfig: {}, + itemId: 'item-4', + }); + + assert.deepEqual(commands[1], ['loadfile', 'https://stream.example/video.m3u8', 'replace']); +}); diff --git a/src/main/runtime/jellyfin-playback-launch.ts b/src/main/runtime/jellyfin-playback-launch.ts index 636a4319..0ab8f50b 100644 --- a/src/main/runtime/jellyfin-playback-launch.ts +++ b/src/main/runtime/jellyfin-playback-launch.ts @@ -107,8 +107,8 @@ export function createPlayJellyfinItemInMpvHandler(deps: { deps.applyJellyfinMpvDefaults(mpvClient); deps.sendMpvCommand(['set_property', 'sub-auto', 'no']); const playbackUrl = applyStartTimeTicksToPlaybackUrl(plan.url, params.startTimeTicksOverride); - deps.updateCurrentMediaTitle?.(plan.title); try { + deps.updateCurrentMediaTitle?.(plan.title); deps.recordJellyfinPlaybackMetadata?.({ mediaPath: playbackUrl, displayTitle: plan.title, @@ -119,7 +119,7 @@ export function createPlayJellyfinItemInMpvHandler(deps: { itemId: params.itemId, }); } catch { - // Best-effort stats metadata must not block playback startup. + // Best-effort metadata/title hooks must not block playback startup. } deps.sendMpvCommand(['loadfile', playbackUrl, 'replace']); if (params.setQuitOnDisconnectArm !== false) { diff --git a/src/main/runtime/jellyfin-subtitle-preload.test.ts b/src/main/runtime/jellyfin-subtitle-preload.test.ts index d6c6a411..79b16a6b 100644 --- a/src/main/runtime/jellyfin-subtitle-preload.test.ts +++ b/src/main/runtime/jellyfin-subtitle-preload.test.ts @@ -331,6 +331,35 @@ test('preload jellyfin subtitles cleans previous cached subtitles before a new p assert.deepEqual(cleanupCalls, [['/tmp/subminer-jellyfin-subtitles-0']]); }); +test('preload jellyfin subtitles logs cleanup failures without rejecting', async () => { + const logs: string[] = []; + let cleanupShouldFail = false; + const preload = createPreloadJellyfinExternalSubtitlesHandler( + makeDeps({ + listJellyfinSubtitleTracks: async () => [ + { index: 0, language: 'eng', title: 'English', deliveryUrl: 'https://sub/a.srt' }, + ], + getMpvClient: () => ({ requestProperty: async () => [] }), + cacheSubtitleTrack: async (track) => ({ + path: `/tmp/subminer-jellyfin-subtitles-${track.index}/track.srt`, + cleanupDir: `/tmp/subminer-jellyfin-subtitles-${track.index}`, + }), + cleanupCachedSubtitles: () => { + if (cleanupShouldFail) { + throw new Error('cleanup failed'); + } + }, + logDebug: (message) => logs.push(message), + }), + ); + + await preload({ session, clientInfo, itemId: 'item-1' }); + cleanupShouldFail = true; + await assert.doesNotReject(() => preload({ session, clientInfo, itemId: 'item-2' })); + + assert.deepEqual(logs, ['Failed to preload Jellyfin external subtitles']); +}); + test('preload jellyfin subtitles serializes overlapping preload runs', async () => { let releaseFirstList!: () => void; const firstListBlocked = new Promise((resolve) => { diff --git a/src/main/runtime/jellyfin-subtitle-preload.ts b/src/main/runtime/jellyfin-subtitle-preload.ts index bebce847..c26d7eb4 100644 --- a/src/main/runtime/jellyfin-subtitle-preload.ts +++ b/src/main/runtime/jellyfin-subtitle-preload.ts @@ -174,9 +174,7 @@ function hasExpectedExternalSubtitleTracks( return true; } const loadedExternalFilenames = new Set( - tracks - .filter((track) => track.externalFilename) - .map((track) => track.externalFilename), + tracks.filter((track) => track.externalFilename).map((track) => track.externalFilename), ); return expectedExternalFilenames.every((filePath) => loadedExternalFilenames.has(filePath)); } @@ -247,9 +245,8 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: { clientInfo: JellyfinClientInfo; itemId: string; }): Promise => { - cleanupActiveCache(); - try { + cleanupActiveCache(); const tracks = await deps.listJellyfinSubtitleTracks( params.session, params.clientInfo, diff --git a/src/main/runtime/update/update-service.test.ts b/src/main/runtime/update/update-service.test.ts index c8addb34..29709ee0 100644 --- a/src/main/runtime/update/update-service.test.ts +++ b/src/main/runtime/update/update-service.test.ts @@ -390,9 +390,5 @@ test('manual update check keeps current prerelease builds on configured stable c const result = await service.checkForUpdates({ source: 'manual' }); assert.equal(result.status, 'up-to-date'); - assert.deepEqual(calls, [ - 'app:stable', - 'fetch:stable', - 'no-update:0.15.0-beta.3', - ]); + assert.deepEqual(calls, ['app:stable', 'fetch:stable', 'no-update:0.15.0-beta.3']); });