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
This commit is contained in:
2026-05-21 22:26:59 -07:00
parent c6328eef09
commit 3a2d7a282d
49 changed files with 976 additions and 106 deletions
+51
View File
@@ -2747,6 +2747,7 @@ const {
reportJellyfinRemoteStopped,
startJellyfinRemoteSession,
stopJellyfinRemoteSession,
cleanupJellyfinSubtitleCache,
runJellyfinCommand,
openJellyfinSetupWindow,
getJellyfinClientInfo,
@@ -2770,6 +2771,15 @@ const {
getLaunchMode: () => getResolvedConfig().mpv.launchMode,
platform: process.platform,
execPath: process.execPath,
getRuntimePluginEntrypoint: () => resolveBundledMpvRuntimePluginEntrypoint(),
getInstalledPluginDetection: () =>
detectInstalledMpvPlugin({
platform: process.platform,
homeDir: os.homedir(),
xdgConfigHome: process.env.XDG_CONFIG_HOME,
appDataDir: app.getPath('appData'),
mpvExecutablePath: getResolvedConfig().mpv.executablePath,
}),
getPluginRuntimeConfig: () => getMpvPluginRuntimeConfig(),
defaultMpvLogPath: DEFAULT_MPV_LOG_PATH,
defaultMpvArgs: MPV_JELLYFIN_DEFAULT_ARGS,
@@ -2805,6 +2815,41 @@ const {
sendMpvCommandRuntime(appState.mpvClient, command);
},
wait: (ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
cacheSubtitleTrack: async (track) => {
if (!track.deliveryUrl) {
throw new Error('Jellyfin subtitle track has no delivery URL');
}
const cacheDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), 'subminer-jellyfin-subtitles-'),
);
const urlPath = (() => {
try {
return new URL(track.deliveryUrl).pathname;
} catch {
return track.deliveryUrl;
}
})();
const ext = path.extname(urlPath).slice(0, 16) || '.srt';
const subtitlePath = path.join(cacheDir, `track-${track.index}${ext}`);
try {
const response = await fetch(track.deliveryUrl);
if (!response.ok) {
throw new Error(`Failed to download Jellyfin subtitle (HTTP ${response.status})`);
}
const bytes = new Uint8Array(await response.arrayBuffer());
await fs.promises.writeFile(subtitlePath, bytes);
} catch (error) {
fs.rmSync(cacheDir, { recursive: true, force: true });
throw error;
}
return { path: subtitlePath, cleanupDir: cacheDir };
},
cleanupCachedSubtitles: (dirs) => {
for (const dir of dirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
},
logDebug: (message, error) => {
logger.debug(message, error);
},
@@ -2823,6 +2868,7 @@ const {
},
),
applyJellyfinMpvDefaults: (mpvClient) => applyJellyfinMpvDefaults(mpvClient),
showVisibleOverlay: () => setVisibleOverlayVisible(true),
sendMpvCommand: (command) => sendMpvCommandRuntime(appState.mpvClient, command),
armQuitOnDisconnect: () => {
jellyfinPlayQuitOnDisconnectArmed = false;
@@ -2846,6 +2892,10 @@ const {
showMpvOsd: (text) => {
showMpvOsd(text);
},
recordJellyfinPlaybackMetadata: (metadata) => {
ensureImmersionTrackerStarted();
appState.immersionTracker?.recordJellyfinPlaybackMetadata(metadata);
},
},
remoteComposerOptions: {
getConfiguredSession: () => getConfiguredJellyfinSession(getResolvedJellyfinConfig()),
@@ -3615,6 +3665,7 @@ const {
appState.yomitanSettingsWindow = null;
},
stopJellyfinRemoteSession: () => stopJellyfinRemoteSession(),
cleanupJellyfinSubtitleCache: () => cleanupJellyfinSubtitleCache(),
stopDiscordPresenceService: () => {
void appState.discordPresenceService?.stop();
appState.discordPresenceService = null;