mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
feat(jellyfin): move auth to env and stored session
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
id: TASK-93
|
||||||
|
title: Remove Jellyfin token/userId from config; use env override and stored session
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-02-21 04:20'
|
||||||
|
updated_date: '2026-02-21 04:27'
|
||||||
|
labels: []
|
||||||
|
dependencies: []
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Remove `jellyfin.accessToken` and `jellyfin.userId` from editable config surface. Resolve Jellyfin auth via env override (`SUBMINER_JELLYFIN_ACCESS_TOKEN`, optional `SUBMINER_JELLYFIN_USER_ID`) and locally stored Jellyfin auth session payload persisted by login/setup.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [ ] #1 `jellyfin.accessToken` and `jellyfin.userId` no longer parsed/documented as config fields.
|
||||||
|
- [ ] #2 Jellyfin login/setup persists token + userId in local encrypted session store.
|
||||||
|
- [ ] #3 Jellyfin runtime resolves auth from env override first, then stored session payload.
|
||||||
|
- [ ] #4 Jellyfin logout clears stored session payload.
|
||||||
|
- [ ] #5 Runtime tests cover env override + stored session fallback behavior.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Implemented: evolved `jellyfin-token-store` to encrypted session payload store (`accessToken` + `userId`), updated Jellyfin resolver precedence to env-first (`SUBMINER_JELLYFIN_ACCESS_TOKEN`, optional `SUBMINER_JELLYFIN_USER_ID`) then stored session fallback, removed `jellyfin.accessToken`/`jellyfin.userId` from config defaults+parsing+public docs/example, and updated CLI/setup auth wiring to persist/clear session store while keeping `serverUrl`/`username` config behavior.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
@@ -477,8 +477,6 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"serverUrl": "http://127.0.0.1:8096",
|
"serverUrl": "http://127.0.0.1:8096",
|
||||||
"username": "",
|
"username": "",
|
||||||
"accessToken": "",
|
|
||||||
"userId": "",
|
|
||||||
"remoteControlEnabled": true,
|
"remoteControlEnabled": true,
|
||||||
"remoteControlAutoConnect": true,
|
"remoteControlAutoConnect": true,
|
||||||
"autoAnnounce": false,
|
"autoAnnounce": false,
|
||||||
@@ -496,8 +494,6 @@ 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 | Optional explicit Jellyfin access token override; leave empty to use stored local token |
|
|
||||||
| `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`) |
|
||||||
| `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) |
|
| `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) |
|
||||||
@@ -512,7 +508,10 @@ 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 auth resolution order:
|
||||||
|
|
||||||
|
1. `SUBMINER_JELLYFIN_ACCESS_TOKEN` (and optional `SUBMINER_JELLYFIN_USER_ID`) environment overrides.
|
||||||
|
2. Locally stored encrypted Jellyfin session payload saved by login/setup (`accessToken` + `userId`).
|
||||||
|
|
||||||
Jellyfin direct app CLI commands (`SubMiner.AppImage ...`):
|
Jellyfin direct app CLI commands (`SubMiner.AppImage ...`):
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ remote playback target in Jellyfin's cast-to-device menu.
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- `jellyfin.enabled=true`
|
- `jellyfin.enabled=true`
|
||||||
- valid `jellyfin.serverUrl`, `jellyfin.accessToken`, and `jellyfin.userId`
|
- valid `jellyfin.serverUrl` and Jellyfin auth session (env override or stored login session)
|
||||||
- `jellyfin.remoteControlEnabled=true` (default)
|
- `jellyfin.remoteControlEnabled=true` (default)
|
||||||
- `jellyfin.remoteControlAutoConnect=true` (default)
|
- `jellyfin.remoteControlAutoConnect=true` (default)
|
||||||
- `jellyfin.autoAnnounce=false` by default (`true` enables auto announce/visibility check logs on connect)
|
- `jellyfin.autoAnnounce=false` by default (`true` enables auto announce/visibility check logs on connect)
|
||||||
@@ -149,8 +149,8 @@ User-visible errors are shown through CLI logs and mpv OSD for:
|
|||||||
|
|
||||||
## Security Notes and Limitations
|
## Security Notes and Limitations
|
||||||
|
|
||||||
- Jellyfin access token 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.
|
||||||
- `jellyfin.accessToken` remains as an optional explicit override in `config.jsonc`.
|
- 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.
|
||||||
- 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.
|
||||||
|
|||||||
@@ -273,15 +273,15 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// 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 local encrypted token storage after login/setup.
|
// Auth session (access token + user id) is stored in local encrypted storage after login/setup.
|
||||||
// jellyfin.accessToken below remains an optional explicit override.
|
// Optional environment overrides:
|
||||||
|
// SUBMINER_JELLYFIN_ACCESS_TOKEN
|
||||||
|
// SUBMINER_JELLYFIN_USER_ID
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"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": "", // Optional explicit access token override; leave empty to use stored local token.
|
|
||||||
"userId": "", // User id setting.
|
|
||||||
"deviceId": "subminer", // Device id setting.
|
"deviceId": "subminer", // Device id setting.
|
||||||
"clientName": "SubMiner", // Client name setting.
|
"clientName": "SubMiner", // Client name setting.
|
||||||
"clientVersion": "0.1.0", // Client version setting.
|
"clientVersion": "0.1.0", // Client version setting.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Read first. Keep concise.
|
|||||||
| agent_id | alias | mission | status | file | last_update_utc |
|
| agent_id | alias | mission | status | file | last_update_utc |
|
||||||
| ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- |
|
| ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- |
|
||||||
| `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` |
|
| `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` |
|
||||||
|
| `codex-frequency-dup-log-20260221T042815Z-r4k1` | `codex-frequency-dup-log` | `Reduce frequency dictionary duplicate-term startup log spam` | `completed` | `docs/subagents/agents/codex-frequency-dup-log-20260221T042815Z-r4k1.md` | `2026-02-21T04:32:40Z` |
|
||||||
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
|
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
|
||||||
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` |
|
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` |
|
||||||
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
|
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
|
||||||
@@ -20,9 +21,10 @@ 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-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Move Jellyfin token/userId out of config into env override + stored session payload` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-21T04:27:24Z` |
|
||||||
| `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-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` |
|
| `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` |
|
||||||
| `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-20T11:48:28Z` |
|
| `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-20T11:48:28Z` |
|
||||||
| `codex-commit-unstaged-20260220T115057Z-k7q2` | `codex-commit-unstaged` | `Commit all current unstaged repository changes with content-derived conventional message` | `in_progress` | `docs/subagents/agents/codex-commit-unstaged-20260220T115057Z-k7q2.md` | `2026-02-20T11:51:18Z` |
|
| `codex-commit-unstaged-20260220T115057Z-k7q2` | `codex-commit-unstaged` | `Commit all current unstaged repository changes with content-derived conventional message` | `in_progress` | `docs/subagents/agents/codex-commit-unstaged-20260220T115057Z-k7q2.md` | `2026-02-20T11:51:18Z` |
|
||||||
| `codex-overlay-whitespace-newline-20260221T040705Z-aw2j` | `codex-overlay-whitespace-newline` | `Fix visible overlay whitespace/newline token rendering bug with TDD regression coverage` | `completed` | `docs/subagents/agents/codex-overlay-whitespace-newline-20260221T040705Z-aw2j.md` | `2026-02-21T04:18:16Z` |
|
| `codex-overlay-whitespace-newline-20260221T040705Z-aw2j` | `codex-overlay-whitespace-newline` | `Fix visible overlay whitespace/newline token rendering bug with TDD regression coverage` | `completed` | `docs/subagents/agents/codex-overlay-whitespace-newline-20260221T040705Z-aw2j.md` | `2026-02-21T04:18:16Z` |
|
||||||
|
| `codex-duplicate-kiku-20260221T043006Z-5vkz` | `codex-duplicate-kiku` | `Fix Kiku duplicate-card detection/grouping regression for Yomitan duplicate-marked + N+1-highlighted cards` | `completed` | `docs/subagents/agents/codex-duplicate-kiku-20260221T043006Z-5vkz.md` | `2026-02-21T04:33:17Z` |
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# codex-jellyfin-secret-store-20260220T101428Z-om4z
|
# codex-jellyfin-secret-store-20260220T101428Z-om4z
|
||||||
|
|
||||||
- alias: `codex-jellyfin-secret-store`
|
- alias: `codex-jellyfin-secret-store`
|
||||||
- mission: `Verify whether Jellyfin token can use same secret-store path as AniList token`
|
- mission: `Move Jellyfin token/userId out of config into env override + stored session payload`
|
||||||
- status: `completed`
|
- status: `completed`
|
||||||
- last_update_utc: `2026-02-20T10:22:45Z`
|
- last_update_utc: `2026-02-21T04:27:24Z`
|
||||||
|
|
||||||
## Intent
|
## Intent
|
||||||
|
|
||||||
@@ -63,3 +63,54 @@
|
|||||||
- `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`
|
- `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 test:fast`
|
||||||
- `bun run docs:build`
|
- `bun run docs:build`
|
||||||
|
|
||||||
|
## Next Intent (2026-02-21)
|
||||||
|
|
||||||
|
- remove `jellyfin.accessToken` config path; add env override (`SUBMINER_JELLYFIN_ACCESS_TOKEN`)
|
||||||
|
- move `jellyfin.userId` out of config into stored Jellyfin auth session payload
|
||||||
|
- keep login/logout semantics via setup/CLI commands
|
||||||
|
- add/update runtime tests first, then implementation + docs
|
||||||
|
|
||||||
|
## 2026-02-21 Outcome
|
||||||
|
|
||||||
|
- removed `jellyfin.accessToken` + `jellyfin.userId` from config defaults/parsing/docs surface
|
||||||
|
- added env override precedence in resolver:
|
||||||
|
- `SUBMINER_JELLYFIN_ACCESS_TOKEN`
|
||||||
|
- `SUBMINER_JELLYFIN_USER_ID` (optional; falls back to stored userId if session exists)
|
||||||
|
- moved Jellyfin store from token-only to auth-session payload (`accessToken` + `userId`)
|
||||||
|
- updated login/setup/logout flows to save/clear stored session payload
|
||||||
|
|
||||||
|
## 2026-02-21 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-main-deps.ts`
|
||||||
|
- `src/main/runtime/jellyfin-cli-auth.ts`
|
||||||
|
- `src/main/runtime/jellyfin-cli-main-deps.ts`
|
||||||
|
- `src/main/runtime/jellyfin-setup-window.ts`
|
||||||
|
- `src/main/runtime/jellyfin-setup-window-main-deps.ts`
|
||||||
|
- `src/main/runtime/jellyfin-remote-commands.ts`
|
||||||
|
- `src/main/runtime/jellyfin-remote-session-lifecycle.ts`
|
||||||
|
- `src/config/definitions.ts`
|
||||||
|
- `src/config/service.ts`
|
||||||
|
- `src/types.ts`
|
||||||
|
- `src/main/runtime/jellyfin-client-info.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-client-info-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-cli-auth.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-cli-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-setup-window.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-setup-window-main-deps.test.ts`
|
||||||
|
- `docs/configuration.md`
|
||||||
|
- `docs/jellyfin-integration.md`
|
||||||
|
- `docs/public/config.example.jsonc`
|
||||||
|
- `backlog/tasks/task-93 - Remove-Jellyfin-token-userId-from-config-use-env-and-stored-session.md`
|
||||||
|
|
||||||
|
## 2026-02-21 Verification
|
||||||
|
|
||||||
|
- `bun run tsc --noEmit`
|
||||||
|
- `bun test src/main/runtime/jellyfin-client-info.test.ts src/main/runtime/jellyfin-cli-auth.test.ts src/main/runtime/jellyfin-setup-window.test.ts src/main/runtime/jellyfin-client-info-main-deps.test.ts src/main/runtime/jellyfin-cli-main-deps.test.ts src/main/runtime/jellyfin-setup-window-main-deps.test.ts src/main/runtime/jellyfin-remote-session-lifecycle.test.ts src/main/runtime/jellyfin-command-dispatch.test.ts src/main/runtime/jellyfin-remote-commands.test.ts src/config/config.test.ts` (73 pass)
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- full `bun run build` currently blocked by local disk-full (`ENOSPC`) in workspace output path
|
||||||
|
|||||||
@@ -20,3 +20,7 @@ Shared notes. Append-only.
|
|||||||
- [2026-02-21T04:09:02Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] completed: whitespace-only token surfaces no longer become token segments; non-preserve mode now flattens token newlines to spaces and renders whitespace as text nodes; added regression test in renderer suite.
|
- [2026-02-21T04:09:02Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] completed: whitespace-only token surfaces no longer become token segments; non-preserve mode now flattens token newlines to spaces and renders whitespace as text nodes; added regression test in renderer suite.
|
||||||
- [2026-02-21T04:14:30Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] preserve-line-breaks follow-up: when token surface mismatches source (e.g., `1` vs `1`), alignment now skips unmatched token instead of appending both source tail + token; fixes duplicated no-break line artifact.
|
- [2026-02-21T04:14:30Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] preserve-line-breaks follow-up: when token surface mismatches source (e.g., `1` vs `1`), alignment now skips unmatched token instead of appending both source tail + token; fixes duplicated no-break line artifact.
|
||||||
- [2026-02-21T04:18:16Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] follow-up fix: non-token fallback now honors preserveLineBreaks flag by collapsing line breaks when disabled; prevents visible multi-line -> single-line transition while tokenized payload arrives.
|
- [2026-02-21T04:18:16Z] [codex-overlay-whitespace-newline-20260221T040705Z-aw2j|codex-overlay-whitespace-newline] follow-up fix: non-token fallback now honors preserveLineBreaks flag by collapsing line breaks when disabled; prevents visible multi-line -> single-line transition while tokenized payload arrives.
|
||||||
|
- [2026-02-21T04:18:58Z] [codex-jellyfin-secret-store-20260220T101428Z-om4z|codex-jellyfin-secret-store] overlap note: follow-up Jellyfin auth refactor touching `src/main.ts`, `src/main/runtime/jellyfin-*`, and config/docs to remove config token/userId fields in favor of env+stored session payload.
|
||||||
|
- [2026-02-21T04:27:24Z] [codex-jellyfin-secret-store-20260220T101428Z-om4z|codex-jellyfin-secret-store] completed TASK-93: removed Jellyfin accessToken/userId config fields; resolver now uses env-first (`SUBMINER_JELLYFIN_ACCESS_TOKEN` + optional `SUBMINER_JELLYFIN_USER_ID`) then stored encrypted session payload; login/setup save session and logout clears session.
|
||||||
|
- [2026-02-21T04:30:06Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] investigating Kiku duplicate grouping regression; expecting touches in `src/anki-integration/duplicate.ts` and duplicate-detection tests only.
|
||||||
|
- [2026-02-21T04:33:17Z] [codex-duplicate-kiku-20260221T043006Z-5vkz|codex-duplicate-kiku] completed TASK-94: duplicate check now resolves `word`/`expression` alias fields when validating candidate notes; added regression test `src/anki-integration/duplicate.test.ts`; targeted build + duplicate/anki-integration tests passed.
|
||||||
|
|||||||
@@ -220,8 +220,6 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
username: '',
|
username: '',
|
||||||
accessToken: '',
|
|
||||||
userId: '',
|
|
||||||
deviceId: 'subminer',
|
deviceId: 'subminer',
|
||||||
clientName: 'SubMiner',
|
clientName: 'SubMiner',
|
||||||
clientVersion: '0.1.0',
|
clientVersion: '0.1.0',
|
||||||
@@ -807,8 +805,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 local encrypted token storage after login/setup.',
|
'Auth session (access token + user id) is stored in local encrypted storage after login/setup.',
|
||||||
'jellyfin.accessToken remains an optional explicit override in config.',
|
'Optional env overrides: SUBMINER_JELLYFIN_ACCESS_TOKEN and SUBMINER_JELLYFIN_USER_ID.',
|
||||||
],
|
],
|
||||||
key: 'jellyfin',
|
key: 'jellyfin',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -519,8 +519,6 @@ export class ConfigService {
|
|||||||
const stringKeys = [
|
const stringKeys = [
|
||||||
'serverUrl',
|
'serverUrl',
|
||||||
'username',
|
'username',
|
||||||
'accessToken',
|
|
||||||
'userId',
|
|
||||||
'deviceId',
|
'deviceId',
|
||||||
'clientName',
|
'clientName',
|
||||||
'clientVersion',
|
'clientVersion',
|
||||||
|
|||||||
@@ -2,16 +2,27 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { safeStorage } from 'electron';
|
import { safeStorage } from 'electron';
|
||||||
|
|
||||||
interface PersistedTokenPayload {
|
interface PersistedSessionPayload {
|
||||||
|
encryptedSession?: string;
|
||||||
|
plaintextSession?: {
|
||||||
|
accessToken?: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
// Legacy payload fields (token only).
|
||||||
encryptedToken?: string;
|
encryptedToken?: string;
|
||||||
plaintextToken?: string;
|
plaintextToken?: string;
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JellyfinStoredSession {
|
||||||
|
accessToken: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface JellyfinTokenStore {
|
export interface JellyfinTokenStore {
|
||||||
loadToken: () => string | null;
|
loadSession: () => JellyfinStoredSession | null;
|
||||||
saveToken: (token: string) => void;
|
saveSession: (session: JellyfinStoredSession) => void;
|
||||||
clearToken: () => void;
|
clearSession: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDirectory(filePath: string): 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);
|
ensureDirectory(filePath);
|
||||||
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8');
|
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8');
|
||||||
}
|
}
|
||||||
@@ -35,65 +46,94 @@ export function createJellyfinTokenStore(
|
|||||||
},
|
},
|
||||||
): JellyfinTokenStore {
|
): JellyfinTokenStore {
|
||||||
return {
|
return {
|
||||||
loadToken(): string | null {
|
loadSession(): JellyfinStoredSession | null {
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(filePath, 'utf-8');
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
||||||
const parsed = JSON.parse(raw) as PersistedTokenPayload;
|
const parsed = JSON.parse(raw) as PersistedSessionPayload;
|
||||||
if (typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) {
|
|
||||||
const encrypted = Buffer.from(parsed.encryptedToken, 'base64');
|
if (typeof parsed.encryptedSession === 'string' && parsed.encryptedSession.length > 0) {
|
||||||
|
const encrypted = Buffer.from(parsed.encryptedSession, 'base64');
|
||||||
if (!safeStorage.isEncryptionAvailable()) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
const decrypted = safeStorage.decryptString(encrypted).trim();
|
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();
|
if (parsed.plaintextSession && typeof parsed.plaintextSession === 'object') {
|
||||||
this.saveToken(plaintext);
|
const accessToken =
|
||||||
return plaintext;
|
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) {
|
} catch (error) {
|
||||||
logger.error('Failed to read Jellyfin token store.', error);
|
logger.error('Failed to read Jellyfin session store.', error);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
saveToken(token: string): void {
|
saveSession(session: JellyfinStoredSession): void {
|
||||||
const trimmed = token.trim();
|
const accessToken = session.accessToken.trim();
|
||||||
if (trimmed.length === 0) {
|
const userId = session.userId.trim();
|
||||||
this.clearToken();
|
if (!accessToken || !userId) {
|
||||||
|
this.clearSession();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!safeStorage.isEncryptionAvailable()) {
|
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, {
|
writePayload(filePath, {
|
||||||
plaintextToken: trimmed,
|
plaintextSession: {
|
||||||
|
accessToken,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const encrypted = safeStorage.encryptString(trimmed);
|
const encrypted = safeStorage.encryptString(JSON.stringify({ accessToken, userId }));
|
||||||
writePayload(filePath, {
|
writePayload(filePath, {
|
||||||
encryptedToken: encrypted.toString('base64'),
|
encryptedSession: encrypted.toString('base64'),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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;
|
if (!fs.existsSync(filePath)) return;
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
logger.info('Cleared stored Jellyfin token.');
|
logger.info('Cleared stored Jellyfin session.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to clear stored Jellyfin token.', error);
|
logger.error('Failed to clear stored Jellyfin session.', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
11
src/main.ts
11
src/main.ts
@@ -1166,7 +1166,8 @@ function getResolvedConfig() {
|
|||||||
const buildGetResolvedJellyfinConfigMainDepsHandler =
|
const buildGetResolvedJellyfinConfigMainDepsHandler =
|
||||||
createBuildGetResolvedJellyfinConfigMainDepsHandler({
|
createBuildGetResolvedJellyfinConfigMainDepsHandler({
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
loadStoredToken: () => jellyfinTokenStore.loadToken(),
|
loadStoredSession: () => jellyfinTokenStore.loadSession(),
|
||||||
|
getEnv: (name: string) => process.env[name],
|
||||||
});
|
});
|
||||||
const getResolvedJellyfinConfigMainDeps =
|
const getResolvedJellyfinConfigMainDeps =
|
||||||
buildGetResolvedJellyfinConfigMainDepsHandler();
|
buildGetResolvedJellyfinConfigMainDepsHandler();
|
||||||
@@ -1322,8 +1323,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),
|
saveStoredSession: (session) => jellyfinTokenStore.saveSession(session),
|
||||||
clearStoredToken: () => jellyfinTokenStore.clearToken(),
|
clearStoredSession: () => jellyfinTokenStore.clearSession(),
|
||||||
logInfo: (message) => logger.info(message),
|
logInfo: (message) => logger.info(message),
|
||||||
});
|
});
|
||||||
const handleJellyfinAuthCommandsMainDeps =
|
const handleJellyfinAuthCommandsMainDeps =
|
||||||
@@ -1586,15 +1587,13 @@ 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),
|
saveStoredSession: (session) => jellyfinTokenStore.saveSession(session),
|
||||||
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: '',
|
|
||||||
userId: session.userId,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,8 +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'),
|
saveStoredSession: () => calls.push('save'),
|
||||||
clearStoredToken: () => calls.push('clear'),
|
clearStoredSession: () => calls.push('clear'),
|
||||||
authenticateWithPassword: async () => {
|
authenticateWithPassword: async () => {
|
||||||
throw new Error('should not authenticate');
|
throw new Error('should not authenticate');
|
||||||
},
|
},
|
||||||
@@ -24,8 +24,6 @@ test('jellyfin auth handler processes logout', async () => {
|
|||||||
jellyfinConfig: {
|
jellyfinConfig: {
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
username: '',
|
username: '',
|
||||||
accessToken: '',
|
|
||||||
userId: '',
|
|
||||||
},
|
},
|
||||||
serverUrl: 'http://localhost',
|
serverUrl: 'http://localhost',
|
||||||
clientInfo: {
|
clientInfo: {
|
||||||
@@ -41,10 +39,18 @@ test('jellyfin auth handler processes logout', async () => {
|
|||||||
|
|
||||||
test('jellyfin auth handler processes login', async () => {
|
test('jellyfin auth handler processes login', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
let patchPayload: unknown = null;
|
||||||
|
let storedSession: unknown = null;
|
||||||
const handleAuth = createHandleJellyfinAuthCommands({
|
const handleAuth = createHandleJellyfinAuthCommands({
|
||||||
patchRawConfig: () => calls.push('patch'),
|
patchRawConfig: (patch) => {
|
||||||
saveStoredToken: () => calls.push('save'),
|
patchPayload = patch;
|
||||||
clearStoredToken: () => calls.push('clear'),
|
calls.push('patch');
|
||||||
|
},
|
||||||
|
saveStoredSession: (session) => {
|
||||||
|
storedSession = session;
|
||||||
|
calls.push('save');
|
||||||
|
},
|
||||||
|
clearStoredSession: () => calls.push('clear'),
|
||||||
authenticateWithPassword: async () => ({
|
authenticateWithPassword: async () => ({
|
||||||
serverUrl: 'http://localhost',
|
serverUrl: 'http://localhost',
|
||||||
username: 'user',
|
username: 'user',
|
||||||
@@ -64,8 +70,6 @@ test('jellyfin auth handler processes login', async () => {
|
|||||||
jellyfinConfig: {
|
jellyfinConfig: {
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
username: '',
|
username: '',
|
||||||
accessToken: '',
|
|
||||||
userId: '',
|
|
||||||
},
|
},
|
||||||
serverUrl: 'http://localhost',
|
serverUrl: 'http://localhost',
|
||||||
clientInfo: {
|
clientInfo: {
|
||||||
@@ -78,14 +82,25 @@ test('jellyfin auth handler processes login', async () => {
|
|||||||
assert.equal(handled, true);
|
assert.equal(handled, true);
|
||||||
assert.ok(calls.includes('save'));
|
assert.ok(calls.includes('save'));
|
||||||
assert.ok(calls.includes('patch'));
|
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')));
|
assert.ok(calls.some((entry) => entry.includes('Jellyfin login succeeded')));
|
||||||
});
|
});
|
||||||
|
|
||||||
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: () => {},
|
saveStoredSession: () => {},
|
||||||
clearStoredToken: () => {},
|
clearStoredSession: () => {},
|
||||||
authenticateWithPassword: async () => ({
|
authenticateWithPassword: async () => ({
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
username: '',
|
username: '',
|
||||||
@@ -105,8 +120,6 @@ test('jellyfin auth handler no-ops when no auth command', async () => {
|
|||||||
jellyfinConfig: {
|
jellyfinConfig: {
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
username: '',
|
username: '',
|
||||||
accessToken: '',
|
|
||||||
userId: '',
|
|
||||||
},
|
},
|
||||||
serverUrl: 'http://localhost',
|
serverUrl: 'http://localhost',
|
||||||
clientInfo: {
|
clientInfo: {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import type { CliArgs } from '../../cli/args';
|
|||||||
type JellyfinConfig = {
|
type JellyfinConfig = {
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
username: string;
|
username: string;
|
||||||
accessToken: string;
|
|
||||||
userId: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type JellyfinClientInfo = {
|
type JellyfinClientInfo = {
|
||||||
@@ -26,8 +24,6 @@ export function createHandleJellyfinAuthCommands(deps: {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
username: string;
|
username: string;
|
||||||
accessToken: string;
|
|
||||||
userId: string;
|
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
clientVersion: string;
|
clientVersion: string;
|
||||||
@@ -39,8 +35,8 @@ export function createHandleJellyfinAuthCommands(deps: {
|
|||||||
password: string,
|
password: string,
|
||||||
clientInfo: JellyfinClientInfo,
|
clientInfo: JellyfinClientInfo,
|
||||||
) => Promise<JellyfinSession>;
|
) => Promise<JellyfinSession>;
|
||||||
saveStoredToken: (token: string) => void;
|
saveStoredSession: (session: { accessToken: string; userId: string }) => void;
|
||||||
clearStoredToken: () => void;
|
clearStoredSession: () => void;
|
||||||
logInfo: (message: string) => void;
|
logInfo: (message: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return async (params: {
|
return async (params: {
|
||||||
@@ -50,14 +46,11 @@ export function createHandleJellyfinAuthCommands(deps: {
|
|||||||
clientInfo: JellyfinClientInfo;
|
clientInfo: JellyfinClientInfo;
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
if (params.args.jellyfinLogout) {
|
if (params.args.jellyfinLogout) {
|
||||||
deps.clearStoredToken();
|
deps.clearStoredSession();
|
||||||
deps.patchRawConfig({
|
deps.patchRawConfig({
|
||||||
jellyfin: {
|
jellyfin: {},
|
||||||
accessToken: '',
|
|
||||||
userId: '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
deps.logInfo('Cleared stored Jellyfin access token.');
|
deps.logInfo('Cleared stored Jellyfin auth session.');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +66,15 @@ export function createHandleJellyfinAuthCommands(deps: {
|
|||||||
password,
|
password,
|
||||||
params.clientInfo,
|
params.clientInfo,
|
||||||
);
|
);
|
||||||
deps.saveStoredToken(session.accessToken);
|
deps.saveStoredSession({
|
||||||
|
accessToken: session.accessToken,
|
||||||
|
userId: session.userId,
|
||||||
|
});
|
||||||
deps.patchRawConfig({
|
deps.patchRawConfig({
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
serverUrl: session.serverUrl,
|
serverUrl: session.serverUrl,
|
||||||
username: session.username,
|
username: session.username,
|
||||||
accessToken: '',
|
|
||||||
userId: session.userId,
|
|
||||||
deviceId: params.clientInfo.deviceId,
|
deviceId: params.clientInfo.deviceId,
|
||||||
clientName: params.clientInfo.clientName,
|
clientName: params.clientInfo.clientName,
|
||||||
clientVersion: params.clientInfo.clientVersion,
|
clientVersion: params.clientInfo.clientVersion,
|
||||||
|
|||||||
@@ -12,8 +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'),
|
saveStoredSession: () => calls.push('save'),
|
||||||
clearStoredToken: () => calls.push('clear'),
|
clearStoredSession: () => calls.push('clear'),
|
||||||
logInfo: (message) => calls.push(`info:${message}`),
|
logInfo: (message) => calls.push(`info:${message}`),
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ test('jellyfin auth commands main deps builder maps callbacks', async () => {
|
|||||||
clientName: '',
|
clientName: '',
|
||||||
clientVersion: '',
|
clientVersion: '',
|
||||||
});
|
});
|
||||||
deps.saveStoredToken('token');
|
deps.saveStoredSession({ accessToken: 'token', userId: 'uid' });
|
||||||
deps.clearStoredToken();
|
deps.clearStoredSession();
|
||||||
deps.logInfo('ok');
|
deps.logInfo('ok');
|
||||||
assert.deepEqual(calls, ['patch', 'save', 'clear', 'info:ok']);
|
assert.deepEqual(calls, ['patch', 'save', 'clear', 'info:ok']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +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),
|
saveStoredSession: (session) => deps.saveStoredSession(session),
|
||||||
clearStoredToken: () => deps.clearStoredToken(),
|
clearStoredSession: () => deps.clearStoredSession(),
|
||||||
logInfo: (message: string) => deps.logInfo(message),
|
logInfo: (message: string) => deps.logInfo(message),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ 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',
|
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'uid' }),
|
||||||
|
getEnv: (key: string) => (key === 'TEST' ? 'x' : undefined),
|
||||||
})();
|
})();
|
||||||
assert.equal(deps.getResolvedConfig(), resolved);
|
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', () => {
|
test('get jellyfin client info main deps builder maps callbacks', () => {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export function createBuildGetResolvedJellyfinConfigMainDepsHandler(
|
|||||||
) {
|
) {
|
||||||
return (): GetResolvedJellyfinConfigMainDeps => ({
|
return (): GetResolvedJellyfinConfigMainDeps => ({
|
||||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||||
loadStoredToken: () => deps.loadStoredToken(),
|
loadStoredSession: () => deps.loadStoredSession(),
|
||||||
|
getEnv: (name: string) => deps.getEnv(name),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,23 +9,23 @@ 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,
|
loadStoredSession: () => null,
|
||||||
|
getEnv: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(getConfig(), jellyfin);
|
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({
|
const getConfig = createGetResolvedJellyfinConfigHandler({
|
||||||
getResolvedConfig: () =>
|
getResolvedConfig: () =>
|
||||||
({
|
({
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
serverUrl: 'http://localhost:8096',
|
serverUrl: 'http://localhost:8096',
|
||||||
accessToken: ' ',
|
|
||||||
userId: 'uid-1',
|
|
||||||
},
|
},
|
||||||
}) as never,
|
}) as never,
|
||||||
loadStoredToken: () => 'stored-token',
|
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'uid-1' }),
|
||||||
|
getEnv: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(getConfig(), {
|
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', () => {
|
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,24 +1,37 @@
|
|||||||
export function createGetResolvedJellyfinConfigHandler(deps: {
|
export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||||
getResolvedConfig: () => { jellyfin: unknown };
|
getResolvedConfig: () => { jellyfin: unknown };
|
||||||
loadStoredToken: () => string | null | undefined;
|
loadStoredSession: () => { accessToken: string; userId: string } | null | undefined;
|
||||||
|
getEnv: (name: string) => string | undefined;
|
||||||
}) {
|
}) {
|
||||||
return () => {
|
return () => {
|
||||||
const jellyfin = deps.getResolvedConfig().jellyfin as {
|
const jellyfin = deps.getResolvedConfig().jellyfin as {
|
||||||
accessToken?: string;
|
userId?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
const configToken = jellyfin.accessToken?.trim() ?? '';
|
|
||||||
if (configToken.length > 0) {
|
const envToken = deps.getEnv('SUBMINER_JELLYFIN_ACCESS_TOKEN')?.trim() ?? '';
|
||||||
return jellyfin as never;
|
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) {
|
if (storedToken.length > 0 && storedUserId.length > 0) {
|
||||||
return jellyfin as never;
|
return {
|
||||||
|
...jellyfin,
|
||||||
|
accessToken: storedToken,
|
||||||
|
userId: storedUserId,
|
||||||
|
} as never;
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
...jellyfin,
|
return jellyfin as never;
|
||||||
accessToken: storedToken,
|
|
||||||
} as never;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ type JellyfinClientInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type JellyfinConfigLike = {
|
type JellyfinConfigLike = {
|
||||||
serverUrl: string;
|
serverUrl?: string;
|
||||||
accessToken: string;
|
accessToken?: string;
|
||||||
userId: string;
|
userId?: string;
|
||||||
username: string;
|
username?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function asInteger(value: unknown): number | undefined {
|
function asInteger(value: unknown): number | undefined {
|
||||||
@@ -39,7 +39,7 @@ export function getConfiguredJellyfinSession(config: JellyfinConfigLike): Jellyf
|
|||||||
serverUrl: config.serverUrl,
|
serverUrl: config.serverUrl,
|
||||||
accessToken: config.accessToken,
|
accessToken: config.accessToken,
|
||||||
userId: config.userId,
|
userId: config.userId,
|
||||||
username: config.username,
|
username: config.username || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ type JellyfinRemoteConfig = {
|
|||||||
remoteControlEnabled: boolean;
|
remoteControlEnabled: boolean;
|
||||||
remoteControlAutoConnect: boolean;
|
remoteControlAutoConnect: boolean;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
accessToken: string;
|
accessToken?: string;
|
||||||
userId: string;
|
userId?: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
clientVersion: string;
|
clientVersion: string;
|
||||||
|
|||||||
@@ -17,7 +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'),
|
saveStoredSession: () => 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}`),
|
||||||
@@ -44,7 +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.saveStoredSession({ accessToken: 'token', userId: 'uid' });
|
||||||
deps.patchJellyfinConfig({
|
deps.patchJellyfinConfig({
|
||||||
serverUrl: 'http://127.0.0.1:8096',
|
serverUrl: 'http://127.0.0.1:8096',
|
||||||
username: 'alice',
|
username: 'alice',
|
||||||
|
|||||||
@@ -15,7 +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),
|
saveStoredSession: (session) => deps.saveStoredSession(session),
|
||||||
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),
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ test('parseJellyfinSetupSubmissionUrl parses setup url parameters', () => {
|
|||||||
|
|
||||||
test('createHandleJellyfinSetupSubmissionHandler applies successful login', async () => {
|
test('createHandleJellyfinSetupSubmissionHandler applies successful login', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
let patchPayload: unknown = null;
|
||||||
|
let savedSession: unknown = null;
|
||||||
const handler = createHandleJellyfinSetupSubmissionHandler({
|
const handler = createHandleJellyfinSetupSubmissionHandler({
|
||||||
parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
|
parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
|
||||||
authenticateWithPassword: async () => ({
|
authenticateWithPassword: async () => ({
|
||||||
@@ -49,8 +51,14 @@ 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'),
|
saveStoredSession: (session) => {
|
||||||
patchJellyfinConfig: () => calls.push('patch'),
|
savedSession = session;
|
||||||
|
calls.push('save');
|
||||||
|
},
|
||||||
|
patchJellyfinConfig: (session) => {
|
||||||
|
patchPayload = session;
|
||||||
|
calls.push('patch');
|
||||||
|
},
|
||||||
logInfo: () => calls.push('info'),
|
logInfo: () => calls.push('info'),
|
||||||
logError: () => calls.push('error'),
|
logError: () => calls.push('error'),
|
||||||
showMpvOsd: (message) => calls.push(`osd:${message}`),
|
showMpvOsd: (message) => calls.push(`osd:${message}`),
|
||||||
@@ -62,6 +70,13 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
|
|||||||
);
|
);
|
||||||
assert.equal(handled, true);
|
assert.equal(handled, true);
|
||||||
assert.deepEqual(calls, ['save', 'patch', 'info', 'osd:Jellyfin login success', 'close']);
|
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 () => {
|
test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async () => {
|
||||||
@@ -72,7 +87,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'),
|
saveStoredSession: () => 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'),
|
||||||
@@ -166,7 +181,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: () => {},
|
saveStoredSession: () => {},
|
||||||
patchJellyfinConfig: () => {},
|
patchJellyfinConfig: () => {},
|
||||||
logInfo: () => {},
|
logInfo: () => {},
|
||||||
logError: () => {},
|
logError: () => {},
|
||||||
@@ -219,7 +234,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'),
|
saveStoredSession: () => 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'),
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function createHandleJellyfinSetupSubmissionHandler(deps: {
|
|||||||
clientInfo: JellyfinClientInfo,
|
clientInfo: JellyfinClientInfo,
|
||||||
) => Promise<JellyfinSession>;
|
) => Promise<JellyfinSession>;
|
||||||
getJellyfinClientInfo: () => JellyfinClientInfo;
|
getJellyfinClientInfo: () => JellyfinClientInfo;
|
||||||
saveStoredToken: (token: string) => void;
|
saveStoredSession: (session: { accessToken: string; userId: 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;
|
||||||
@@ -137,7 +137,10 @@ export function createHandleJellyfinSetupSubmissionHandler(deps: {
|
|||||||
submission.password,
|
submission.password,
|
||||||
deps.getJellyfinClientInfo(),
|
deps.getJellyfinClientInfo(),
|
||||||
);
|
);
|
||||||
deps.saveStoredToken(session.accessToken);
|
deps.saveStoredSession({
|
||||||
|
accessToken: session.accessToken,
|
||||||
|
userId: session.userId,
|
||||||
|
});
|
||||||
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');
|
||||||
@@ -197,7 +200,7 @@ export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSet
|
|||||||
clientInfo: JellyfinClientInfo,
|
clientInfo: JellyfinClientInfo,
|
||||||
) => Promise<JellyfinSession>;
|
) => Promise<JellyfinSession>;
|
||||||
getJellyfinClientInfo: () => JellyfinClientInfo;
|
getJellyfinClientInfo: () => JellyfinClientInfo;
|
||||||
saveStoredToken: (token: string) => void;
|
saveStoredSession: (session: { accessToken: string; userId: 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;
|
||||||
@@ -221,7 +224,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),
|
saveStoredSession: (session) => deps.saveStoredSession(session),
|
||||||
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),
|
||||||
|
|||||||
@@ -342,8 +342,6 @@ export interface JellyfinConfig {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
accessToken?: string;
|
|
||||||
userId?: string;
|
|
||||||
deviceId?: string;
|
deviceId?: string;
|
||||||
clientName?: string;
|
clientName?: string;
|
||||||
clientVersion?: string;
|
clientVersion?: string;
|
||||||
@@ -515,8 +513,6 @@ export interface ResolvedConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
serverUrl: string;
|
serverUrl: string;
|
||||||
username: string;
|
username: string;
|
||||||
accessToken: string;
|
|
||||||
userId: string;
|
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
clientVersion: string;
|
clientVersion: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user