mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
feat(jellyfin): store access token in encrypted local store
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
id: TASK-92
|
||||||
|
title: Store Jellyfin token in encrypted local token store like AniList
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-20 02:22'
|
||||||
|
updated_date: '2026-02-20 02:22'
|
||||||
|
labels: []
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Persist Jellyfin access token in local encrypted token storage and resolve `jellyfin.accessToken` from config-or-store, matching AniList token handling pattern.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [ ] #1 Jellyfin login/setup saves token to encrypted local store instead of persisting in config.
|
||||||
|
- [ ] #2 Jellyfin logout clears local stored token.
|
||||||
|
- [ ] #3 Runtime resolves Jellyfin token from config override first, then stored token fallback when config value is blank.
|
||||||
|
- [ ] #4 Jellyfin runtime tests cover new token-store save/clear/fallback behavior.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Added `src/core/services/jellyfin-token-store.ts` using Electron `safeStorage` with encrypted payload + plaintext fallback/migration behavior matching AniList store pattern. Wired token store in `src/main.ts` and changed Jellyfin flows so login/setup save token in store and write blank `jellyfin.accessToken` to config, while logout clears store. Updated resolved Jellyfin config path (`src/main/runtime/jellyfin-client-info.ts`) to apply config-first + stored-token fallback. Updated runtime deps/tests: `jellyfin-client-info*`, `jellyfin-cli-auth*`, `jellyfin-cli-main-deps*`, `jellyfin-setup-window*`, and `jellyfin-setup-window-main-deps*`. Updated docs/config notes to reflect local encrypted token storage behavior.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
@@ -496,7 +496,7 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
|
|||||||
| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
|
| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) |
|
||||||
| `serverUrl` | string (URL) | Jellyfin server base URL |
|
| `serverUrl` | string (URL) | Jellyfin server base URL |
|
||||||
| `username` | string | Default username used by `--jellyfin-login` |
|
| `username` | string | Default username used by `--jellyfin-login` |
|
||||||
| `accessToken` | string | Stored Jellyfin access token (treat as secret) |
|
| `accessToken` | string | Optional explicit Jellyfin access token override; leave empty to use stored local token |
|
||||||
| `userId` | string | Jellyfin user id bound to token/session |
|
| `userId` | string | Jellyfin user id bound to token/session |
|
||||||
| `deviceId` | string | Client device id sent in auth headers (default: `subminer`) |
|
| `deviceId` | string | Client device id sent in auth headers (default: `subminer`) |
|
||||||
| `clientName` | string | Client name sent in auth headers (default: `SubMiner`) |
|
| `clientName` | string | Client name sent in auth headers (default: `SubMiner`) |
|
||||||
@@ -512,6 +512,8 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
|
|||||||
| `directPlayContainers` | string[] | Container allowlist for direct play decisions |
|
| `directPlayContainers` | string[] | Container allowlist for direct play decisions |
|
||||||
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
|
| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) |
|
||||||
|
|
||||||
|
When `jellyfin.accessToken` is empty, SubMiner uses the locally stored encrypted token saved from Jellyfin login/setup.
|
||||||
|
|
||||||
Jellyfin direct app CLI commands (`SubMiner.AppImage ...`):
|
Jellyfin direct app CLI commands (`SubMiner.AppImage ...`):
|
||||||
|
|
||||||
- `--jellyfin`: open the in-app Jellyfin setup window (server/user/password form).
|
- `--jellyfin`: open the in-app Jellyfin setup window (server/user/password form).
|
||||||
|
|||||||
@@ -149,8 +149,9 @@ User-visible errors are shown through CLI logs and mpv OSD for:
|
|||||||
|
|
||||||
## Security Notes and Limitations
|
## Security Notes and Limitations
|
||||||
|
|
||||||
- Jellyfin access token is persisted in `config.jsonc`.
|
- Jellyfin access token is stored in local encrypted token storage after login/setup.
|
||||||
- Treat config files as secrets and avoid committing them.
|
- `jellyfin.accessToken` remains as an optional explicit override in `config.jsonc`.
|
||||||
|
- 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.
|
||||||
- Optional setup UI is available via `--jellyfin`; all actions are also available via CLI flags.
|
- Optional setup UI is available via `--jellyfin`; all actions are also available via CLI flags.
|
||||||
- `subminer` wrapper uses Jellyfin subcommands (`subminer jellyfin ...`, alias `subminer jf ...`). Use `SubMiner.AppImage` for direct `--jellyfin-libraries` and `--jellyfin-items`.
|
- `subminer` wrapper uses Jellyfin subcommands (`subminer jellyfin ...`, alias `subminer jf ...`). Use `SubMiner.AppImage` for direct `--jellyfin-libraries` and `--jellyfin-items`.
|
||||||
|
|||||||
@@ -273,13 +273,14 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// Jellyfin
|
// Jellyfin
|
||||||
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||||
// Access token is stored in config and should be treated as a secret.
|
// Access token is stored in local encrypted token storage after login/setup.
|
||||||
|
// jellyfin.accessToken below remains an optional explicit override.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"enabled": false, // Enable optional Jellyfin integration and CLI control commands. Values: true | false
|
"enabled": false, // Enable optional Jellyfin integration and CLI control commands. Values: true | false
|
||||||
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
|
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
|
||||||
"username": "", // Default Jellyfin username used during CLI login.
|
"username": "", // Default Jellyfin username used during CLI login.
|
||||||
"accessToken": "", // Access token setting.
|
"accessToken": "", // Optional explicit access token override; leave empty to use stored local token.
|
||||||
"userId": "", // User id setting.
|
"userId": "", // User id setting.
|
||||||
"deviceId": "subminer", // Device id setting.
|
"deviceId": "subminer", // Device id setting.
|
||||||
"clientName": "SubMiner", // Client name setting.
|
"clientName": "SubMiner", // Client name setting.
|
||||||
|
|||||||
@@ -18,3 +18,6 @@ Read first. Keep concise.
|
|||||||
| `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` |
|
| `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` |
|
||||||
| `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` |
|
| `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` |
|
||||||
| `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` |
|
| `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` |
|
||||||
|
| `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Verify whether Jellyfin token can use same secret-store path as AniList token` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-20T10:22:45Z` |
|
||||||
|
| `codex-vitepress-subagents-ignore-20260220T101755Z-k2m9` | `codex-vitepress-subagents-ignore` | `Exclude docs/subagents from VitePress build` | `completed` | `docs/subagents/agents/codex-vitepress-subagents-ignore-20260220T101755Z-k2m9.md` | `2026-02-20T10:18:30Z` |
|
||||||
|
| `codex-preserve-linebreak-display-20260220T110436Z-r8f1` | `codex-preserve-linebreak-display` | `Fix visible overlay display artifact when subtitleStyle.preserveLineBreaks is disabled` | `completed` | `docs/subagents/agents/codex-preserve-linebreak-display-20260220T110436Z-r8f1.md` | `2026-02-20T11:10:51Z` |
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# codex-jellyfin-secret-store-20260220T101428Z-om4z
|
||||||
|
|
||||||
|
- alias: `codex-jellyfin-secret-store`
|
||||||
|
- mission: `Verify whether Jellyfin token can use same secret-store path as AniList token`
|
||||||
|
- status: `completed`
|
||||||
|
- last_update_utc: `2026-02-20T10:22:45Z`
|
||||||
|
|
||||||
|
## Intent
|
||||||
|
|
||||||
|
- compare AniList token persistence path vs Jellyfin token persistence path
|
||||||
|
- answer feasibility + current behavior from code
|
||||||
|
|
||||||
|
## Planned Files
|
||||||
|
|
||||||
|
- `src/main/runtime/anilist-token-refresh.ts`
|
||||||
|
- `src/main/runtime/*anilist*`
|
||||||
|
- `src/core/services/jellyfin.ts`
|
||||||
|
- `src/main/runtime/*jellyfin*`
|
||||||
|
- `src/config/*`
|
||||||
|
- docs refs if needed
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- user asking architecture/feasibility question; likely no code change requested yet
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
- AniList token store present: `src/core/services/anilist/anilist-token-store.ts` (`safeStorage` encrypt/decrypt + persisted file)
|
||||||
|
- AniList runtime wiring present: `src/main.ts` creates `anilistTokenStore`; refresh/setup paths use `saveToken/loadToken`
|
||||||
|
- Jellyfin auth currently writes token directly into config via `patchRawConfig({ jellyfin: { accessToken } })`
|
||||||
|
- Docs confirm current behavior: Jellyfin token persisted in config (`docs/jellyfin-integration.md`)
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- no code changes; answered feasibility/current state from repo
|
||||||
|
- implemented requested change: Jellyfin token now persisted in local encrypted token store with config override fallback
|
||||||
|
|
||||||
|
## Files Touched
|
||||||
|
|
||||||
|
- `src/core/services/jellyfin-token-store.ts`
|
||||||
|
- `src/main.ts`
|
||||||
|
- `src/main/runtime/jellyfin-client-info.ts`
|
||||||
|
- `src/main/runtime/jellyfin-client-info.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-client-info-main-deps.ts`
|
||||||
|
- `src/main/runtime/jellyfin-client-info-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-cli-auth.ts`
|
||||||
|
- `src/main/runtime/jellyfin-cli-auth.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-cli-main-deps.ts`
|
||||||
|
- `src/main/runtime/jellyfin-cli-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-setup-window.ts`
|
||||||
|
- `src/main/runtime/jellyfin-setup-window.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-setup-window-main-deps.ts`
|
||||||
|
- `src/main/runtime/jellyfin-setup-window-main-deps.test.ts`
|
||||||
|
- `docs/jellyfin-integration.md`
|
||||||
|
- `docs/configuration.md`
|
||||||
|
- `docs/public/config.example.jsonc`
|
||||||
|
- `src/config/definitions.ts`
|
||||||
|
- `backlog/tasks/task-92 - Store-Jellyfin-token-in-encrypted-local-token-store-like-AniList.md`
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `bun run build`
|
||||||
|
- `node --test dist/main/runtime/jellyfin-client-info.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-client-info-main-deps.test.js dist/main/runtime/jellyfin-cli-main-deps.test.js dist/main/runtime/jellyfin-setup-window-main-deps.test.js`
|
||||||
|
- `bun run test:fast`
|
||||||
|
- `bun run docs:build`
|
||||||
@@ -12,3 +12,7 @@ Shared notes. Append-only.
|
|||||||
- [2026-02-20T06:35:38Z] [codex-preserve-linebreaks-20260220T063538Z-s4nd|codex-preserve-linebreaks] overlap note: touching subtitle config + renderer render path (`src/types.ts`, `src/config/*`, `src/renderer/subtitle-render.ts`, docs/config examples) to add optional preserve-line-breaks behavior while keeping default normalization unchanged.
|
- [2026-02-20T06:35:38Z] [codex-preserve-linebreaks-20260220T063538Z-s4nd|codex-preserve-linebreaks] overlap note: touching subtitle config + renderer render path (`src/types.ts`, `src/config/*`, `src/renderer/subtitle-render.ts`, docs/config examples) to add optional preserve-line-breaks behavior while keeping default normalization unchanged.
|
||||||
- [2026-02-20T06:42:51Z] [codex-preserve-linebreaks-20260220T063538Z-s4nd|codex-preserve-linebreaks] completed TASK-91; added `subtitleStyle.preserveLineBreaks` config (default false), renderer token/source alignment helper to preserve visible overlay line breaks when enabled, config+renderer tests green.
|
- [2026-02-20T06:42:51Z] [codex-preserve-linebreaks-20260220T063538Z-s4nd|codex-preserve-linebreaks] completed TASK-91; added `subtitleStyle.preserveLineBreaks` config (default false), renderer token/source alignment helper to preserve visible overlay line breaks when enabled, config+renderer tests green.
|
||||||
- [2026-02-20T09:24:08Z] [codex-bundle-config-example-20260220T092408Z-a1b2|codex-bundle-config-example] conflict note: target file .github/workflows/release.yml already modified by codex-release-mpv-plugin session; applying minimal additive delta for config example bundling only.
|
- [2026-02-20T09:24:08Z] [codex-bundle-config-example-20260220T092408Z-a1b2|codex-bundle-config-example] conflict note: target file .github/workflows/release.yml already modified by codex-release-mpv-plugin session; applying minimal additive delta for config example bundling only.
|
||||||
|
- [2026-02-20T10:22:45Z] [codex-jellyfin-secret-store-20260220T101428Z-om4z|codex-jellyfin-secret-store] completed TASK-92: Jellyfin token now stored via local encrypted token store (`safeStorage`) with config override fallback; login/setup save token to store, logout clears store; runtime/docs/tests updated.
|
||||||
|
- [2026-02-20T11:04:36Z] [codex-preserve-linebreak-display-20260220T110436Z-r8f1|codex-preserve-linebreak-display] overlap note: touching `src/renderer/subtitle-render.ts` + renderer tests to fix preserve-linebreaks disabled display artifact while preserving TASK-91 behavior.
|
||||||
|
- [2026-02-20T11:07:29Z] [codex-preserve-linebreak-display-20260220T110436Z-r8f1|codex-preserve-linebreak-display] completed follow-up for TASK-91: non-preserve mode now flattens token CR/LF to spaces instead of emitting `<br>` from token surfaces; regression test added.
|
||||||
|
- [2026-02-20T11:10:51Z] [codex-preserve-linebreak-display-20260220T110436Z-r8f1|codex-preserve-linebreak-display] second follow-up: handle overlap token streams by aligning non-preserve rendering to normalized source text and skipping unmatched tail tokens (prevents duplicated second-line phrase).
|
||||||
|
|||||||
@@ -807,7 +807,8 @@ export const CONFIG_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
|||||||
title: 'Jellyfin',
|
title: 'Jellyfin',
|
||||||
description: [
|
description: [
|
||||||
'Optional Jellyfin integration for auth, browsing, and playback launch.',
|
'Optional Jellyfin integration for auth, browsing, and playback launch.',
|
||||||
'Access token is stored in config and should be treated as a secret.',
|
'Access token is stored in local encrypted token storage after login/setup.',
|
||||||
|
'jellyfin.accessToken remains an optional explicit override in config.',
|
||||||
],
|
],
|
||||||
key: 'jellyfin',
|
key: 'jellyfin',
|
||||||
},
|
},
|
||||||
|
|||||||
100
src/core/services/jellyfin-token-store.ts
Normal file
100
src/core/services/jellyfin-token-store.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { safeStorage } from 'electron';
|
||||||
|
|
||||||
|
interface PersistedTokenPayload {
|
||||||
|
encryptedToken?: string;
|
||||||
|
plaintextToken?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JellyfinTokenStore {
|
||||||
|
loadToken: () => string | null;
|
||||||
|
saveToken: (token: string) => void;
|
||||||
|
clearToken: () => 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: PersistedTokenPayload): 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 {
|
||||||
|
loadToken(): string | 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');
|
||||||
|
if (!safeStorage.isEncryptionAvailable()) {
|
||||||
|
logger.warn('Jellyfin token encryption is not available on this system.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const decrypted = safeStorage.decryptString(encrypted).trim();
|
||||||
|
return decrypted.length > 0 ? decrypted : null;
|
||||||
|
}
|
||||||
|
if (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) {
|
||||||
|
const plaintext = parsed.plaintextToken.trim();
|
||||||
|
this.saveToken(plaintext);
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to read Jellyfin token store.', error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveToken(token: string): void {
|
||||||
|
const trimmed = token.trim();
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
this.clearToken();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!safeStorage.isEncryptionAvailable()) {
|
||||||
|
logger.warn('Jellyfin token encryption unavailable; storing token in plaintext fallback.');
|
||||||
|
writePayload(filePath, {
|
||||||
|
plaintextToken: trimmed,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const encrypted = safeStorage.encryptString(trimmed);
|
||||||
|
writePayload(filePath, {
|
||||||
|
encryptedToken: encrypted.toString('base64'),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to persist Jellyfin token.', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearToken(): void {
|
||||||
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
logger.info('Cleared stored Jellyfin token.');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to clear stored Jellyfin token.', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
16
src/main.ts
16
src/main.ts
@@ -530,6 +530,7 @@ import {
|
|||||||
} from './core/services/anilist/anilist-updater';
|
} from './core/services/anilist/anilist-updater';
|
||||||
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
||||||
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
||||||
|
import { createJellyfinTokenStore } from './core/services/jellyfin-token-store';
|
||||||
import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc';
|
import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc';
|
||||||
import { createAppReadyRuntimeRunner } from './main/app-lifecycle';
|
import { createAppReadyRuntimeRunner } from './main/app-lifecycle';
|
||||||
import { handleMpvCommandFromIpcRuntime } from './main/ipc-mpv-command';
|
import { handleMpvCommandFromIpcRuntime } from './main/ipc-mpv-command';
|
||||||
@@ -581,6 +582,7 @@ const ANILIST_UPDATE_MIN_WATCH_SECONDS = 10 * 60;
|
|||||||
const ANILIST_DURATION_RETRY_INTERVAL_MS = 15_000;
|
const ANILIST_DURATION_RETRY_INTERVAL_MS = 15_000;
|
||||||
const ANILIST_MAX_ATTEMPTED_UPDATE_KEYS = 1000;
|
const ANILIST_MAX_ATTEMPTED_UPDATE_KEYS = 1000;
|
||||||
const ANILIST_TOKEN_STORE_FILE = 'anilist-token-store.json';
|
const ANILIST_TOKEN_STORE_FILE = 'anilist-token-store.json';
|
||||||
|
const JELLYFIN_TOKEN_STORE_FILE = 'jellyfin-token-store.json';
|
||||||
const ANILIST_RETRY_QUEUE_FILE = 'anilist-retry-queue.json';
|
const ANILIST_RETRY_QUEUE_FILE = 'anilist-retry-queue.json';
|
||||||
const TRAY_TOOLTIP = 'SubMiner';
|
const TRAY_TOOLTIP = 'SubMiner';
|
||||||
|
|
||||||
@@ -644,6 +646,14 @@ const anilistTokenStore = createAnilistTokenStore(
|
|||||||
error: (message: string, details?: unknown) => console.error(message, details),
|
error: (message: string, details?: unknown) => console.error(message, details),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const jellyfinTokenStore = createJellyfinTokenStore(
|
||||||
|
path.join(USER_DATA_PATH, JELLYFIN_TOKEN_STORE_FILE),
|
||||||
|
{
|
||||||
|
info: (message: string) => console.info(message),
|
||||||
|
warn: (message: string, details?: unknown) => console.warn(message, details),
|
||||||
|
error: (message: string, details?: unknown) => console.error(message, details),
|
||||||
|
},
|
||||||
|
);
|
||||||
const anilistUpdateQueue = createAnilistUpdateQueue(
|
const anilistUpdateQueue = createAnilistUpdateQueue(
|
||||||
path.join(USER_DATA_PATH, ANILIST_RETRY_QUEUE_FILE),
|
path.join(USER_DATA_PATH, ANILIST_RETRY_QUEUE_FILE),
|
||||||
{
|
{
|
||||||
@@ -1205,6 +1215,7 @@ function getResolvedConfig() {
|
|||||||
const buildGetResolvedJellyfinConfigMainDepsHandler =
|
const buildGetResolvedJellyfinConfigMainDepsHandler =
|
||||||
createBuildGetResolvedJellyfinConfigMainDepsHandler({
|
createBuildGetResolvedJellyfinConfigMainDepsHandler({
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
|
loadStoredToken: () => jellyfinTokenStore.loadToken(),
|
||||||
});
|
});
|
||||||
const getResolvedJellyfinConfigHandler = createGetResolvedJellyfinConfigHandler(
|
const getResolvedJellyfinConfigHandler = createGetResolvedJellyfinConfigHandler(
|
||||||
buildGetResolvedJellyfinConfigMainDepsHandler(),
|
buildGetResolvedJellyfinConfigMainDepsHandler(),
|
||||||
@@ -1345,6 +1356,8 @@ const buildHandleJellyfinAuthCommandsMainDepsHandler =
|
|||||||
},
|
},
|
||||||
authenticateWithPassword: (serverUrl, username, password, clientInfo) =>
|
authenticateWithPassword: (serverUrl, username, password, clientInfo) =>
|
||||||
authenticateWithPasswordRuntime(serverUrl, username, password, clientInfo),
|
authenticateWithPasswordRuntime(serverUrl, username, password, clientInfo),
|
||||||
|
saveStoredToken: (token) => jellyfinTokenStore.saveToken(token),
|
||||||
|
clearStoredToken: () => jellyfinTokenStore.clearToken(),
|
||||||
logInfo: (message) => logger.info(message),
|
logInfo: (message) => logger.info(message),
|
||||||
});
|
});
|
||||||
const handleJellyfinAuthCommands = createHandleJellyfinAuthCommands(
|
const handleJellyfinAuthCommands = createHandleJellyfinAuthCommands(
|
||||||
@@ -1585,13 +1598,14 @@ const buildOpenJellyfinSetupWindowMainDepsHandler =
|
|||||||
authenticateWithPassword: (server, username, password, clientInfo) =>
|
authenticateWithPassword: (server, username, password, clientInfo) =>
|
||||||
authenticateWithPasswordRuntime(server, username, password, clientInfo),
|
authenticateWithPasswordRuntime(server, username, password, clientInfo),
|
||||||
getJellyfinClientInfo: () => getJellyfinClientInfo(),
|
getJellyfinClientInfo: () => getJellyfinClientInfo(),
|
||||||
|
saveStoredToken: (token) => jellyfinTokenStore.saveToken(token),
|
||||||
patchJellyfinConfig: (session) => {
|
patchJellyfinConfig: (session) => {
|
||||||
configService.patchRawConfig({
|
configService.patchRawConfig({
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
serverUrl: session.serverUrl,
|
serverUrl: session.serverUrl,
|
||||||
username: session.username,
|
username: session.username,
|
||||||
accessToken: session.accessToken,
|
accessToken: '',
|
||||||
userId: session.userId,
|
userId: session.userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ test('jellyfin auth handler processes logout', async () => {
|
|||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const handleAuth = createHandleJellyfinAuthCommands({
|
const handleAuth = createHandleJellyfinAuthCommands({
|
||||||
patchRawConfig: () => calls.push('patch'),
|
patchRawConfig: () => calls.push('patch'),
|
||||||
|
saveStoredToken: () => calls.push('save'),
|
||||||
|
clearStoredToken: () => calls.push('clear'),
|
||||||
authenticateWithPassword: async () => {
|
authenticateWithPassword: async () => {
|
||||||
throw new Error('should not authenticate');
|
throw new Error('should not authenticate');
|
||||||
},
|
},
|
||||||
@@ -34,13 +36,15 @@ test('jellyfin auth handler processes logout', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(handled, true);
|
assert.equal(handled, true);
|
||||||
assert.equal(calls[0], 'patch');
|
assert.deepEqual(calls.slice(0, 2), ['clear', 'patch']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('jellyfin auth handler processes login', async () => {
|
test('jellyfin auth handler processes login', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const handleAuth = createHandleJellyfinAuthCommands({
|
const handleAuth = createHandleJellyfinAuthCommands({
|
||||||
patchRawConfig: () => calls.push('patch'),
|
patchRawConfig: () => calls.push('patch'),
|
||||||
|
saveStoredToken: () => calls.push('save'),
|
||||||
|
clearStoredToken: () => calls.push('clear'),
|
||||||
authenticateWithPassword: async () => ({
|
authenticateWithPassword: async () => ({
|
||||||
serverUrl: 'http://localhost',
|
serverUrl: 'http://localhost',
|
||||||
username: 'user',
|
username: 'user',
|
||||||
@@ -72,6 +76,7 @@ test('jellyfin auth handler processes login', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(handled, true);
|
assert.equal(handled, true);
|
||||||
|
assert.ok(calls.includes('save'));
|
||||||
assert.ok(calls.includes('patch'));
|
assert.ok(calls.includes('patch'));
|
||||||
assert.ok(calls.some((entry) => entry.includes('Jellyfin login succeeded')));
|
assert.ok(calls.some((entry) => entry.includes('Jellyfin login succeeded')));
|
||||||
});
|
});
|
||||||
@@ -79,6 +84,8 @@ test('jellyfin auth handler processes login', async () => {
|
|||||||
test('jellyfin auth handler no-ops when no auth command', async () => {
|
test('jellyfin auth handler no-ops when no auth command', async () => {
|
||||||
const handleAuth = createHandleJellyfinAuthCommands({
|
const handleAuth = createHandleJellyfinAuthCommands({
|
||||||
patchRawConfig: () => {},
|
patchRawConfig: () => {},
|
||||||
|
saveStoredToken: () => {},
|
||||||
|
clearStoredToken: () => {},
|
||||||
authenticateWithPassword: async () => ({
|
authenticateWithPassword: async () => ({
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
username: '',
|
username: '',
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export function createHandleJellyfinAuthCommands(deps: {
|
|||||||
password: string,
|
password: string,
|
||||||
clientInfo: JellyfinClientInfo,
|
clientInfo: JellyfinClientInfo,
|
||||||
) => Promise<JellyfinSession>;
|
) => Promise<JellyfinSession>;
|
||||||
|
saveStoredToken: (token: string) => void;
|
||||||
|
clearStoredToken: () => void;
|
||||||
logInfo: (message: string) => void;
|
logInfo: (message: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return async (params: {
|
return async (params: {
|
||||||
@@ -48,6 +50,7 @@ export function createHandleJellyfinAuthCommands(deps: {
|
|||||||
clientInfo: JellyfinClientInfo;
|
clientInfo: JellyfinClientInfo;
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
if (params.args.jellyfinLogout) {
|
if (params.args.jellyfinLogout) {
|
||||||
|
deps.clearStoredToken();
|
||||||
deps.patchRawConfig({
|
deps.patchRawConfig({
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
@@ -70,12 +73,13 @@ export function createHandleJellyfinAuthCommands(deps: {
|
|||||||
password,
|
password,
|
||||||
params.clientInfo,
|
params.clientInfo,
|
||||||
);
|
);
|
||||||
|
deps.saveStoredToken(session.accessToken);
|
||||||
deps.patchRawConfig({
|
deps.patchRawConfig({
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
serverUrl: session.serverUrl,
|
serverUrl: session.serverUrl,
|
||||||
username: session.username,
|
username: session.username,
|
||||||
accessToken: session.accessToken,
|
accessToken: '',
|
||||||
userId: session.userId,
|
userId: session.userId,
|
||||||
deviceId: params.clientInfo.deviceId,
|
deviceId: params.clientInfo.deviceId,
|
||||||
clientName: params.clientInfo.clientName,
|
clientName: params.clientInfo.clientName,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ test('jellyfin auth commands main deps builder maps callbacks', async () => {
|
|||||||
const deps = createBuildHandleJellyfinAuthCommandsMainDepsHandler({
|
const deps = createBuildHandleJellyfinAuthCommandsMainDepsHandler({
|
||||||
patchRawConfig: () => calls.push('patch'),
|
patchRawConfig: () => calls.push('patch'),
|
||||||
authenticateWithPassword: async () => ({}) as never,
|
authenticateWithPassword: async () => ({}) as never,
|
||||||
|
saveStoredToken: () => calls.push('save'),
|
||||||
|
clearStoredToken: () => calls.push('clear'),
|
||||||
logInfo: (message) => calls.push(`info:${message}`),
|
logInfo: (message) => calls.push(`info:${message}`),
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -21,8 +23,10 @@ test('jellyfin auth commands main deps builder maps callbacks', async () => {
|
|||||||
clientName: '',
|
clientName: '',
|
||||||
clientVersion: '',
|
clientVersion: '',
|
||||||
});
|
});
|
||||||
|
deps.saveStoredToken('token');
|
||||||
|
deps.clearStoredToken();
|
||||||
deps.logInfo('ok');
|
deps.logInfo('ok');
|
||||||
assert.deepEqual(calls, ['patch', 'info:ok']);
|
assert.deepEqual(calls, ['patch', 'save', 'clear', 'info:ok']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('jellyfin list commands main deps builder maps callbacks', async () => {
|
test('jellyfin list commands main deps builder maps callbacks', async () => {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export function createBuildHandleJellyfinAuthCommandsMainDepsHandler(
|
|||||||
patchRawConfig: (patch) => deps.patchRawConfig(patch),
|
patchRawConfig: (patch) => deps.patchRawConfig(patch),
|
||||||
authenticateWithPassword: (serverUrl, username, password, clientInfo) =>
|
authenticateWithPassword: (serverUrl, username, password, clientInfo) =>
|
||||||
deps.authenticateWithPassword(serverUrl, username, password, clientInfo),
|
deps.authenticateWithPassword(serverUrl, username, password, clientInfo),
|
||||||
|
saveStoredToken: (token) => deps.saveStoredToken(token),
|
||||||
|
clearStoredToken: () => deps.clearStoredToken(),
|
||||||
logInfo: (message: string) => deps.logInfo(message),
|
logInfo: (message: string) => deps.logInfo(message),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ test('get resolved jellyfin config main deps builder maps callbacks', () => {
|
|||||||
const resolved = { jellyfin: { url: 'https://example.com' } };
|
const resolved = { jellyfin: { url: 'https://example.com' } };
|
||||||
const deps = createBuildGetResolvedJellyfinConfigMainDepsHandler({
|
const deps = createBuildGetResolvedJellyfinConfigMainDepsHandler({
|
||||||
getResolvedConfig: () => resolved as never,
|
getResolvedConfig: () => resolved as never,
|
||||||
|
loadStoredToken: () => 'stored-token',
|
||||||
})();
|
})();
|
||||||
assert.equal(deps.getResolvedConfig(), resolved);
|
assert.equal(deps.getResolvedConfig(), resolved);
|
||||||
|
assert.equal(deps.loadStoredToken(), 'stored-token');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('get jellyfin client info main deps builder maps callbacks', () => {
|
test('get jellyfin client info main deps builder maps callbacks', () => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export function createBuildGetResolvedJellyfinConfigMainDepsHandler(
|
|||||||
) {
|
) {
|
||||||
return (): GetResolvedJellyfinConfigMainDeps => ({
|
return (): GetResolvedJellyfinConfigMainDeps => ({
|
||||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||||
|
loadStoredToken: () => deps.loadStoredToken(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,32 @@ test('get resolved jellyfin config returns jellyfin section from resolved config
|
|||||||
const jellyfin = { url: 'https://jellyfin.local' } as never;
|
const jellyfin = { url: 'https://jellyfin.local' } as never;
|
||||||
const getConfig = createGetResolvedJellyfinConfigHandler({
|
const getConfig = createGetResolvedJellyfinConfigHandler({
|
||||||
getResolvedConfig: () => ({ jellyfin } as never),
|
getResolvedConfig: () => ({ jellyfin } as never),
|
||||||
|
loadStoredToken: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(getConfig(), jellyfin);
|
assert.equal(getConfig(), jellyfin);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('get resolved jellyfin config falls back to stored token when config token is blank', () => {
|
||||||
|
const getConfig = createGetResolvedJellyfinConfigHandler({
|
||||||
|
getResolvedConfig: () =>
|
||||||
|
({
|
||||||
|
jellyfin: {
|
||||||
|
serverUrl: 'http://localhost:8096',
|
||||||
|
accessToken: ' ',
|
||||||
|
userId: 'uid-1',
|
||||||
|
},
|
||||||
|
}) as never,
|
||||||
|
loadStoredToken: () => 'stored-token',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(getConfig(), {
|
||||||
|
serverUrl: 'http://localhost:8096',
|
||||||
|
accessToken: 'stored-token',
|
||||||
|
userId: 'uid-1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('jellyfin client info resolves defaults when fields are missing', () => {
|
test('jellyfin client info resolves defaults when fields are missing', () => {
|
||||||
const getClientInfo = createGetJellyfinClientInfoHandler({
|
const getClientInfo = createGetJellyfinClientInfoHandler({
|
||||||
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never),
|
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never),
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
export function createGetResolvedJellyfinConfigHandler(deps: {
|
export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||||
getResolvedConfig: () => { jellyfin: unknown };
|
getResolvedConfig: () => { jellyfin: unknown };
|
||||||
|
loadStoredToken: () => string | null | undefined;
|
||||||
}) {
|
}) {
|
||||||
return () => deps.getResolvedConfig().jellyfin as never;
|
return () => {
|
||||||
|
const jellyfin = deps.getResolvedConfig().jellyfin as {
|
||||||
|
accessToken?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
const configToken = jellyfin.accessToken?.trim() ?? '';
|
||||||
|
if (configToken.length > 0) {
|
||||||
|
return jellyfin as never;
|
||||||
|
}
|
||||||
|
const storedToken = deps.loadStoredToken()?.trim() ?? '';
|
||||||
|
if (storedToken.length === 0) {
|
||||||
|
return jellyfin as never;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...jellyfin,
|
||||||
|
accessToken: storedToken,
|
||||||
|
} as never;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGetJellyfinClientInfoHandler(deps: {
|
export function createGetJellyfinClientInfoHandler(deps: {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
|
|||||||
userId: 'uid',
|
userId: 'uid',
|
||||||
}),
|
}),
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'dev' }),
|
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'dev' }),
|
||||||
|
saveStoredToken: () => calls.push('save'),
|
||||||
patchJellyfinConfig: () => calls.push('patch'),
|
patchJellyfinConfig: () => calls.push('patch'),
|
||||||
logInfo: (message) => calls.push(`info:${message}`),
|
logInfo: (message) => calls.push(`info:${message}`),
|
||||||
logError: (message) => calls.push(`error:${message}`),
|
logError: (message) => calls.push(`error:${message}`),
|
||||||
@@ -43,6 +44,7 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
|
|||||||
accessToken: 'token',
|
accessToken: 'token',
|
||||||
userId: 'uid',
|
userId: 'uid',
|
||||||
});
|
});
|
||||||
|
deps.saveStoredToken('token');
|
||||||
deps.patchJellyfinConfig({
|
deps.patchJellyfinConfig({
|
||||||
serverUrl: 'http://127.0.0.1:8096',
|
serverUrl: 'http://127.0.0.1:8096',
|
||||||
username: 'alice',
|
username: 'alice',
|
||||||
@@ -55,5 +57,5 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
|
|||||||
deps.clearSetupWindow();
|
deps.clearSetupWindow();
|
||||||
deps.setSetupWindow({} as never);
|
deps.setSetupWindow({} as never);
|
||||||
assert.equal(deps.encodeURIComponent('a b'), 'a%20b');
|
assert.equal(deps.encodeURIComponent('a b'), 'a%20b');
|
||||||
assert.deepEqual(calls, ['patch', 'info:ok', 'error:bad', 'osd:toast', 'clear', 'set-window']);
|
assert.deepEqual(calls, ['save', 'patch', 'info:ok', 'error:bad', 'osd:toast', 'clear', 'set-window']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function createBuildOpenJellyfinSetupWindowMainDepsHandler(
|
|||||||
authenticateWithPassword: (server: string, username: string, password: string, clientInfo) =>
|
authenticateWithPassword: (server: string, username: string, password: string, clientInfo) =>
|
||||||
deps.authenticateWithPassword(server, username, password, clientInfo),
|
deps.authenticateWithPassword(server, username, password, clientInfo),
|
||||||
getJellyfinClientInfo: () => deps.getJellyfinClientInfo(),
|
getJellyfinClientInfo: () => deps.getJellyfinClientInfo(),
|
||||||
|
saveStoredToken: (token: string) => deps.saveStoredToken(token),
|
||||||
patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session),
|
patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session),
|
||||||
logInfo: (message: string) => deps.logInfo(message),
|
logInfo: (message: string) => deps.logInfo(message),
|
||||||
logError: (message: string, error: unknown) => deps.logError(message, error),
|
logError: (message: string, error: unknown) => deps.logError(message, error),
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
|
|||||||
userId: 'uid',
|
userId: 'uid',
|
||||||
}),
|
}),
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
||||||
|
saveStoredToken: () => calls.push('save'),
|
||||||
patchJellyfinConfig: () => calls.push('patch'),
|
patchJellyfinConfig: () => calls.push('patch'),
|
||||||
logInfo: () => calls.push('info'),
|
logInfo: () => calls.push('info'),
|
||||||
logError: () => calls.push('error'),
|
logError: () => calls.push('error'),
|
||||||
@@ -60,7 +61,7 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
|
|||||||
'subminer://jellyfin-setup?server=http%3A%2F%2Flocalhost&username=a&password=b',
|
'subminer://jellyfin-setup?server=http%3A%2F%2Flocalhost&username=a&password=b',
|
||||||
);
|
);
|
||||||
assert.equal(handled, true);
|
assert.equal(handled, true);
|
||||||
assert.deepEqual(calls, ['patch', 'info', 'osd:Jellyfin login success', 'close']);
|
assert.deepEqual(calls, ['save', 'patch', 'info', 'osd:Jellyfin login success', 'close']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async () => {
|
test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async () => {
|
||||||
@@ -71,6 +72,7 @@ test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async
|
|||||||
throw new Error('bad credentials');
|
throw new Error('bad credentials');
|
||||||
},
|
},
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
||||||
|
saveStoredToken: () => calls.push('save'),
|
||||||
patchJellyfinConfig: () => calls.push('patch'),
|
patchJellyfinConfig: () => calls.push('patch'),
|
||||||
logInfo: () => calls.push('info'),
|
logInfo: () => calls.push('info'),
|
||||||
logError: () => calls.push('error'),
|
logError: () => calls.push('error'),
|
||||||
@@ -164,6 +166,7 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is
|
|||||||
throw new Error('should not auth');
|
throw new Error('should not auth');
|
||||||
},
|
},
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
||||||
|
saveStoredToken: () => {},
|
||||||
patchJellyfinConfig: () => {},
|
patchJellyfinConfig: () => {},
|
||||||
logInfo: () => {},
|
logInfo: () => {},
|
||||||
logError: () => {},
|
logError: () => {},
|
||||||
@@ -216,6 +219,7 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
|||||||
userId: 'uid',
|
userId: 'uid',
|
||||||
}),
|
}),
|
||||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
||||||
|
saveStoredToken: () => calls.push('save'),
|
||||||
patchJellyfinConfig: () => calls.push('patch'),
|
patchJellyfinConfig: () => calls.push('patch'),
|
||||||
logInfo: () => calls.push('info'),
|
logInfo: () => calls.push('info'),
|
||||||
logError: () => calls.push('error'),
|
logError: () => calls.push('error'),
|
||||||
@@ -245,6 +249,7 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
|||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
assert.equal(prevented, true);
|
assert.equal(prevented, true);
|
||||||
|
assert.ok(calls.includes('save'));
|
||||||
assert.ok(calls.includes('patch'));
|
assert.ok(calls.includes('patch'));
|
||||||
assert.ok(calls.includes('osd:Jellyfin login success'));
|
assert.ok(calls.includes('osd:Jellyfin login success'));
|
||||||
assert.ok(calls.includes('close'));
|
assert.ok(calls.includes('close'));
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export function createHandleJellyfinSetupSubmissionHandler(deps: {
|
|||||||
clientInfo: JellyfinClientInfo,
|
clientInfo: JellyfinClientInfo,
|
||||||
) => Promise<JellyfinSession>;
|
) => Promise<JellyfinSession>;
|
||||||
getJellyfinClientInfo: () => JellyfinClientInfo;
|
getJellyfinClientInfo: () => JellyfinClientInfo;
|
||||||
|
saveStoredToken: (token: string) => void;
|
||||||
patchJellyfinConfig: (session: JellyfinSession) => void;
|
patchJellyfinConfig: (session: JellyfinSession) => void;
|
||||||
logInfo: (message: string) => void;
|
logInfo: (message: string) => void;
|
||||||
logError: (message: string, error: unknown) => void;
|
logError: (message: string, error: unknown) => void;
|
||||||
@@ -136,6 +137,7 @@ export function createHandleJellyfinSetupSubmissionHandler(deps: {
|
|||||||
submission.password,
|
submission.password,
|
||||||
deps.getJellyfinClientInfo(),
|
deps.getJellyfinClientInfo(),
|
||||||
);
|
);
|
||||||
|
deps.saveStoredToken(session.accessToken);
|
||||||
deps.patchJellyfinConfig(session);
|
deps.patchJellyfinConfig(session);
|
||||||
deps.logInfo(`Jellyfin setup saved for ${session.username}.`);
|
deps.logInfo(`Jellyfin setup saved for ${session.username}.`);
|
||||||
deps.showMpvOsd('Jellyfin login success');
|
deps.showMpvOsd('Jellyfin login success');
|
||||||
@@ -195,6 +197,7 @@ export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSet
|
|||||||
clientInfo: JellyfinClientInfo,
|
clientInfo: JellyfinClientInfo,
|
||||||
) => Promise<JellyfinSession>;
|
) => Promise<JellyfinSession>;
|
||||||
getJellyfinClientInfo: () => JellyfinClientInfo;
|
getJellyfinClientInfo: () => JellyfinClientInfo;
|
||||||
|
saveStoredToken: (token: string) => void;
|
||||||
patchJellyfinConfig: (session: JellyfinSession) => void;
|
patchJellyfinConfig: (session: JellyfinSession) => void;
|
||||||
logInfo: (message: string) => void;
|
logInfo: (message: string) => void;
|
||||||
logError: (message: string, error: unknown) => void;
|
logError: (message: string, error: unknown) => void;
|
||||||
@@ -218,6 +221,7 @@ export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSet
|
|||||||
authenticateWithPassword: (server, username, password, clientInfo) =>
|
authenticateWithPassword: (server, username, password, clientInfo) =>
|
||||||
deps.authenticateWithPassword(server, username, password, clientInfo),
|
deps.authenticateWithPassword(server, username, password, clientInfo),
|
||||||
getJellyfinClientInfo: () => deps.getJellyfinClientInfo(),
|
getJellyfinClientInfo: () => deps.getJellyfinClientInfo(),
|
||||||
|
saveStoredToken: (token) => deps.saveStoredToken(token),
|
||||||
patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session),
|
patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session),
|
||||||
logInfo: (message) => deps.logInfo(message),
|
logInfo: (message) => deps.logInfo(message),
|
||||||
logError: (message, error) => deps.logError(message, error),
|
logError: (message, error) => deps.logError(message, error),
|
||||||
|
|||||||
Reference in New Issue
Block a user