fix(config): remove trailing commas from config.example.jsonc

- Strip trailing commas throughout both config.example.jsonc copies
- Reformat inline arrays to multi-line for JSON strictness
- Update Jellyfin subtitle preload and playback launch tests and impl
This commit is contained in:
2026-05-22 02:07:10 -07:00
parent fd6a11118c
commit 3de7ed8b54
5 changed files with 77 additions and 12 deletions
@@ -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<Array<string | number>> = [];
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']);
});
+2 -2
View File
@@ -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) {
@@ -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<void>((resolve) => {
@@ -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<void> => {
cleanupActiveCache();
try {
cleanupActiveCache();
const tracks = await deps.listJellyfinSubtitleTracks(
params.session,
params.clientInfo,
@@ -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']);
});