feat(jellyfin): move auth to env and stored session

This commit is contained in:
2026-02-20 20:37:21 -08:00
parent d6676f7132
commit 8ac3d517fe
26 changed files with 336 additions and 132 deletions

View File

@@ -220,8 +220,6 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
enabled: false,
serverUrl: '',
username: '',
accessToken: '',
userId: '',
deviceId: 'subminer',
clientName: 'SubMiner',
clientVersion: '0.1.0',
@@ -807,8 +805,8 @@ export const CONFIG_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
title: 'Jellyfin',
description: [
'Optional Jellyfin integration for auth, browsing, and playback launch.',
'Access token is stored in local encrypted token storage after login/setup.',
'jellyfin.accessToken remains an optional explicit override in config.',
'Auth session (access token + user id) is stored in local encrypted storage after login/setup.',
'Optional env overrides: SUBMINER_JELLYFIN_ACCESS_TOKEN and SUBMINER_JELLYFIN_USER_ID.',
],
key: 'jellyfin',
},

View File

@@ -519,8 +519,6 @@ export class ConfigService {
const stringKeys = [
'serverUrl',
'username',
'accessToken',
'userId',
'deviceId',
'clientName',
'clientVersion',

View File

@@ -2,16 +2,27 @@ import * as fs from 'fs';
import * as path from 'path';
import { safeStorage } from 'electron';
interface PersistedTokenPayload {
interface PersistedSessionPayload {
encryptedSession?: string;
plaintextSession?: {
accessToken?: string;
userId?: string;
};
// Legacy payload fields (token only).
encryptedToken?: string;
plaintextToken?: string;
updatedAt?: number;
}
export interface JellyfinStoredSession {
accessToken: string;
userId: string;
}
export interface JellyfinTokenStore {
loadToken: () => string | null;
saveToken: (token: string) => void;
clearToken: () => void;
loadSession: () => JellyfinStoredSession | null;
saveSession: (session: JellyfinStoredSession) => void;
clearSession: () => void;
}
function ensureDirectory(filePath: string): void {
@@ -21,7 +32,7 @@ function ensureDirectory(filePath: string): void {
}
}
function writePayload(filePath: string, payload: PersistedTokenPayload): void {
function writePayload(filePath: string, payload: PersistedSessionPayload): void {
ensureDirectory(filePath);
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8');
}
@@ -35,65 +46,94 @@ export function createJellyfinTokenStore(
},
): JellyfinTokenStore {
return {
loadToken(): string | null {
loadSession(): JellyfinStoredSession | null {
if (!fs.existsSync(filePath)) {
return null;
}
try {
const raw = fs.readFileSync(filePath, 'utf-8');
const parsed = JSON.parse(raw) as PersistedTokenPayload;
if (typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) {
const encrypted = Buffer.from(parsed.encryptedToken, 'base64');
const parsed = JSON.parse(raw) as PersistedSessionPayload;
if (typeof parsed.encryptedSession === 'string' && parsed.encryptedSession.length > 0) {
const encrypted = Buffer.from(parsed.encryptedSession, 'base64');
if (!safeStorage.isEncryptionAvailable()) {
logger.warn('Jellyfin token encryption is not available on this system.');
logger.warn('Jellyfin session encryption is not available on this system.');
return null;
}
const decrypted = safeStorage.decryptString(encrypted).trim();
return decrypted.length > 0 ? decrypted : null;
const session = JSON.parse(decrypted) as Partial<JellyfinStoredSession>;
const accessToken = typeof session.accessToken === 'string' ? session.accessToken.trim() : '';
const userId = typeof session.userId === 'string' ? session.userId.trim() : '';
if (!accessToken || !userId) return null;
return { accessToken, userId };
}
if (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) {
const plaintext = parsed.plaintextToken.trim();
this.saveToken(plaintext);
return plaintext;
if (parsed.plaintextSession && typeof parsed.plaintextSession === 'object') {
const accessToken =
typeof parsed.plaintextSession.accessToken === 'string'
? parsed.plaintextSession.accessToken.trim()
: '';
const userId =
typeof parsed.plaintextSession.userId === 'string'
? parsed.plaintextSession.userId.trim()
: '';
if (accessToken && userId) {
const session = { accessToken, userId };
this.saveSession(session);
return session;
}
}
if (
(typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) ||
(typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0)
) {
logger.warn('Ignoring legacy Jellyfin token-only store payload because userId is missing.');
}
} catch (error) {
logger.error('Failed to read Jellyfin token store.', error);
logger.error('Failed to read Jellyfin session store.', error);
}
return null;
},
saveToken(token: string): void {
const trimmed = token.trim();
if (trimmed.length === 0) {
this.clearToken();
saveSession(session: JellyfinStoredSession): void {
const accessToken = session.accessToken.trim();
const userId = session.userId.trim();
if (!accessToken || !userId) {
this.clearSession();
return;
}
try {
if (!safeStorage.isEncryptionAvailable()) {
logger.warn('Jellyfin token encryption unavailable; storing token in plaintext fallback.');
logger.warn(
'Jellyfin session encryption unavailable; storing session in plaintext fallback.',
);
writePayload(filePath, {
plaintextToken: trimmed,
plaintextSession: {
accessToken,
userId,
},
updatedAt: Date.now(),
});
return;
}
const encrypted = safeStorage.encryptString(trimmed);
const encrypted = safeStorage.encryptString(JSON.stringify({ accessToken, userId }));
writePayload(filePath, {
encryptedToken: encrypted.toString('base64'),
encryptedSession: encrypted.toString('base64'),
updatedAt: Date.now(),
});
} catch (error) {
logger.error('Failed to persist Jellyfin token.', error);
logger.error('Failed to persist Jellyfin session.', error);
}
},
clearToken(): void {
clearSession(): void {
if (!fs.existsSync(filePath)) return;
try {
fs.unlinkSync(filePath);
logger.info('Cleared stored Jellyfin token.');
logger.info('Cleared stored Jellyfin session.');
} catch (error) {
logger.error('Failed to clear stored Jellyfin token.', error);
logger.error('Failed to clear stored Jellyfin session.', error);
}
},
};

View File

@@ -1166,7 +1166,8 @@ function getResolvedConfig() {
const buildGetResolvedJellyfinConfigMainDepsHandler =
createBuildGetResolvedJellyfinConfigMainDepsHandler({
getResolvedConfig: () => getResolvedConfig(),
loadStoredToken: () => jellyfinTokenStore.loadToken(),
loadStoredSession: () => jellyfinTokenStore.loadSession(),
getEnv: (name: string) => process.env[name],
});
const getResolvedJellyfinConfigMainDeps =
buildGetResolvedJellyfinConfigMainDepsHandler();
@@ -1322,8 +1323,8 @@ const buildHandleJellyfinAuthCommandsMainDepsHandler =
},
authenticateWithPassword: (serverUrl, username, password, clientInfo) =>
authenticateWithPasswordRuntime(serverUrl, username, password, clientInfo),
saveStoredToken: (token) => jellyfinTokenStore.saveToken(token),
clearStoredToken: () => jellyfinTokenStore.clearToken(),
saveStoredSession: (session) => jellyfinTokenStore.saveSession(session),
clearStoredSession: () => jellyfinTokenStore.clearSession(),
logInfo: (message) => logger.info(message),
});
const handleJellyfinAuthCommandsMainDeps =
@@ -1586,15 +1587,13 @@ const buildOpenJellyfinSetupWindowMainDepsHandler =
authenticateWithPassword: (server, username, password, clientInfo) =>
authenticateWithPasswordRuntime(server, username, password, clientInfo),
getJellyfinClientInfo: () => getJellyfinClientInfo(),
saveStoredToken: (token) => jellyfinTokenStore.saveToken(token),
saveStoredSession: (session) => jellyfinTokenStore.saveSession(session),
patchJellyfinConfig: (session) => {
configService.patchRawConfig({
jellyfin: {
enabled: true,
serverUrl: session.serverUrl,
username: session.username,
accessToken: '',
userId: session.userId,
},
});
},

View File

@@ -6,8 +6,8 @@ test('jellyfin auth handler processes logout', async () => {
const calls: string[] = [];
const handleAuth = createHandleJellyfinAuthCommands({
patchRawConfig: () => calls.push('patch'),
saveStoredToken: () => calls.push('save'),
clearStoredToken: () => calls.push('clear'),
saveStoredSession: () => calls.push('save'),
clearStoredSession: () => calls.push('clear'),
authenticateWithPassword: async () => {
throw new Error('should not authenticate');
},
@@ -24,8 +24,6 @@ test('jellyfin auth handler processes logout', async () => {
jellyfinConfig: {
serverUrl: '',
username: '',
accessToken: '',
userId: '',
},
serverUrl: 'http://localhost',
clientInfo: {
@@ -41,10 +39,18 @@ test('jellyfin auth handler processes logout', async () => {
test('jellyfin auth handler processes login', async () => {
const calls: string[] = [];
let patchPayload: unknown = null;
let storedSession: unknown = null;
const handleAuth = createHandleJellyfinAuthCommands({
patchRawConfig: () => calls.push('patch'),
saveStoredToken: () => calls.push('save'),
clearStoredToken: () => calls.push('clear'),
patchRawConfig: (patch) => {
patchPayload = patch;
calls.push('patch');
},
saveStoredSession: (session) => {
storedSession = session;
calls.push('save');
},
clearStoredSession: () => calls.push('clear'),
authenticateWithPassword: async () => ({
serverUrl: 'http://localhost',
username: 'user',
@@ -64,8 +70,6 @@ test('jellyfin auth handler processes login', async () => {
jellyfinConfig: {
serverUrl: '',
username: '',
accessToken: '',
userId: '',
},
serverUrl: 'http://localhost',
clientInfo: {
@@ -78,14 +82,25 @@ test('jellyfin auth handler processes login', async () => {
assert.equal(handled, true);
assert.ok(calls.includes('save'));
assert.ok(calls.includes('patch'));
assert.deepEqual(storedSession, { accessToken: 'token', userId: 'uid' });
assert.deepEqual(patchPayload, {
jellyfin: {
enabled: true,
serverUrl: 'http://localhost',
username: 'user',
deviceId: 'd1',
clientName: 'SubMiner',
clientVersion: '1.0',
},
});
assert.ok(calls.some((entry) => entry.includes('Jellyfin login succeeded')));
});
test('jellyfin auth handler no-ops when no auth command', async () => {
const handleAuth = createHandleJellyfinAuthCommands({
patchRawConfig: () => {},
saveStoredToken: () => {},
clearStoredToken: () => {},
saveStoredSession: () => {},
clearStoredSession: () => {},
authenticateWithPassword: async () => ({
serverUrl: '',
username: '',
@@ -105,8 +120,6 @@ test('jellyfin auth handler no-ops when no auth command', async () => {
jellyfinConfig: {
serverUrl: '',
username: '',
accessToken: '',
userId: '',
},
serverUrl: 'http://localhost',
clientInfo: {

View File

@@ -3,8 +3,6 @@ import type { CliArgs } from '../../cli/args';
type JellyfinConfig = {
serverUrl: string;
username: string;
accessToken: string;
userId: string;
};
type JellyfinClientInfo = {
@@ -26,8 +24,6 @@ export function createHandleJellyfinAuthCommands(deps: {
enabled: boolean;
serverUrl: string;
username: string;
accessToken: string;
userId: string;
deviceId: string;
clientName: string;
clientVersion: string;
@@ -39,8 +35,8 @@ export function createHandleJellyfinAuthCommands(deps: {
password: string,
clientInfo: JellyfinClientInfo,
) => Promise<JellyfinSession>;
saveStoredToken: (token: string) => void;
clearStoredToken: () => void;
saveStoredSession: (session: { accessToken: string; userId: string }) => void;
clearStoredSession: () => void;
logInfo: (message: string) => void;
}) {
return async (params: {
@@ -50,14 +46,11 @@ export function createHandleJellyfinAuthCommands(deps: {
clientInfo: JellyfinClientInfo;
}): Promise<boolean> => {
if (params.args.jellyfinLogout) {
deps.clearStoredToken();
deps.clearStoredSession();
deps.patchRawConfig({
jellyfin: {
accessToken: '',
userId: '',
},
jellyfin: {},
});
deps.logInfo('Cleared stored Jellyfin access token.');
deps.logInfo('Cleared stored Jellyfin auth session.');
return true;
}
@@ -73,14 +66,15 @@ export function createHandleJellyfinAuthCommands(deps: {
password,
params.clientInfo,
);
deps.saveStoredToken(session.accessToken);
deps.saveStoredSession({
accessToken: session.accessToken,
userId: session.userId,
});
deps.patchRawConfig({
jellyfin: {
enabled: true,
serverUrl: session.serverUrl,
username: session.username,
accessToken: '',
userId: session.userId,
deviceId: params.clientInfo.deviceId,
clientName: params.clientInfo.clientName,
clientVersion: params.clientInfo.clientVersion,

View File

@@ -12,8 +12,8 @@ test('jellyfin auth commands main deps builder maps callbacks', async () => {
const deps = createBuildHandleJellyfinAuthCommandsMainDepsHandler({
patchRawConfig: () => calls.push('patch'),
authenticateWithPassword: async () => ({}) as never,
saveStoredToken: () => calls.push('save'),
clearStoredToken: () => calls.push('clear'),
saveStoredSession: () => calls.push('save'),
clearStoredSession: () => calls.push('clear'),
logInfo: (message) => calls.push(`info:${message}`),
})();
@@ -23,8 +23,8 @@ test('jellyfin auth commands main deps builder maps callbacks', async () => {
clientName: '',
clientVersion: '',
});
deps.saveStoredToken('token');
deps.clearStoredToken();
deps.saveStoredSession({ accessToken: 'token', userId: 'uid' });
deps.clearStoredSession();
deps.logInfo('ok');
assert.deepEqual(calls, ['patch', 'save', 'clear', 'info:ok']);
});

View File

@@ -25,8 +25,8 @@ export function createBuildHandleJellyfinAuthCommandsMainDepsHandler(
patchRawConfig: (patch) => deps.patchRawConfig(patch),
authenticateWithPassword: (serverUrl, username, password, clientInfo) =>
deps.authenticateWithPassword(serverUrl, username, password, clientInfo),
saveStoredToken: (token) => deps.saveStoredToken(token),
clearStoredToken: () => deps.clearStoredToken(),
saveStoredSession: (session) => deps.saveStoredSession(session),
clearStoredSession: () => deps.clearStoredSession(),
logInfo: (message: string) => deps.logInfo(message),
});
}

View File

@@ -9,10 +9,12 @@ test('get resolved jellyfin config main deps builder maps callbacks', () => {
const resolved = { jellyfin: { url: 'https://example.com' } };
const deps = createBuildGetResolvedJellyfinConfigMainDepsHandler({
getResolvedConfig: () => resolved as never,
loadStoredToken: () => 'stored-token',
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'uid' }),
getEnv: (key: string) => (key === 'TEST' ? 'x' : undefined),
})();
assert.equal(deps.getResolvedConfig(), resolved);
assert.equal(deps.loadStoredToken(), 'stored-token');
assert.deepEqual(deps.loadStoredSession(), { accessToken: 'stored-token', userId: 'uid' });
assert.equal(deps.getEnv('TEST'), 'x');
});
test('get jellyfin client info main deps builder maps callbacks', () => {

View File

@@ -11,7 +11,8 @@ export function createBuildGetResolvedJellyfinConfigMainDepsHandler(
) {
return (): GetResolvedJellyfinConfigMainDeps => ({
getResolvedConfig: () => deps.getResolvedConfig(),
loadStoredToken: () => deps.loadStoredToken(),
loadStoredSession: () => deps.loadStoredSession(),
getEnv: (name: string) => deps.getEnv(name),
});
}

View File

@@ -9,23 +9,23 @@ test('get resolved jellyfin config returns jellyfin section from resolved config
const jellyfin = { url: 'https://jellyfin.local' } as never;
const getConfig = createGetResolvedJellyfinConfigHandler({
getResolvedConfig: () => ({ jellyfin } as never),
loadStoredToken: () => null,
loadStoredSession: () => null,
getEnv: () => undefined,
});
assert.equal(getConfig(), jellyfin);
});
test('get resolved jellyfin config falls back to stored token when config token is blank', () => {
test('get resolved jellyfin config falls back to stored session when env is unset', () => {
const getConfig = createGetResolvedJellyfinConfigHandler({
getResolvedConfig: () =>
({
jellyfin: {
serverUrl: 'http://localhost:8096',
accessToken: ' ',
userId: 'uid-1',
},
}) as never,
loadStoredToken: () => 'stored-token',
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'uid-1' }),
getEnv: () => undefined,
});
assert.deepEqual(getConfig(), {
@@ -35,6 +35,50 @@ test('get resolved jellyfin config falls back to stored token when config token
});
});
test('get resolved jellyfin config prefers env token and env user id over stored session', () => {
const getConfig = createGetResolvedJellyfinConfigHandler({
getResolvedConfig: () =>
({
jellyfin: {
serverUrl: 'http://localhost:8096',
},
}) as never,
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }),
getEnv: (key: string) =>
key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN'
? 'env-token'
: key === 'SUBMINER_JELLYFIN_USER_ID'
? 'env-user'
: undefined,
});
assert.deepEqual(getConfig(), {
serverUrl: 'http://localhost:8096',
accessToken: 'env-token',
userId: 'env-user',
});
});
test('get resolved jellyfin config uses stored user id when env token set without env user id', () => {
const getConfig = createGetResolvedJellyfinConfigHandler({
getResolvedConfig: () =>
({
jellyfin: {
serverUrl: 'http://localhost:8096',
},
}) as never,
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }),
getEnv: (key: string) =>
key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined,
});
assert.deepEqual(getConfig(), {
serverUrl: 'http://localhost:8096',
accessToken: 'env-token',
userId: 'stored-user',
});
});
test('jellyfin client info resolves defaults when fields are missing', () => {
const getClientInfo = createGetJellyfinClientInfoHandler({
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never),

View File

@@ -1,24 +1,37 @@
export function createGetResolvedJellyfinConfigHandler(deps: {
getResolvedConfig: () => { jellyfin: unknown };
loadStoredToken: () => string | null | undefined;
loadStoredSession: () => { accessToken: string; userId: string } | null | undefined;
getEnv: (name: string) => string | undefined;
}) {
return () => {
const jellyfin = deps.getResolvedConfig().jellyfin as {
accessToken?: string;
userId?: string;
[key: string]: unknown;
};
const configToken = jellyfin.accessToken?.trim() ?? '';
if (configToken.length > 0) {
return jellyfin as never;
const envToken = deps.getEnv('SUBMINER_JELLYFIN_ACCESS_TOKEN')?.trim() ?? '';
const envUserId = deps.getEnv('SUBMINER_JELLYFIN_USER_ID')?.trim() ?? '';
const stored = deps.loadStoredSession();
const storedToken = stored?.accessToken?.trim() ?? '';
const storedUserId = stored?.userId?.trim() ?? '';
if (envToken.length > 0) {
return {
...jellyfin,
accessToken: envToken,
userId: envUserId || storedUserId || '',
} as never;
}
const storedToken = deps.loadStoredToken()?.trim() ?? '';
if (storedToken.length === 0) {
return jellyfin as never;
if (storedToken.length > 0 && storedUserId.length > 0) {
return {
...jellyfin,
accessToken: storedToken,
userId: storedUserId,
} as never;
}
return {
...jellyfin,
accessToken: storedToken,
} as never;
return jellyfin as never;
};
}

View File

@@ -20,10 +20,10 @@ type JellyfinClientInfo = {
};
type JellyfinConfigLike = {
serverUrl: string;
accessToken: string;
userId: string;
username: string;
serverUrl?: string;
accessToken?: string;
userId?: string;
username?: string;
};
function asInteger(value: unknown): number | undefined {
@@ -39,7 +39,7 @@ export function getConfiguredJellyfinSession(config: JellyfinConfigLike): Jellyf
serverUrl: config.serverUrl,
accessToken: config.accessToken,
userId: config.userId,
username: config.username,
username: config.username || '',
};
}

View File

@@ -2,8 +2,8 @@ type JellyfinRemoteConfig = {
remoteControlEnabled: boolean;
remoteControlAutoConnect: boolean;
serverUrl: string;
accessToken: string;
userId: string;
accessToken?: string;
userId?: string;
deviceId: string;
clientName: string;
clientVersion: string;

View File

@@ -17,7 +17,7 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
userId: 'uid',
}),
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'dev' }),
saveStoredToken: () => calls.push('save'),
saveStoredSession: () => calls.push('save'),
patchJellyfinConfig: () => calls.push('patch'),
logInfo: (message) => calls.push(`info:${message}`),
logError: (message) => calls.push(`error:${message}`),
@@ -44,7 +44,7 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
accessToken: 'token',
userId: 'uid',
});
deps.saveStoredToken('token');
deps.saveStoredSession({ accessToken: 'token', userId: 'uid' });
deps.patchJellyfinConfig({
serverUrl: 'http://127.0.0.1:8096',
username: 'alice',

View File

@@ -15,7 +15,7 @@ export function createBuildOpenJellyfinSetupWindowMainDepsHandler(
authenticateWithPassword: (server: string, username: string, password: string, clientInfo) =>
deps.authenticateWithPassword(server, username, password, clientInfo),
getJellyfinClientInfo: () => deps.getJellyfinClientInfo(),
saveStoredToken: (token: string) => deps.saveStoredToken(token),
saveStoredSession: (session) => deps.saveStoredSession(session),
patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session),
logInfo: (message: string) => deps.logInfo(message),
logError: (message: string, error: unknown) => deps.logError(message, error),

View File

@@ -40,6 +40,8 @@ test('parseJellyfinSetupSubmissionUrl parses setup url parameters', () => {
test('createHandleJellyfinSetupSubmissionHandler applies successful login', async () => {
const calls: string[] = [];
let patchPayload: unknown = null;
let savedSession: unknown = null;
const handler = createHandleJellyfinSetupSubmissionHandler({
parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
authenticateWithPassword: async () => ({
@@ -49,8 +51,14 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
userId: 'uid',
}),
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
saveStoredToken: () => calls.push('save'),
patchJellyfinConfig: () => calls.push('patch'),
saveStoredSession: (session) => {
savedSession = session;
calls.push('save');
},
patchJellyfinConfig: (session) => {
patchPayload = session;
calls.push('patch');
},
logInfo: () => calls.push('info'),
logError: () => calls.push('error'),
showMpvOsd: (message) => calls.push(`osd:${message}`),
@@ -62,6 +70,13 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
);
assert.equal(handled, true);
assert.deepEqual(calls, ['save', 'patch', 'info', 'osd:Jellyfin login success', 'close']);
assert.deepEqual(savedSession, { accessToken: 'token', userId: 'uid' });
assert.deepEqual(patchPayload, {
serverUrl: 'http://localhost',
username: 'user',
accessToken: 'token',
userId: 'uid',
});
});
test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async () => {
@@ -72,7 +87,7 @@ test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async
throw new Error('bad credentials');
},
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
saveStoredToken: () => calls.push('save'),
saveStoredSession: () => calls.push('save'),
patchJellyfinConfig: () => calls.push('patch'),
logInfo: () => calls.push('info'),
logError: () => calls.push('error'),
@@ -166,7 +181,7 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is
throw new Error('should not auth');
},
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
saveStoredToken: () => {},
saveStoredSession: () => {},
patchJellyfinConfig: () => {},
logInfo: () => {},
logError: () => {},
@@ -219,7 +234,7 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
userId: 'uid',
}),
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
saveStoredToken: () => calls.push('save'),
saveStoredSession: () => calls.push('save'),
patchJellyfinConfig: () => calls.push('patch'),
logInfo: () => calls.push('info'),
logError: () => calls.push('error'),

View File

@@ -117,7 +117,7 @@ export function createHandleJellyfinSetupSubmissionHandler(deps: {
clientInfo: JellyfinClientInfo,
) => Promise<JellyfinSession>;
getJellyfinClientInfo: () => JellyfinClientInfo;
saveStoredToken: (token: string) => void;
saveStoredSession: (session: { accessToken: string; userId: string }) => void;
patchJellyfinConfig: (session: JellyfinSession) => void;
logInfo: (message: string) => void;
logError: (message: string, error: unknown) => void;
@@ -137,7 +137,10 @@ export function createHandleJellyfinSetupSubmissionHandler(deps: {
submission.password,
deps.getJellyfinClientInfo(),
);
deps.saveStoredToken(session.accessToken);
deps.saveStoredSession({
accessToken: session.accessToken,
userId: session.userId,
});
deps.patchJellyfinConfig(session);
deps.logInfo(`Jellyfin setup saved for ${session.username}.`);
deps.showMpvOsd('Jellyfin login success');
@@ -197,7 +200,7 @@ export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSet
clientInfo: JellyfinClientInfo,
) => Promise<JellyfinSession>;
getJellyfinClientInfo: () => JellyfinClientInfo;
saveStoredToken: (token: string) => void;
saveStoredSession: (session: { accessToken: string; userId: string }) => void;
patchJellyfinConfig: (session: JellyfinSession) => void;
logInfo: (message: string) => void;
logError: (message: string, error: unknown) => void;
@@ -221,7 +224,7 @@ export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSet
authenticateWithPassword: (server, username, password, clientInfo) =>
deps.authenticateWithPassword(server, username, password, clientInfo),
getJellyfinClientInfo: () => deps.getJellyfinClientInfo(),
saveStoredToken: (token) => deps.saveStoredToken(token),
saveStoredSession: (session) => deps.saveStoredSession(session),
patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session),
logInfo: (message) => deps.logInfo(message),
logError: (message, error) => deps.logError(message, error),

View File

@@ -342,8 +342,6 @@ export interface JellyfinConfig {
enabled?: boolean;
serverUrl?: string;
username?: string;
accessToken?: string;
userId?: string;
deviceId?: string;
clientName?: string;
clientVersion?: string;
@@ -515,8 +513,6 @@ export interface ResolvedConfig {
enabled: boolean;
serverUrl: string;
username: string;
accessToken: string;
userId: string;
deviceId: string;
clientName: string;
clientVersion: string;