Files
SubMiner/src/main/runtime/jellyfin-playback-launch.ts
sudacode a6d85def34 refactor(main): eliminate unsafe runtime cast escapes
Tighten main/runtime dependency contracts to remove non-test `as never` and `as unknown as` usage so type drift surfaces during compile/test checks instead of at runtime.
2026-02-22 13:59:08 -08:00

129 lines
4.2 KiB
TypeScript

import type { JellyfinAuthSession, JellyfinPlaybackPlan } from '../../core/services/jellyfin';
import type { JellyfinConfig } from '../../types';
import type { MpvRuntimeClientLike } from '../../core/services/mpv';
type JellyfinClientInfo = {
clientName: string;
clientVersion: string;
deviceId: string;
};
type ActivePlaybackState = {
itemId: string;
mediaSourceId: undefined;
audioStreamIndex?: number | null;
subtitleStreamIndex?: number | null;
playMethod: 'DirectPlay' | 'Transcode';
};
export function createPlayJellyfinItemInMpvHandler(deps: {
ensureMpvConnectedForPlayback: () => Promise<boolean>;
getMpvClient: () => MpvRuntimeClientLike | null;
resolvePlaybackPlan: (params: {
session: JellyfinAuthSession;
clientInfo: JellyfinClientInfo;
jellyfinConfig: JellyfinConfig;
itemId: string;
audioStreamIndex?: number | null;
subtitleStreamIndex?: number | null;
}) => Promise<JellyfinPlaybackPlan>;
applyJellyfinMpvDefaults: (mpvClient: MpvRuntimeClientLike) => void;
sendMpvCommand: (command: Array<string | number>) => void;
armQuitOnDisconnect: () => void;
schedule: (callback: () => void, delayMs: number) => void;
convertTicksToSeconds: (ticks: number) => number;
preloadExternalSubtitles: (params: {
session: JellyfinAuthSession;
clientInfo: JellyfinClientInfo;
itemId: string;
}) => void;
setActivePlayback: (state: ActivePlaybackState) => void;
setLastProgressAtMs: (value: number) => void;
reportPlaying: (payload: {
itemId: string;
mediaSourceId: undefined;
playMethod: 'DirectPlay' | 'Transcode';
audioStreamIndex?: number | null;
subtitleStreamIndex?: number | null;
eventName: 'start';
}) => void;
showMpvOsd: (text: string) => void;
}) {
return async (params: {
session: JellyfinAuthSession;
clientInfo: JellyfinClientInfo;
jellyfinConfig: JellyfinConfig;
itemId: string;
audioStreamIndex?: number | null;
subtitleStreamIndex?: number | null;
startTimeTicksOverride?: number;
setQuitOnDisconnectArm?: boolean;
}): Promise<void> => {
const connected = await deps.ensureMpvConnectedForPlayback();
const mpvClient = deps.getMpvClient();
if (!connected || !mpvClient) {
throw new Error(
'MPV not connected and auto-launch failed. Ensure mpv is installed and available in PATH.',
);
}
const plan = await deps.resolvePlaybackPlan({
session: params.session,
clientInfo: params.clientInfo,
jellyfinConfig: params.jellyfinConfig,
itemId: params.itemId,
audioStreamIndex: params.audioStreamIndex,
subtitleStreamIndex: params.subtitleStreamIndex,
});
deps.applyJellyfinMpvDefaults(mpvClient);
deps.sendMpvCommand(['set_property', 'sub-auto', 'no']);
deps.sendMpvCommand(['loadfile', plan.url, 'replace']);
if (params.setQuitOnDisconnectArm !== false) {
deps.armQuitOnDisconnect();
}
deps.sendMpvCommand([
'set_property',
'force-media-title',
`[Jellyfin/${plan.mode}] ${plan.title}`,
]);
deps.sendMpvCommand(['set_property', 'sid', 'no']);
deps.schedule(() => {
deps.sendMpvCommand(['set_property', 'sid', 'no']);
}, 500);
const startTimeTicks =
typeof params.startTimeTicksOverride === 'number'
? Math.max(0, params.startTimeTicksOverride)
: plan.startTimeTicks;
if (startTimeTicks > 0) {
deps.sendMpvCommand(['seek', deps.convertTicksToSeconds(startTimeTicks), 'absolute+exact']);
}
deps.preloadExternalSubtitles({
session: params.session,
clientInfo: params.clientInfo,
itemId: params.itemId,
});
const playMethod = plan.mode === 'direct' ? 'DirectPlay' : 'Transcode';
deps.setActivePlayback({
itemId: params.itemId,
mediaSourceId: undefined,
audioStreamIndex: plan.audioStreamIndex,
subtitleStreamIndex: plan.subtitleStreamIndex,
playMethod,
});
deps.setLastProgressAtMs(0);
deps.reportPlaying({
itemId: params.itemId,
mediaSourceId: undefined,
playMethod,
audioStreamIndex: plan.audioStreamIndex,
subtitleStreamIndex: plan.subtitleStreamIndex,
eventName: 'start',
});
deps.showMpvOsd(`Jellyfin ${plan.mode}: ${plan.title}`);
};
}