mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-22 02:56:24 -07:00
feat(core): add Electron runtime, services, and app composition
This commit is contained in:
140
src/core/services/jellyfin-token-store.ts
Normal file
140
src/core/services/jellyfin-token-store.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { safeStorage } from 'electron';
|
||||
|
||||
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 {
|
||||
loadSession: () => JellyfinStoredSession | null;
|
||||
saveSession: (session: JellyfinStoredSession) => void;
|
||||
clearSession: () => void;
|
||||
}
|
||||
|
||||
function ensureDirectory(filePath: string): void {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function writePayload(filePath: string, payload: PersistedSessionPayload): void {
|
||||
ensureDirectory(filePath);
|
||||
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
export function createJellyfinTokenStore(
|
||||
filePath: string,
|
||||
logger: {
|
||||
info: (message: string) => void;
|
||||
warn: (message: string, details?: unknown) => void;
|
||||
error: (message: string, details?: unknown) => void;
|
||||
},
|
||||
): JellyfinTokenStore {
|
||||
return {
|
||||
loadSession(): JellyfinStoredSession | null {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf-8');
|
||||
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 session encryption is not available on this system.');
|
||||
return null;
|
||||
}
|
||||
const decrypted = safeStorage.decryptString(encrypted).trim();
|
||||
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 (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 session store.', error);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
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 session encryption unavailable; storing session in plaintext fallback.',
|
||||
);
|
||||
writePayload(filePath, {
|
||||
plaintextSession: {
|
||||
accessToken,
|
||||
userId,
|
||||
},
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const encrypted = safeStorage.encryptString(JSON.stringify({ accessToken, userId }));
|
||||
writePayload(filePath, {
|
||||
encryptedSession: encrypted.toString('base64'),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist Jellyfin session.', error);
|
||||
}
|
||||
},
|
||||
|
||||
clearSession(): void {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
logger.info('Cleared stored Jellyfin session.');
|
||||
} catch (error) {
|
||||
logger.error('Failed to clear stored Jellyfin session.', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user