From 9d73971f3b65fe0053c66af487a354bba1cae4ff Mon Sep 17 00:00:00 2001 From: sudacode Date: Mon, 23 Feb 2026 23:59:14 -0800 Subject: [PATCH] feat(launcher): pass through password-store for jellyfin flows --- docs/configuration.md | 2 ++ docs/jellyfin-integration.md | 2 ++ docs/usage.md | 1 + launcher/commands/jellyfin-command.ts | 10 +++++++++ launcher/config/args-normalizer.ts | 5 +++++ launcher/config/cli-parser-builder.ts | 3 +++ launcher/jellyfin.ts | 1 + launcher/main.test.ts | 30 +++++++++++++++++++++++++++ launcher/parse-args.test.ts | 11 ++++++++++ launcher/types.ts | 1 + 10 files changed, 66 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 63726e7..b24566a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -508,6 +508,7 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner | `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) | Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted storage after login/setup. +- On Linux, token storage defaults to `gnome-libsecret` for `safeStorage`. Override with `--password-store=` on launcher/app invocations when needed. Launcher subcommands: @@ -516,6 +517,7 @@ Launcher subcommands: - `subminer jellyfin --logout` clears stored credentials. - `subminer jellyfin -p` opens play picker. - `subminer jellyfin -d` starts cast discovery mode. +- These launcher commands also accept `--password-store=` to override the launcher-app forwarded Electron switch. See [Jellyfin Integration](/jellyfin-integration) for the full setup and cast-to-device guide. diff --git a/docs/jellyfin-integration.md b/docs/jellyfin-integration.md index e295020..6073435 100644 --- a/docs/jellyfin-integration.md +++ b/docs/jellyfin-integration.md @@ -12,6 +12,7 @@ SubMiner includes an optional Jellyfin CLI integration for: - Jellyfin server URL and user credentials - For `--jellyfin-play`: connected mpv IPC socket (`--start` or existing mpv plugin workflow) +- On Linux, token encryption defaults to `gnome-libsecret`; pass `--password-store=` to override. ## Setup @@ -150,6 +151,7 @@ User-visible errors are shown through CLI logs and mpv OSD for: ## Security Notes and Limitations - Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted token storage after login/setup. +- Launcher wrappers support `--password-store=` and forward it through to the app process. - Optional environment overrides are supported: `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID`. - Treat both token storage and config files as secrets and avoid committing them. - Password is used only for login and is not stored. diff --git a/docs/usage.md b/docs/usage.md index 894453e..4e1273c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -95,6 +95,7 @@ SubMiner.AppImage --help # Show all options - `--background` launched from a terminal detaches and returns the prompt; stop it with tray Quit or `SubMiner.AppImage --stop`. - Linux desktop launcher starts SubMiner with `--background` by default (via electron-builder `linux.executableArgs`). - On Linux, the app now defaults `safeStorage` to `gnome-libsecret` for encrypted token persistence. + Launcher pass-through commands also support `--password-store=` and forward it to the app when present. Override with e.g. `--password-store=basic_text`. - Use both when needed, for example `SubMiner.AppImage --start --dev --log-level debug`. diff --git a/launcher/commands/jellyfin-command.ts b/launcher/commands/jellyfin-command.ts index d05d22a..4c2a37b 100644 --- a/launcher/commands/jellyfin-command.ts +++ b/launcher/commands/jellyfin-command.ts @@ -10,9 +10,16 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi return false; } + const appendPasswordStore = (forwarded: string[]): void => { + if (args.passwordStore) { + forwarded.push('--password-store', args.passwordStore); + } + }; + if (args.jellyfin) { const forwarded = ['--jellyfin']; if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel); + appendPasswordStore(forwarded); runAppCommandWithInherit(appPath, forwarded); } @@ -35,12 +42,14 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi password, ]; if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel); + appendPasswordStore(forwarded); runAppCommandWithInherit(appPath, forwarded); } if (args.jellyfinLogout) { const forwarded = ['--jellyfin-logout']; if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel); + appendPasswordStore(forwarded); runAppCommandWithInherit(appPath, forwarded); } @@ -58,6 +67,7 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi if (args.jellyfinDiscovery) { const forwarded = ['--start']; if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel); + appendPasswordStore(forwarded); runAppCommandWithInherit(appPath, forwarded); } diff --git a/launcher/config/args-normalizer.ts b/launcher/config/args-normalizer.ts index 3408b81..2e5d5db 100644 --- a/launcher/config/args-normalizer.ts +++ b/launcher/config/args-normalizer.ts @@ -134,6 +134,7 @@ export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig): texthookerOnly: false, useRofi: false, logLevel: 'info', + passwordStore: '', target: '', targetKind: '', }; @@ -161,6 +162,7 @@ export function applyRootOptionsToArgs( if (typeof options.profile === 'string') parsed.profile = options.profile; if (options.start === true) parsed.startOverlay = true; if (typeof options.logLevel === 'string') parsed.logLevel = parseLogLevel(options.logLevel); + if (typeof options.passwordStore === 'string') parsed.passwordStore = options.passwordStore; if (options.rofi === true) parsed.useRofi = true; if (options.startOverlay === true) parsed.autoStartOverlay = true; if (options.texthooker === false) parsed.useTexthooker = false; @@ -175,6 +177,9 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations if (invocations.jellyfinInvocation.logLevel) { parsed.logLevel = parseLogLevel(invocations.jellyfinInvocation.logLevel); } + if (typeof invocations.jellyfinInvocation.passwordStore === 'string') { + parsed.passwordStore = invocations.jellyfinInvocation.passwordStore; + } const action = (invocations.jellyfinInvocation.action || '').toLowerCase(); if (action && !['setup', 'discovery', 'play', 'login', 'logout'].includes(action)) { fail(`Unknown jellyfin action: ${invocations.jellyfinInvocation.action}`); diff --git a/launcher/config/cli-parser-builder.ts b/launcher/config/cli-parser-builder.ts index f486f7a..99a7015 100644 --- a/launcher/config/cli-parser-builder.ts +++ b/launcher/config/cli-parser-builder.ts @@ -10,6 +10,7 @@ export interface JellyfinInvocation { server?: string; username?: string; password?: string; + passwordStore?: string; logLevel?: string; } @@ -168,6 +169,7 @@ export function parseCliPrograms( .option('-s, --server ', 'Jellyfin server URL') .option('-u, --username ', 'Jellyfin username') .option('-w, --password ', 'Jellyfin password') + .option('--password-store ', 'Pass through Electron safeStorage backend') .option('--log-level ', 'Log level') .action((action: string | undefined, options: Record) => { jellyfinInvocation = { @@ -180,6 +182,7 @@ export function parseCliPrograms( server: typeof options.server === 'string' ? options.server : undefined, username: typeof options.username === 'string' ? options.username : undefined, password: typeof options.password === 'string' ? options.password : undefined, + passwordStore: typeof options.passwordStore === 'string' ? options.passwordStore : undefined, logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined, }; }); diff --git a/launcher/jellyfin.ts b/launcher/jellyfin.ts index 61b1711..e1d0a04 100644 --- a/launcher/jellyfin.ts +++ b/launcher/jellyfin.ts @@ -395,5 +395,6 @@ export async function runJellyfinPlayMenu( } const forwarded = ['--start', '--jellyfin-play', '--jellyfin-item-id', itemId]; if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel); + if (args.passwordStore) forwarded.push('--password-store', args.passwordStore); runAppCommandWithInheritLogged(appPath, forwarded, args.logLevel, 'jellyfin-play'); } diff --git a/launcher/main.test.ts b/launcher/main.test.ts index 121c6bb..3cdfec1 100644 --- a/launcher/main.test.ts +++ b/launcher/main.test.ts @@ -207,3 +207,33 @@ test('jellyfin login routes credentials to app command', () => { ); }); }); + +test('jellyfin setup forwards password-store to app command', () => { + withTempDir((root) => { + const homeDir = path.join(root, 'home'); + const xdgConfigHome = path.join(root, 'xdg'); + const appPath = path.join(root, 'fake-subminer.sh'); + const capturePath = path.join(root, 'captured-args.txt'); + fs.writeFileSync( + appPath, + '#!/bin/sh\nif [ -n "$SUBMINER_TEST_CAPTURE" ]; then printf "%s\\n" "$@" > "$SUBMINER_TEST_CAPTURE"; fi\nexit 0\n', + ); + fs.chmodSync(appPath, 0o755); + + const env = { + ...makeTestEnv(homeDir, xdgConfigHome), + SUBMINER_APPIMAGE_PATH: appPath, + SUBMINER_TEST_CAPTURE: capturePath, + }; + const result = runLauncher( + ['jf', 'setup', '--password-store', 'gnome-libsecret'], + env, + ); + + assert.equal(result.status, 0); + assert.equal( + fs.readFileSync(capturePath, 'utf8'), + '--jellyfin\n--password-store\ngnome-libsecret\n', + ); + }); +}); diff --git a/launcher/parse-args.test.ts b/launcher/parse-args.test.ts index 1b921db..e35d8ed 100644 --- a/launcher/parse-args.test.ts +++ b/launcher/parse-args.test.ts @@ -30,6 +30,17 @@ test('parseArgs maps jellyfin play action and log-level override', () => { assert.equal(parsed.logLevel, 'debug'); }); +test('parseArgs forwards jellyfin password-store option', () => { + const parsed = parseArgs( + ['jf', 'setup', '--password-store', 'gnome-libsecret'], + 'subminer', + {}, + ); + + assert.equal(parsed.jellyfin, true); + assert.equal(parsed.passwordStore, 'gnome-libsecret'); +}); + test('parseArgs maps config show action', () => { const parsed = parseArgs(['config', 'show'], 'subminer', {}); diff --git a/launcher/types.ts b/launcher/types.ts index c0d217a..19b94fc 100644 --- a/launcher/types.ts +++ b/launcher/types.ts @@ -79,6 +79,7 @@ export interface Args { texthookerOnly: boolean; useRofi: boolean; logLevel: LogLevel; + passwordStore: string; target: string; targetKind: '' | 'file' | 'url'; jimakuApiKey: string;