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; }; type JellyfinSetupWindowLike = FocusableWindowLike & { webContents: JellyfinSetupWebContentsLike; loadURL: (url: string) => unknown; on: (event: 'closed', handler: () => void) => void; isDestroyed: () => boolean; close: () => void; }; function escapeHtmlAttr(value: string): string { return value.replace(/"/g, '"'); } export function createMaybeFocusExistingJellyfinSetupWindowHandler(deps: { getSetupWindow: () => FocusableWindowLike | null; }) { return (): boolean => { const window = deps.getSetupWindow(); if (!window) { return false; } window.focus(); return true; }; } export function buildJellyfinSetupFormHtml(defaultServer: string, defaultUser: string): string { return ` Jellyfin Setup

Jellyfin Setup

Login info is used to fetch a token and save Jellyfin config values.

Equivalent CLI: --jellyfin-login --jellyfin-server ... --jellyfin-username ... --jellyfin-password ...
`; } export function parseJellyfinSetupSubmissionUrl(rawUrl: string): { server: string; username: string; password: string; } | null { if (!rawUrl.startsWith('subminer://jellyfin-setup')) { return null; } const parsed = new URL(rawUrl); return { server: parsed.searchParams.get('server') || '', username: parsed.searchParams.get('username') || '', password: parsed.searchParams.get('password') || '', }; } export function createHandleJellyfinSetupSubmissionHandler(deps: { parseSubmissionUrl: ( rawUrl: string, ) => { 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; patchJellyfinConfig: (session: JellyfinSession) => void; logInfo: (message: string) => void; logError: (message: string, error: unknown) => void; showMpvOsd: (message: string) => void; closeSetupWindow: () => void; }) { return async (rawUrl: string): Promise => { const submission = deps.parseSubmissionUrl(rawUrl); if (!submission) { return false; } try { const session = await deps.authenticateWithPassword( submission.server, submission.username, submission.password, deps.getJellyfinClientInfo(), ); deps.saveStoredSession({ accessToken: session.accessToken, userId: session.userId, }); deps.patchJellyfinConfig(session); deps.logInfo(`Jellyfin setup saved for ${session.username}.`); deps.showMpvOsd('Jellyfin login success'); deps.closeSetupWindow(); } catch (error) { const message = error instanceof Error ? error.message : String(error); deps.logError('Jellyfin setup failed', error); deps.showMpvOsd(`Jellyfin login failed: ${message}`); } 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; }; } 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 }; buildSetupFormHtml: (defaultServer: string, defaultUser: string) => string; parseSubmissionUrl: ( rawUrl: string, ) => { 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; patchJellyfinConfig: (session: JellyfinSession) => void; logInfo: (message: string) => void; logError: (message: string, error: unknown) => void; showMpvOsd: (message: string) => void; clearSetupWindow: () => void; setSetupWindow: (window: TWindow) => void; encodeURIComponent: (value: string) => string; }) { return (): void => { if (deps.maybeFocusExistingSetupWindow()) { return; } const setupWindow = deps.createSetupWindow(); const defaults = deps.getResolvedJellyfinConfig(); const defaultServer = defaults.serverUrl || 'http://127.0.0.1:8096'; const defaultUser = defaults.username || ''; const formHtml = deps.buildSetupFormHtml(defaultServer, defaultUser); 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), patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session), logInfo: (message) => deps.logInfo(message), logError: (message, error) => deps.logError(message, error), showMpvOsd: (message) => deps.showMpvOsd(message), closeSetupWindow: () => { if (!setupWindow.isDestroyed()) { setupWindow.close(); } }, }); const handleNavigation = createHandleJellyfinSetupNavigationHandler({ setupSchemePrefix: 'subminer://jellyfin-setup', handleSubmission: (rawUrl) => handleSubmission(rawUrl), 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?.(); } }, }); }); void setupWindow.loadURL(`data:text/html;charset=utf-8,${deps.encodeURIComponent(formHtml)}`); setupWindow.on('closed', () => { handleWindowClosed(); }); handleWindowOpened(); }; }