Files
SubMiner/src/main/runtime/jellyfin-remote-connection.ts
T
sudacode d373de7a92 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
2026-05-22 23:41:05 -07:00

140 lines
4.7 KiB
TypeScript

import { buildMpvLaunchModeArgs } from '../../shared/mpv-launch-mode';
import {
buildSubminerPluginRuntimeScriptOptParts,
type SubminerPluginRuntimeScriptOptConfig,
} from '../../shared/subminer-plugin-script-opts';
import type { MpvLaunchMode } from '../../types/config';
import type { InstalledMpvPluginDetection } from './first-run-setup-plugin';
type MpvClientLike = {
connected: boolean;
connect: () => void;
};
type SpawnedProcessLike = {
on: (event: 'error', listener: (error: unknown) => void) => void;
unref: () => void;
};
export type WaitForMpvConnectedDeps = {
getMpvClient: () => MpvClientLike | null;
now: () => number;
sleep: (delayMs: number) => Promise<void>;
};
export function createWaitForMpvConnectedHandler(deps: WaitForMpvConnectedDeps) {
return async (timeoutMs = 7000): Promise<boolean> => {
const client = deps.getMpvClient();
if (!client) return false;
if (client.connected) return true;
try {
client.connect();
} catch {}
const startedAt = deps.now();
while (deps.now() - startedAt < timeoutMs) {
if (deps.getMpvClient()?.connected) return true;
await deps.sleep(100);
}
return Boolean(deps.getMpvClient()?.connected);
};
}
export type LaunchMpvForJellyfinDeps = {
getSocketPath: () => string;
getLaunchMode: () => MpvLaunchMode;
platform: NodeJS.Platform;
execPath: string;
getRuntimePluginEntrypoint?: () => string | null | undefined;
getInstalledPluginDetection?: () => InstalledMpvPluginDetection;
getPluginRuntimeConfig?: () => SubminerPluginRuntimeScriptOptConfig;
defaultMpvLogPath: string;
defaultMpvArgs: readonly string[];
removeSocketPath: (socketPath: string) => void;
spawnMpv: (args: string[]) => SpawnedProcessLike;
logWarn: (message: string, error: unknown) => void;
logInfo: (message: string) => void;
};
export function createLaunchMpvIdleForJellyfinPlaybackHandler(deps: LaunchMpvForJellyfinDeps) {
return (): void => {
const socketPath = deps.getSocketPath();
if (deps.platform !== 'win32') {
try {
deps.removeSocketPath(socketPath);
} catch {
// ignore stale socket cleanup errors
}
}
const pluginRuntimeConfig = deps.getPluginRuntimeConfig?.();
const scriptOptParts = pluginRuntimeConfig
? buildSubminerPluginRuntimeScriptOptParts(
{
...pluginRuntimeConfig,
socketPath,
},
deps.execPath,
)
: [`subminer-binary_path=${deps.execPath}`, `subminer-socket_path=${socketPath}`];
const scriptOpts = `--script-opts=${scriptOptParts.join(',')}`;
const installedPlugin = deps.getInstalledPluginDetection?.();
const runtimePluginEntrypoint = installedPlugin?.installed
? ''
: (deps.getRuntimePluginEntrypoint?.()?.trim() ?? '');
if (installedPlugin?.installed && installedPlugin.path) {
deps.logInfo(`Using installed mpv plugin for Jellyfin playback: ${installedPlugin.path}`);
}
const mpvArgs = [
...deps.defaultMpvArgs,
...buildMpvLaunchModeArgs(deps.getLaunchMode()),
...(runtimePluginEntrypoint ? [`--script=${runtimePluginEntrypoint}`] : []),
'--idle=yes',
scriptOpts,
`--log-file=${deps.defaultMpvLogPath}`,
`--input-ipc-server=${socketPath}`,
];
const proc = deps.spawnMpv(mpvArgs);
proc.on('error', (error) => {
deps.logWarn('Failed to launch mpv for Jellyfin remote playback', error);
});
proc.unref();
deps.logInfo(`Launched mpv for Jellyfin playback on socket: ${socketPath}`);
};
}
export type EnsureMpvConnectedDeps = {
getMpvClient: () => MpvClientLike | null;
setMpvClient: (client: MpvClientLike | null) => void;
createMpvClient: () => MpvClientLike;
waitForMpvConnected: (timeoutMs: number) => Promise<boolean>;
launchMpvIdleForJellyfinPlayback: () => void;
getAutoLaunchInFlight: () => Promise<boolean> | null;
setAutoLaunchInFlight: (promise: Promise<boolean> | null) => void;
connectTimeoutMs: number;
autoLaunchTimeoutMs: number;
};
export function createEnsureMpvConnectedForJellyfinPlaybackHandler(deps: EnsureMpvConnectedDeps) {
return async (): Promise<boolean> => {
if (!deps.getMpvClient()) {
deps.setMpvClient(deps.createMpvClient());
}
const connected = await deps.waitForMpvConnected(deps.connectTimeoutMs);
if (connected) return true;
if (!deps.getAutoLaunchInFlight()) {
const inFlight = (async () => {
deps.launchMpvIdleForJellyfinPlayback();
return deps.waitForMpvConnected(deps.autoLaunchTimeoutMs);
})().finally(() => {
deps.setAutoLaunchInFlight(null);
});
deps.setAutoLaunchInFlight(inFlight);
}
return deps.getAutoLaunchInFlight() as Promise<boolean>;
};
}