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; getMpvClient: () => MpvRuntimeClientLike | null; resolvePlaybackPlan: (params: { session: JellyfinAuthSession; clientInfo: JellyfinClientInfo; jellyfinConfig: JellyfinConfig; itemId: string; audioStreamIndex?: number | null; subtitleStreamIndex?: number | null; }) => Promise; applyJellyfinMpvDefaults: (mpvClient: MpvRuntimeClientLike) => void; sendMpvCommand: (command: Array) => 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 => { 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}`); }; }