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.
`;
}
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();
};
}