import { normalizeJellyfinRecentServers } from './jellyfin-cli-auth'; type JellyfinSession = { serverUrl: string; username: string; accessToken: string; userId: string; }; type JellyfinClientInfo = { clientName: string; clientVersion: string; deviceId: string; }; type FocusableWindowLike = { focus: () => void; }; type JellyfinSetupWebContentsLike = { on: (event: 'will-navigate', handler: (event: unknown, url: string) => void) => void; executeJavaScript?: (code: string, userGesture?: boolean) => Promise; }; type JellyfinSetupWindowLike = FocusableWindowLike & { webContents: JellyfinSetupWebContentsLike; loadURL: (url: string) => unknown; on: (event: 'closed', handler: () => void) => void; isDestroyed: () => boolean; close: () => void; }; export type JellyfinSetupAction = 'login' | 'logout' | 'done'; export type JellyfinSetupSubmission = { action: JellyfinSetupAction; server: string; username: string; password: string; }; export type JellyfinSetupViewState = { selectedServerUrl: string; username: string; hasStoredSession: boolean; statusMessage: string; statusKind: 'idle' | 'success' | 'error' | 'loading'; }; type JellyfinSetupViewOverrides = { selectedServerUrl?: string; username?: string; statusMessage?: string; statusKind?: JellyfinSetupViewState['statusKind']; }; export type JellyfinSetupIpcResult = { handled: boolean; statusMessage?: string; statusKind?: JellyfinSetupViewState['statusKind']; }; type RegisterJellyfinSetupIpcHandler = ( handler: (submission: unknown) => Promise, ) => () => void; function escapeHtmlAttr(value: string): string { return value.replace(/"/g, '"'); } function escapeHtml(value: string): string { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } function normalizeSetupAction(value: unknown): JellyfinSetupAction { return value === 'logout' || value === 'done' ? value : 'login'; } function normalizeString(value: unknown): string { return typeof value === 'string' ? value : ''; } export function createMaybeFocusExistingJellyfinSetupWindowHandler(deps: { getSetupWindow: () => FocusableWindowLike | null; }) { return (): boolean => { const window = deps.getSetupWindow(); if (!window) { return false; } window.focus(); return true; }; } export function buildJellyfinSetupViewState(input: { config: { serverUrl?: string | null; username?: string | null; recentServers?: unknown[]; }; defaultServerUrl: string; hasStoredSession: boolean; statusMessage?: string; statusKind?: JellyfinSetupViewState['statusKind']; selectedServerUrl?: string; username?: string; }): JellyfinSetupViewState { const configServer = normalizeJellyfinRecentServers([input.config.serverUrl || ''])[0] || ''; const recentServers = normalizeJellyfinRecentServers(input.config.recentServers || []); const defaultServer = normalizeJellyfinRecentServers([input.defaultServerUrl])[0] || ''; const selectedServerUrl = normalizeJellyfinRecentServers([input.selectedServerUrl || ''])[0] || configServer || recentServers[0] || defaultServer; return { selectedServerUrl, username: input.username ?? input.config.username ?? '', hasStoredSession: input.hasStoredSession, statusMessage: input.statusMessage || '', statusKind: input.statusKind || 'idle', }; } export function buildJellyfinSetupFormHtml(state: JellyfinSetupViewState): string { const statusClass = `status ${state.statusKind}`; return ` Jellyfin Setup

Jellyfin Setup

Enter your Jellyfin server URL, sign in, and SubMiner will save a session token for Jellyfin commands and cast discovery.

${escapeHtml(state.statusMessage)}
${ state.hasStoredSession ? '' : '' }
Equivalent CLI: --jellyfin-login --jellyfin-server ... --jellyfin-username ... --jellyfin-password ...
`; } export function parseJellyfinSetupSubmissionUrl(rawUrl: string): { action: JellyfinSetupAction; server: string; username: string; password: string; } | null { if (!rawUrl.startsWith('subminer://jellyfin-setup')) { return null; } const parsed = new URL(rawUrl); const rawAction = parsed.searchParams.get('action') || 'login'; const action: JellyfinSetupAction = rawAction === 'logout' || rawAction === 'done' ? rawAction : 'login'; return { action, server: parsed.searchParams.get('server') || '', username: parsed.searchParams.get('username') || '', password: parsed.searchParams.get('password') || '', }; } export function normalizeJellyfinSetupIpcSubmission( value: unknown, ): JellyfinSetupSubmission | null { if (!isRecord(value)) { return null; } return { action: normalizeSetupAction(value.action), server: normalizeString(value.server), username: normalizeString(value.username), password: normalizeString(value.password), }; } export function buildJellyfinSetupSubmissionUrl(submission: JellyfinSetupSubmission): string { const params = new URLSearchParams(); params.set('action', submission.action); if (submission.action === 'login') { params.set('server', submission.server); params.set('username', submission.username); } return `subminer://jellyfin-setup?${params.toString()}`; } export function createHandleJellyfinSetupSubmissionHandler(deps: { parseSubmissionUrl: ( rawUrl: string, ) => { action: JellyfinSetupAction; server: string; username: string; password: string } | null; authenticateWithPassword: ( server: string, username: string, password: string, clientInfo: JellyfinClientInfo, ) => Promise; getJellyfinClientInfo: () => JellyfinClientInfo; saveStoredSession: (session: { accessToken: string; userId: string }) => void; clearStoredSession: () => void; patchJellyfinConfig: (session: JellyfinSession) => void; persistAuthenticatedSession?: (session: JellyfinSession, clientInfo: JellyfinClientInfo) => void; restartRemoteSession?: () => Promise | void; stopRemoteSession?: () => void; logInfo: (message: string) => void; logError: (message: string, error: unknown) => void; showMpvOsd: (message: string) => void; closeSetupWindow: () => void; reloadSetupWindow: (state?: JellyfinSetupViewOverrides) => void; }) { let loginInFlight = false; return async (rawUrl: string, passwordOverride?: string): Promise => { const submission = deps.parseSubmissionUrl(rawUrl); if (!submission) { return false; } if (submission.action === 'done') { deps.closeSetupWindow(); return true; } if (submission.action === 'logout') { try { deps.clearStoredSession(); deps.stopRemoteSession?.(); deps.logInfo('Cleared stored Jellyfin auth session.'); deps.showMpvOsd('Jellyfin logged out'); deps.reloadSetupWindow({ statusMessage: 'Jellyfin session cleared.', statusKind: 'success', }); } catch (error) { const message = error instanceof Error ? error.message : String(error); deps.logError('Jellyfin logout failed', error); deps.showMpvOsd(`Jellyfin logout failed: ${message}`); deps.reloadSetupWindow({ statusMessage: message, statusKind: 'error', }); } return true; } if (loginInFlight) { deps.showMpvOsd('Jellyfin login already in progress'); deps.reloadSetupWindow({ selectedServerUrl: submission.server, username: submission.username, statusMessage: 'Jellyfin login already in progress.', statusKind: 'loading', }); return true; } loginInFlight = true; try { const clientInfo = deps.getJellyfinClientInfo(); const session = await deps.authenticateWithPassword( submission.server, submission.username, passwordOverride ?? submission.password, clientInfo, ); if (deps.persistAuthenticatedSession) { deps.persistAuthenticatedSession(session, clientInfo); } else { deps.saveStoredSession({ accessToken: session.accessToken, userId: session.userId }); deps.patchJellyfinConfig(session); } await deps.restartRemoteSession?.(); deps.logInfo(`Jellyfin setup saved for ${session.username}.`); deps.showMpvOsd('Jellyfin login success'); deps.reloadSetupWindow({ selectedServerUrl: session.serverUrl, username: session.username, statusMessage: `Authenticated as ${session.username}.`, statusKind: 'success', }); } catch (error) { const message = error instanceof Error ? error.message : String(error); deps.logError('Jellyfin setup failed', error); deps.showMpvOsd(`Jellyfin login failed: ${message}`); deps.reloadSetupWindow({ selectedServerUrl: submission.server, username: submission.username, statusMessage: message, statusKind: 'error', }); } finally { loginInFlight = false; } return true; }; } export function createHandleJellyfinSetupNavigationHandler(deps: { setupSchemePrefix: string; handleSubmission: (rawUrl: string) => Promise; logError: (message: string, error: unknown) => void; }) { return (params: { url: string; preventDefault: () => void }): boolean => { if (!params.url.startsWith(deps.setupSchemePrefix)) { return false; } params.preventDefault(); void deps.handleSubmission(params.url).catch((error) => { deps.logError('Failed handling Jellyfin setup submission', error); }); return true; }; } async function readJellyfinSetupPasswordFromWindow( setupWindow: JellyfinSetupWindowLike, ): Promise { const executeJavaScript = setupWindow.webContents.executeJavaScript; if (!executeJavaScript) { return undefined; } const value = await executeJavaScript( `(() => { const input = document.getElementById("password"); const password = String(window.__subminerJellyfinPassword || input?.value || ""); window.__subminerJellyfinPassword = ""; if (input) input.value = ""; return password; })()`, true, ); return typeof value === 'string' ? value : ''; } export function createHandleJellyfinSetupWindowClosedHandler(deps: { clearSetupWindow: () => void; }) { return (): void => { deps.clearSetupWindow(); }; } export function createHandleJellyfinSetupWindowOpenedHandler(deps: { setSetupWindow: () => void }) { return (): void => { deps.setSetupWindow(); }; } export function createOpenJellyfinSetupWindowHandler< TWindow extends JellyfinSetupWindowLike, >(deps: { maybeFocusExistingSetupWindow: () => boolean; createSetupWindow: () => TWindow; getResolvedJellyfinConfig: () => { serverUrl?: string | null; username?: string | null; recentServers?: unknown[]; }; buildSetupFormHtml: (state: JellyfinSetupViewState) => string; parseSubmissionUrl: ( rawUrl: string, ) => { action: JellyfinSetupAction; server: string; username: string; password: string } | null; authenticateWithPassword: ( server: string, username: string, password: string, clientInfo: JellyfinClientInfo, ) => Promise; getJellyfinClientInfo: () => JellyfinClientInfo; saveStoredSession: (session: { accessToken: string; userId: string }) => void; clearStoredSession: () => void; patchJellyfinConfig: (session: JellyfinSession) => void; persistAuthenticatedSession?: (session: JellyfinSession, clientInfo: JellyfinClientInfo) => void; restartRemoteSession?: () => Promise | void; stopRemoteSession?: () => void; logInfo: (message: string) => void; logError: (message: string, error: unknown) => void; showMpvOsd: (message: string) => void; clearSetupWindow: () => void; setSetupWindow: (window: TWindow) => void; registerSetupIpcHandler?: RegisterJellyfinSetupIpcHandler; encodeURIComponent: (value: string) => string; defaultServerUrl: string; hasStoredSession: () => boolean; }) { return (): void => { if (deps.maybeFocusExistingSetupWindow()) { return; } const setupWindow = deps.createSetupWindow(); const loadSetupForm = (overrides: JellyfinSetupViewOverrides = {}) => { const state = buildJellyfinSetupViewState({ config: deps.getResolvedJellyfinConfig(), defaultServerUrl: deps.defaultServerUrl, hasStoredSession: deps.hasStoredSession(), selectedServerUrl: overrides.selectedServerUrl, username: overrides.username, statusMessage: overrides.statusMessage, statusKind: overrides.statusKind, }); const formHtml = deps.buildSetupFormHtml(state); void setupWindow.loadURL(`data:text/html;charset=utf-8,${deps.encodeURIComponent(formHtml)}`); }; const handleSubmission = createHandleJellyfinSetupSubmissionHandler({ parseSubmissionUrl: (rawUrl) => deps.parseSubmissionUrl(rawUrl), authenticateWithPassword: (server, username, password, clientInfo) => deps.authenticateWithPassword(server, username, password, clientInfo), getJellyfinClientInfo: () => deps.getJellyfinClientInfo(), saveStoredSession: (session) => deps.saveStoredSession(session), clearStoredSession: () => deps.clearStoredSession(), patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session), persistAuthenticatedSession: deps.persistAuthenticatedSession ? (session, clientInfo) => deps.persistAuthenticatedSession?.(session, clientInfo) : undefined, restartRemoteSession: deps.restartRemoteSession ? () => deps.restartRemoteSession?.() : undefined, stopRemoteSession: deps.stopRemoteSession ? () => deps.stopRemoteSession?.() : undefined, logInfo: (message) => deps.logInfo(message), logError: (message, error) => deps.logError(message, error), showMpvOsd: (message) => deps.showMpvOsd(message), closeSetupWindow: () => { if (!setupWindow.isDestroyed()) { setupWindow.close(); } }, reloadSetupWindow: (state) => { if (!setupWindow.isDestroyed()) { loadSetupForm(state); } }, }); const unregisterSetupIpcHandler = deps.registerSetupIpcHandler?.(async (payload) => { const submission = normalizeJellyfinSetupIpcSubmission(payload); if (!submission) { return { handled: false, statusMessage: 'Invalid Jellyfin setup request.', statusKind: 'error', }; } const handled = await handleSubmission( buildJellyfinSetupSubmissionUrl(submission), submission.password, ); return { handled }; }); const handleNavigation = createHandleJellyfinSetupNavigationHandler({ setupSchemePrefix: 'subminer://jellyfin-setup', handleSubmission: async (rawUrl) => { const submission = deps.parseSubmissionUrl(rawUrl); let password: string | undefined; if (submission?.action === 'login' && !submission.password) { try { password = await readJellyfinSetupPasswordFromWindow(setupWindow); } catch (error) { deps.logError('Failed reading Jellyfin setup password', error); password = ''; } } return handleSubmission(rawUrl, password); }, logError: (message, error) => deps.logError(message, error), }); const handleWindowClosed = createHandleJellyfinSetupWindowClosedHandler({ clearSetupWindow: () => deps.clearSetupWindow(), }); const handleWindowOpened = createHandleJellyfinSetupWindowOpenedHandler({ setSetupWindow: () => deps.setSetupWindow(setupWindow), }); setupWindow.webContents.on('will-navigate', (event, url) => { handleNavigation({ url, preventDefault: () => { if (event && typeof event === 'object' && 'preventDefault' in event) { const typedEvent = event as { preventDefault?: () => void }; typedEvent.preventDefault?.(); } }, }); }); loadSetupForm(); setupWindow.on('closed', () => { unregisterSetupIpcHandler?.(); handleWindowClosed(); }); handleWindowOpened(); }; }