mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
feat(launcher): pass through password-store for jellyfin flows
This commit is contained in:
@@ -508,6 +508,7 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
|
|||||||
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
|
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
|
||||||
|
|
||||||
Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted storage after login/setup.
|
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=<backend>` on launcher/app invocations when needed.
|
||||||
|
|
||||||
Launcher subcommands:
|
Launcher subcommands:
|
||||||
|
|
||||||
@@ -516,6 +517,7 @@ Launcher subcommands:
|
|||||||
- `subminer jellyfin --logout` clears stored credentials.
|
- `subminer jellyfin --logout` clears stored credentials.
|
||||||
- `subminer jellyfin -p` opens play picker.
|
- `subminer jellyfin -p` opens play picker.
|
||||||
- `subminer jellyfin -d` starts cast discovery mode.
|
- `subminer jellyfin -d` starts cast discovery mode.
|
||||||
|
- These launcher commands also accept `--password-store=<backend>` to override the launcher-app forwarded Electron switch.
|
||||||
|
|
||||||
See [Jellyfin Integration](/jellyfin-integration) for the full setup and cast-to-device guide.
|
See [Jellyfin Integration](/jellyfin-integration) for the full setup and cast-to-device guide.
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ SubMiner includes an optional Jellyfin CLI integration for:
|
|||||||
|
|
||||||
- Jellyfin server URL and user credentials
|
- Jellyfin server URL and user credentials
|
||||||
- For `--jellyfin-play`: connected mpv IPC socket (`--start` or existing mpv plugin workflow)
|
- For `--jellyfin-play`: connected mpv IPC socket (`--start` or existing mpv plugin workflow)
|
||||||
|
- On Linux, token encryption defaults to `gnome-libsecret`; pass `--password-store=<backend>` to override.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -150,6 +151,7 @@ User-visible errors are shown through CLI logs and mpv OSD for:
|
|||||||
## Security Notes and Limitations
|
## Security Notes and Limitations
|
||||||
|
|
||||||
- Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted token storage after login/setup.
|
- Jellyfin auth session (`accessToken` + `userId`) is stored in local encrypted token storage after login/setup.
|
||||||
|
- Launcher wrappers support `--password-store=<backend>` and forward it through to the app process.
|
||||||
- Optional environment overrides are supported: `SUBMINER_JELLYFIN_ACCESS_TOKEN` and `SUBMINER_JELLYFIN_USER_ID`.
|
- 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.
|
- Treat both token storage and config files as secrets and avoid committing them.
|
||||||
- Password is used only for login and is not stored.
|
- Password is used only for login and is not stored.
|
||||||
|
|||||||
@@ -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`.
|
- `--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`).
|
- 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.
|
- On Linux, the app now defaults `safeStorage` to `gnome-libsecret` for encrypted token persistence.
|
||||||
|
Launcher pass-through commands also support `--password-store=<backend>` and forward it to the app when present.
|
||||||
Override with e.g. `--password-store=basic_text`.
|
Override with e.g. `--password-store=basic_text`.
|
||||||
- Use both when needed, for example `SubMiner.AppImage --start --dev --log-level debug`.
|
- Use both when needed, for example `SubMiner.AppImage --start --dev --log-level debug`.
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,16 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appendPasswordStore = (forwarded: string[]): void => {
|
||||||
|
if (args.passwordStore) {
|
||||||
|
forwarded.push('--password-store', args.passwordStore);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (args.jellyfin) {
|
if (args.jellyfin) {
|
||||||
const forwarded = ['--jellyfin'];
|
const forwarded = ['--jellyfin'];
|
||||||
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
|
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
|
||||||
|
appendPasswordStore(forwarded);
|
||||||
runAppCommandWithInherit(appPath, forwarded);
|
runAppCommandWithInherit(appPath, forwarded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,12 +42,14 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi
|
|||||||
password,
|
password,
|
||||||
];
|
];
|
||||||
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
|
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
|
||||||
|
appendPasswordStore(forwarded);
|
||||||
runAppCommandWithInherit(appPath, forwarded);
|
runAppCommandWithInherit(appPath, forwarded);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.jellyfinLogout) {
|
if (args.jellyfinLogout) {
|
||||||
const forwarded = ['--jellyfin-logout'];
|
const forwarded = ['--jellyfin-logout'];
|
||||||
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
|
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
|
||||||
|
appendPasswordStore(forwarded);
|
||||||
runAppCommandWithInherit(appPath, forwarded);
|
runAppCommandWithInherit(appPath, forwarded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +67,7 @@ export async function runJellyfinCommand(context: LauncherCommandContext): Promi
|
|||||||
if (args.jellyfinDiscovery) {
|
if (args.jellyfinDiscovery) {
|
||||||
const forwarded = ['--start'];
|
const forwarded = ['--start'];
|
||||||
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
|
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
|
||||||
|
appendPasswordStore(forwarded);
|
||||||
runAppCommandWithInherit(appPath, forwarded);
|
runAppCommandWithInherit(appPath, forwarded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export function createDefaultArgs(launcherConfig: LauncherYoutubeSubgenConfig):
|
|||||||
texthookerOnly: false,
|
texthookerOnly: false,
|
||||||
useRofi: false,
|
useRofi: false,
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
|
passwordStore: '',
|
||||||
target: '',
|
target: '',
|
||||||
targetKind: '',
|
targetKind: '',
|
||||||
};
|
};
|
||||||
@@ -161,6 +162,7 @@ export function applyRootOptionsToArgs(
|
|||||||
if (typeof options.profile === 'string') parsed.profile = options.profile;
|
if (typeof options.profile === 'string') parsed.profile = options.profile;
|
||||||
if (options.start === true) parsed.startOverlay = true;
|
if (options.start === true) parsed.startOverlay = true;
|
||||||
if (typeof options.logLevel === 'string') parsed.logLevel = parseLogLevel(options.logLevel);
|
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.rofi === true) parsed.useRofi = true;
|
||||||
if (options.startOverlay === true) parsed.autoStartOverlay = true;
|
if (options.startOverlay === true) parsed.autoStartOverlay = true;
|
||||||
if (options.texthooker === false) parsed.useTexthooker = false;
|
if (options.texthooker === false) parsed.useTexthooker = false;
|
||||||
@@ -175,6 +177,9 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
|
|||||||
if (invocations.jellyfinInvocation.logLevel) {
|
if (invocations.jellyfinInvocation.logLevel) {
|
||||||
parsed.logLevel = parseLogLevel(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();
|
const action = (invocations.jellyfinInvocation.action || '').toLowerCase();
|
||||||
if (action && !['setup', 'discovery', 'play', 'login', 'logout'].includes(action)) {
|
if (action && !['setup', 'discovery', 'play', 'login', 'logout'].includes(action)) {
|
||||||
fail(`Unknown jellyfin action: ${invocations.jellyfinInvocation.action}`);
|
fail(`Unknown jellyfin action: ${invocations.jellyfinInvocation.action}`);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface JellyfinInvocation {
|
|||||||
server?: string;
|
server?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
passwordStore?: string;
|
||||||
logLevel?: string;
|
logLevel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ export function parseCliPrograms(
|
|||||||
.option('-s, --server <url>', 'Jellyfin server URL')
|
.option('-s, --server <url>', 'Jellyfin server URL')
|
||||||
.option('-u, --username <name>', 'Jellyfin username')
|
.option('-u, --username <name>', 'Jellyfin username')
|
||||||
.option('-w, --password <pass>', 'Jellyfin password')
|
.option('-w, --password <pass>', 'Jellyfin password')
|
||||||
|
.option('--password-store <backend>', 'Pass through Electron safeStorage backend')
|
||||||
.option('--log-level <level>', 'Log level')
|
.option('--log-level <level>', 'Log level')
|
||||||
.action((action: string | undefined, options: Record<string, unknown>) => {
|
.action((action: string | undefined, options: Record<string, unknown>) => {
|
||||||
jellyfinInvocation = {
|
jellyfinInvocation = {
|
||||||
@@ -180,6 +182,7 @@ export function parseCliPrograms(
|
|||||||
server: typeof options.server === 'string' ? options.server : undefined,
|
server: typeof options.server === 'string' ? options.server : undefined,
|
||||||
username: typeof options.username === 'string' ? options.username : undefined,
|
username: typeof options.username === 'string' ? options.username : undefined,
|
||||||
password: typeof options.password === 'string' ? options.password : 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,
|
logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -395,5 +395,6 @@ export async function runJellyfinPlayMenu(
|
|||||||
}
|
}
|
||||||
const forwarded = ['--start', '--jellyfin-play', '--jellyfin-item-id', itemId];
|
const forwarded = ['--start', '--jellyfin-play', '--jellyfin-item-id', itemId];
|
||||||
if (args.logLevel !== 'info') forwarded.push('--log-level', args.logLevel);
|
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');
|
runAppCommandWithInheritLogged(appPath, forwarded, args.logLevel, 'jellyfin-play');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -30,6 +30,17 @@ test('parseArgs maps jellyfin play action and log-level override', () => {
|
|||||||
assert.equal(parsed.logLevel, 'debug');
|
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', () => {
|
test('parseArgs maps config show action', () => {
|
||||||
const parsed = parseArgs(['config', 'show'], 'subminer', {});
|
const parsed = parseArgs(['config', 'show'], 'subminer', {});
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export interface Args {
|
|||||||
texthookerOnly: boolean;
|
texthookerOnly: boolean;
|
||||||
useRofi: boolean;
|
useRofi: boolean;
|
||||||
logLevel: LogLevel;
|
logLevel: LogLevel;
|
||||||
|
passwordStore: string;
|
||||||
target: string;
|
target: string;
|
||||||
targetKind: '' | 'file' | 'url';
|
targetKind: '' | 'file' | 'url';
|
||||||
jimakuApiKey: string;
|
jimakuApiKey: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user