From 4ad81095085b5df803cd4ce328b8d1efbb4c0235 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 21 Feb 2026 17:50:09 -0800 Subject: [PATCH] fix(shortcuts): gate feature-dependent bindings Disable Anki-dependent shortcuts when AnkiConnect is off and require jellyfin.enabled for remote startup warmups to avoid initializing disabled integrations. --- ...pendent-keybindings-behind-config-flags.md | 50 ++++++++++-- docs/configuration.md | 44 ++++++----- package.json | 2 +- src/core/utils/shortcut-config.test.ts | 76 +++++++++++++++++++ src/core/utils/shortcut-config.ts | 24 ++++-- src/main.ts | 7 +- .../jellyfin-remote-session-lifecycle.test.ts | 36 ++++++++- .../jellyfin-remote-session-lifecycle.ts | 6 +- src/main/runtime/startup-warmups.test.ts | 46 ++++++++++- 9 files changed, 251 insertions(+), 40 deletions(-) create mode 100644 src/core/utils/shortcut-config.test.ts diff --git a/backlog/tasks/task-84 - Gate-feature-dependent-keybindings-behind-config-flags.md b/backlog/tasks/task-84 - Gate-feature-dependent-keybindings-behind-config-flags.md index 9cc5317..4e1ddff 100644 --- a/backlog/tasks/task-84 - Gate-feature-dependent-keybindings-behind-config-flags.md +++ b/backlog/tasks/task-84 - Gate-feature-dependent-keybindings-behind-config-flags.md @@ -1,9 +1,11 @@ --- id: TASK-84 title: Gate feature-dependent keybindings behind config flags -status: To Do -assignee: [] +status: Done +assignee: + - opencode-task84-keybindings-gating created_date: '2026-02-19 08:41' +updated_date: '2026-02-22 01:35' labels: [] dependencies: [] priority: medium @@ -17,9 +19,43 @@ Ensure feature-specific keybindings only work when their related feature is enab ## Acceptance Criteria -- [ ] #1 Feature-dependent keybindings are effectively disabled when their corresponding feature flag/config is off, with no user-facing error dialogs. -- [ ] #2 When a feature is disabled in config, its related integration code is not loaded or initialized during startup (including Jellyfin as a concrete case). -- [ ] #3 When a feature is enabled, existing keybinding behavior continues to work as expected. -- [ ] #4 Automated tests cover enabled and disabled paths for keybinding gating and disabled-integration loading behavior. -- [ ] #5 User-facing docs/config guidance explain that feature keybindings require the corresponding feature to be enabled. +- [x] #1 Feature-dependent keybindings are effectively disabled when their corresponding feature flag/config is off, with no user-facing error dialogs. +- [x] #2 When a feature is disabled in config, its related integration code is not loaded or initialized during startup (including Jellyfin as a concrete case). +- [x] #3 When a feature is enabled, existing keybinding behavior continues to work as expected. +- [x] #4 Automated tests cover enabled and disabled paths for keybinding gating and disabled-integration loading behavior. +- [x] #5 User-facing docs/config guidance explain that feature keybindings require the corresponding feature to be enabled. + +## Implementation Plan + + +1) Add feature-gated shortcut resolution in `src/core/utils/shortcut-config.ts` so Anki-dependent shortcuts resolve to `null` when `ankiConnect.enabled` is false, with focused tests in `src/core/utils/shortcut-config.test.ts`. +2) Gate Jellyfin startup integration in `src/main/runtime/jellyfin-remote-session-lifecycle.ts` and startup warmup wiring in `src/main.ts` so remote session initialization requires `jellyfin.enabled`, `jellyfin.remoteControlEnabled`, and `jellyfin.remoteControlAutoConnect`. +3) Expand automated coverage for enabled/disabled paths (shortcut gating + Jellyfin startup no-op), then include new shortcut gating tests in `test:core:src`. +4) Update `docs/configuration.md` to clarify feature-dependent shortcuts require enabled features and Jellyfin remote autoconnect prerequisites. +5) Run verification (`bun test` focused suites, `bun run test:core:src`, `bun run docs:build`) and finalize TASK-84 notes/AC/final summary (no commit). + + +## Implementation Notes + + +Plan captured in docs/plans/2026-02-22-task-84-feature-keybinding-gates.md. Proceeding immediately with execution via executing-plans flow per user request. + +Implemented feature-dependent shortcut gating in `src/core/utils/shortcut-config.ts`: when `ankiConnect.enabled` is false, Anki/Kiku-dependent shortcuts resolve to `null` (`updateLastCardFromClipboard`, `triggerFieldGrouping`, `mineSentence`, `mineSentenceMultiple`, `markAudioCard`). + +Added focused shortcut gating coverage in `src/core/utils/shortcut-config.test.ts` (disabled + enabled + normalization fallback), and wired this file into `test:core:src` in `package.json`. + +Added disabled-integration startup guards for Jellyfin remote initialization: `createStartJellyfinRemoteSessionHandler` now early-returns when `jellyfin.enabled` is false, and startup warmup predicate in `src/main.ts` now requires `enabled && remoteControlEnabled && remoteControlAutoConnect`. + +Extended Jellyfin startup tests in `src/main/runtime/jellyfin-remote-session-lifecycle.test.ts` and `src/main/runtime/startup-warmups.test.ts` to cover disabled and fully-enabled paths. + +Updated docs in `docs/configuration.md` to clarify feature-dependent shortcuts require enabled integrations and Jellyfin remote autoconnect prerequisites. + +Verification passed: `bun test src/core/utils/shortcut-config.test.ts src/main/runtime/jellyfin-remote-session-lifecycle.test.ts src/main/runtime/startup-warmups.test.ts src/core/services/overlay-shortcut-handler.test.ts`, `bun run test:core:src`, and `bun run docs:build`. + + +## Final Summary + + +Implemented config-driven feature gates for shortcuts and integration startup so feature-dependent behavior is disabled cleanly when the feature is off. Anki/Kiku-dependent overlay shortcuts now resolve to null when `ankiConnect.enabled` is false, Jellyfin remote startup now requires `jellyfin.enabled` in addition to remote-control flags, focused automated tests cover enabled/disabled paths, and configuration docs now explicitly describe these feature prerequisites. + diff --git a/docs/configuration.md b/docs/configuration.md index 52b5b4b..d532798 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -493,26 +493,26 @@ Jellyfin integration is optional and disabled by default. When enabled, SubMiner } ``` -| Option | Values | Description | -| -------------------------- | --------------- | --------------------------------------------------------------------------------------- | -| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) | -| `serverUrl` | string (URL) | Jellyfin server base URL | -| `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`) | -| `clientName` | string | Client name sent in auth headers (default: `SubMiner`) | -| `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) | -| `defaultLibraryId` | string | Default library id for `--jellyfin-items` when CLI value is omitted | -| `remoteControlEnabled` | `true`, `false` | Enable Jellyfin cast/remote-control session support | -| `remoteControlAutoConnect` | `true`, `false` | Auto-connect Jellyfin remote session on app startup | -| `autoAnnounce` | `true`, `false` | Auto-run cast-target visibility announce check on connect (default: `false`) | -| `remoteControlDeviceName` | string | Device name shown in Jellyfin cast/device lists | -| `pullPictures` | `true`, `false` | Enable poster/icon fetching for launcher Jellyfin pickers | -| `iconCacheDir` | string | Cache directory for launcher-fetched Jellyfin poster icons | -| `directPlayPreferred` | `true`, `false` | Prefer direct stream URLs before transcoding | -| `directPlayContainers` | string[] | Container allowlist for direct play decisions | -| `transcodeVideoCodec` | string | Preferred transcode video codec fallback (default: `h264`) | +| Option | Values | Description | +| -------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------ | +| `enabled` | `true`, `false` | Enable Jellyfin integration and CLI commands (default: `false`) | +| `serverUrl` | string (URL) | Jellyfin server base URL | +| `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`) | +| `clientName` | string | Client name sent in auth headers (default: `SubMiner`) | +| `clientVersion` | string | Client version sent in auth headers (default: `0.1.0`) | +| `defaultLibraryId` | string | Default library id for `--jellyfin-items` when CLI value is omitted | +| `remoteControlEnabled` | `true`, `false` | Enable Jellyfin cast/remote-control session support | +| `remoteControlAutoConnect` | `true`, `false` | Auto-connect Jellyfin remote session on app startup (requires `jellyfin.enabled` and `remoteControlEnabled`) | +| `autoAnnounce` | `true`, `false` | Auto-run cast-target visibility announce check on connect (default: `false`) | +| `remoteControlDeviceName` | string | Device name shown in Jellyfin cast/device lists | +| `pullPictures` | `true`, `false` | Enable poster/icon fetching for launcher Jellyfin pickers | +| `iconCacheDir` | string | Cache directory for launcher-fetched Jellyfin poster icons | +| `directPlayPreferred` | `true`, `false` | Prefer direct stream URLs before transcoding | +| `directPlayContainers` | string[] | Container allowlist for direct play decisions | +| `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. @@ -534,6 +534,8 @@ Launcher subcommand equivalents: - `subminer jellyfin -p` opens play picker. - `subminer jellyfin -d` starts cast discovery mode. +Jellyfin remote auto-connect runs only when all three are `true`: `jellyfin.enabled`, `jellyfin.remoteControlEnabled`, and `jellyfin.remoteControlAutoConnect`. + ### Keybindings Add a `keybindings` array to configure keyboard shortcuts that send commands to mpv: @@ -679,6 +681,8 @@ See `config.example.jsonc` for detailed configuration options. Set any shortcut to `null` to disable it. +Feature-dependent shortcuts/keybindings only run when their related integration is enabled. For example, Anki/Kiku shortcuts require `ankiConnect.enabled` (and Kiku-specific behavior where applicable), and Jellyfin remote startup behavior requires Jellyfin to be enabled. + ### Subtitle Position Set the initial vertical subtitle position (measured from the bottom of the screen): diff --git a/package.json b/package.json index 455fa4b..13f4dad 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test:config:smoke:dist": "node --test dist/config/path-resolution.test.js", "test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts", "test:launcher:src": "bun test launcher/config.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts", - "test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/renderer/error-recovery.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts", + "test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/renderer/error-recovery.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts", "test:core:dist": "node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js", "test:core:smoke:dist": "node --test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js", "test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist", diff --git a/src/core/utils/shortcut-config.test.ts b/src/core/utils/shortcut-config.test.ts new file mode 100644 index 0000000..9ac47a7 --- /dev/null +++ b/src/core/utils/shortcut-config.test.ts @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import type { Config } from '../../types'; +import { resolveConfiguredShortcuts } from './shortcut-config'; + +test('forces Anki-dependent shortcuts to null when AnkiConnect is explicitly disabled', () => { + const config: Config = { + ankiConnect: { enabled: false }, + shortcuts: { + copySubtitle: 'Ctrl+KeyC', + updateLastCardFromClipboard: 'Ctrl+KeyU', + triggerFieldGrouping: 'Alt+KeyG', + mineSentence: 'Ctrl+Digit1', + mineSentenceMultiple: 'Ctrl+Digit2', + markAudioCard: 'Alt+KeyM', + }, + }; + const defaults: Config = { + shortcuts: { + updateLastCardFromClipboard: 'Alt+KeyL', + triggerFieldGrouping: 'Alt+KeyF', + mineSentence: 'KeyQ', + mineSentenceMultiple: 'KeyW', + markAudioCard: 'KeyE', + }, + }; + + const resolved = resolveConfiguredShortcuts(config, defaults); + + assert.equal(resolved.updateLastCardFromClipboard, null); + assert.equal(resolved.triggerFieldGrouping, null); + assert.equal(resolved.mineSentence, null); + assert.equal(resolved.mineSentenceMultiple, null); + assert.equal(resolved.markAudioCard, null); + assert.equal(resolved.copySubtitle, 'Ctrl+C'); +}); + +test('keeps Anki-dependent shortcuts enabled and normalized when AnkiConnect is enabled', () => { + const config: Config = { + ankiConnect: { enabled: true }, + shortcuts: { + updateLastCardFromClipboard: 'Ctrl+KeyU', + mineSentence: 'Ctrl+Digit1', + mineSentenceMultiple: 'Ctrl+Digit2', + }, + }; + const defaults: Config = { + shortcuts: { + triggerFieldGrouping: 'Alt+KeyG', + markAudioCard: 'Alt+KeyM', + }, + }; + + const resolved = resolveConfiguredShortcuts(config, defaults); + + assert.equal(resolved.updateLastCardFromClipboard, 'Ctrl+U'); + assert.equal(resolved.triggerFieldGrouping, 'Alt+G'); + assert.equal(resolved.mineSentence, 'Ctrl+1'); + assert.equal(resolved.mineSentenceMultiple, 'Ctrl+2'); + assert.equal(resolved.markAudioCard, 'Alt+M'); +}); + +test('normalizes fallback shortcuts when AnkiConnect flag is unset', () => { + const config: Config = {}; + const defaults: Config = { + shortcuts: { + mineSentence: 'KeyQ', + openRuntimeOptions: 'Digit9', + }, + }; + + const resolved = resolveConfiguredShortcuts(config, defaults); + + assert.equal(resolved.mineSentence, 'Q'); + assert.equal(resolved.openRuntimeOptions, '9'); +}); diff --git a/src/core/utils/shortcut-config.ts b/src/core/utils/shortcut-config.ts index 4eea382..f7e7c5b 100644 --- a/src/core/utils/shortcut-config.ts +++ b/src/core/utils/shortcut-config.ts @@ -21,6 +21,8 @@ export function resolveConfiguredShortcuts( config: Config, defaultConfig: Config, ): ConfiguredShortcuts { + const isAnkiConnectDisabled = config.ankiConnect?.enabled === false; + const normalizeShortcut = (value: string | null | undefined): string | null | undefined => { if (typeof value !== 'string') return value; return value.replace(/\bKey([A-Z])\b/g, '$1').replace(/\bDigit([0-9])\b/g, '$1'); @@ -42,20 +44,28 @@ export function resolveConfiguredShortcuts( config.shortcuts?.copySubtitleMultiple ?? defaultConfig.shortcuts?.copySubtitleMultiple, ), updateLastCardFromClipboard: normalizeShortcut( - config.shortcuts?.updateLastCardFromClipboard ?? - defaultConfig.shortcuts?.updateLastCardFromClipboard, + isAnkiConnectDisabled + ? null + : (config.shortcuts?.updateLastCardFromClipboard ?? + defaultConfig.shortcuts?.updateLastCardFromClipboard), ), triggerFieldGrouping: normalizeShortcut( - config.shortcuts?.triggerFieldGrouping ?? defaultConfig.shortcuts?.triggerFieldGrouping, + isAnkiConnectDisabled + ? null + : (config.shortcuts?.triggerFieldGrouping ?? defaultConfig.shortcuts?.triggerFieldGrouping), ), triggerSubsync: normalizeShortcut( config.shortcuts?.triggerSubsync ?? defaultConfig.shortcuts?.triggerSubsync, ), mineSentence: normalizeShortcut( - config.shortcuts?.mineSentence ?? defaultConfig.shortcuts?.mineSentence, + isAnkiConnectDisabled + ? null + : (config.shortcuts?.mineSentence ?? defaultConfig.shortcuts?.mineSentence), ), mineSentenceMultiple: normalizeShortcut( - config.shortcuts?.mineSentenceMultiple ?? defaultConfig.shortcuts?.mineSentenceMultiple, + isAnkiConnectDisabled + ? null + : (config.shortcuts?.mineSentenceMultiple ?? defaultConfig.shortcuts?.mineSentenceMultiple), ), multiCopyTimeoutMs: config.shortcuts?.multiCopyTimeoutMs ?? defaultConfig.shortcuts?.multiCopyTimeoutMs ?? 5000, @@ -63,7 +73,9 @@ export function resolveConfiguredShortcuts( config.shortcuts?.toggleSecondarySub ?? defaultConfig.shortcuts?.toggleSecondarySub, ), markAudioCard: normalizeShortcut( - config.shortcuts?.markAudioCard ?? defaultConfig.shortcuts?.markAudioCard, + isAnkiConnectDisabled + ? null + : (config.shortcuts?.markAudioCard ?? defaultConfig.shortcuts?.markAudioCard), ), openRuntimeOptions: normalizeShortcut( config.shortcuts?.openRuntimeOptions ?? defaultConfig.shortcuts?.openRuntimeOptions, diff --git a/src/main.ts b/src/main.ts index e5e92be..2a1beaa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2233,7 +2233,12 @@ const { }, isTexthookerOnlyMode: () => appState.texthookerOnlyMode, ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}), - shouldAutoConnectJellyfinRemote: () => getResolvedConfig().jellyfin.remoteControlAutoConnect, + shouldAutoConnectJellyfinRemote: () => { + const jellyfin = getResolvedConfig().jellyfin; + return ( + jellyfin.enabled && jellyfin.remoteControlEnabled && jellyfin.remoteControlAutoConnect + ); + }, startJellyfinRemoteSession: () => startJellyfinRemoteSession(), }, }, diff --git a/src/main/runtime/jellyfin-remote-session-lifecycle.test.ts b/src/main/runtime/jellyfin-remote-session-lifecycle.test.ts index 15c03a6..ba82d46 100644 --- a/src/main/runtime/jellyfin-remote-session-lifecycle.test.ts +++ b/src/main/runtime/jellyfin-remote-session-lifecycle.test.ts @@ -7,6 +7,7 @@ import { function createConfig(overrides?: Partial>) { return { + enabled: true, remoteControlEnabled: true, remoteControlAutoConnect: true, serverUrl: 'http://localhost', @@ -21,6 +22,34 @@ function createConfig(overrides?: Partial>) { } as never; } +test('start handler no-ops when jellyfin integration is disabled', async () => { + let created = false; + const startRemote = createStartJellyfinRemoteSessionHandler({ + getJellyfinConfig: () => createConfig({ enabled: false }), + getCurrentSession: () => null, + setCurrentSession: () => {}, + createRemoteSessionService: () => { + created = true; + return { + start: () => {}, + stop: () => {}, + advertiseNow: async () => true, + }; + }, + defaultDeviceId: 'default-device', + defaultClientName: 'SubMiner', + defaultClientVersion: '1.0', + handlePlay: async () => {}, + handlePlaystate: async () => {}, + handleGeneralCommand: async () => {}, + logInfo: () => {}, + logWarn: () => {}, + }); + + await startRemote(); + assert.equal(created, false); +}); + test('start handler no-ops when remote control is disabled', async () => { let created = false; const startRemote = createStartJellyfinRemoteSessionHandler({ @@ -50,8 +79,11 @@ test('start handler no-ops when remote control is disabled', async () => { }); test('start handler creates, starts, and stores session', async () => { - let storedSession: { start: () => void; stop: () => void; advertiseNow: () => Promise } | null = - null; + let storedSession: { + start: () => void; + stop: () => void; + advertiseNow: () => Promise; + } | null = null; let started = false; const infos: string[] = []; const startRemote = createStartJellyfinRemoteSessionHandler({ diff --git a/src/main/runtime/jellyfin-remote-session-lifecycle.ts b/src/main/runtime/jellyfin-remote-session-lifecycle.ts index c8d6060..adc0710 100644 --- a/src/main/runtime/jellyfin-remote-session-lifecycle.ts +++ b/src/main/runtime/jellyfin-remote-session-lifecycle.ts @@ -1,4 +1,5 @@ type JellyfinRemoteConfig = { + enabled: boolean; remoteControlEnabled: boolean; remoteControlAutoConnect: boolean; serverUrl: string; @@ -54,6 +55,7 @@ export function createStartJellyfinRemoteSessionHandler(deps: { }) { return async (): Promise => { const jellyfinConfig = deps.getJellyfinConfig(); + if (jellyfinConfig.enabled === false) return; if (jellyfinConfig.remoteControlEnabled === false) return; if (jellyfinConfig.remoteControlAutoConnect === false) return; if (!jellyfinConfig.serverUrl || !jellyfinConfig.accessToken || !jellyfinConfig.userId) return; @@ -87,7 +89,9 @@ export function createStartJellyfinRemoteSessionHandler(deps: { if (registered) { deps.logInfo('Jellyfin cast target is visible to server sessions.'); } else { - deps.logWarn('Jellyfin remote connected but device not visible in server sessions yet.'); + deps.logWarn( + 'Jellyfin remote connected but device not visible in server sessions yet.', + ); } }); } diff --git a/src/main/runtime/startup-warmups.test.ts b/src/main/runtime/startup-warmups.test.ts index e0cce03..3d30162 100644 --- a/src/main/runtime/startup-warmups.test.ts +++ b/src/main/runtime/startup-warmups.test.ts @@ -5,6 +5,14 @@ import { createStartBackgroundWarmupsHandler, } from './startup-warmups'; +function shouldAutoConnectJellyfinRemote(config: { + enabled: boolean; + remoteControlEnabled: boolean; + remoteControlAutoConnect: boolean; +}): boolean { + return config.enabled && config.remoteControlEnabled && config.remoteControlAutoConnect; +} + test('launchBackgroundWarmupTask logs completion timing', async () => { const debugLogs: string[] = []; const launchTask = createLaunchBackgroundWarmupTaskHandler({ @@ -41,7 +49,7 @@ test('startBackgroundWarmups no-ops when already started', () => { assert.equal(launches, 0); }); -test('startBackgroundWarmups schedules base warmups and optional jellyfin warmup', () => { +test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.enabled is false', () => { const labels: string[] = []; let started = false; const startWarmups = createStartBackgroundWarmupsHandler({ @@ -56,7 +64,41 @@ test('startBackgroundWarmups schedules base warmups and optional jellyfin warmup createMecabTokenizerAndCheck: async () => {}, ensureYomitanExtensionLoaded: async () => {}, prewarmSubtitleDictionaries: async () => {}, - shouldAutoConnectJellyfinRemote: () => true, + shouldAutoConnectJellyfinRemote: () => + shouldAutoConnectJellyfinRemote({ + enabled: false, + remoteControlEnabled: true, + remoteControlAutoConnect: true, + }), + startJellyfinRemoteSession: async () => {}, + }); + + startWarmups(); + assert.equal(started, true); + assert.deepEqual(labels, ['mecab', 'yomitan-extension', 'subtitle-dictionaries']); +}); + +test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags are enabled', () => { + const labels: string[] = []; + let started = false; + const startWarmups = createStartBackgroundWarmupsHandler({ + getStarted: () => started, + setStarted: (value) => { + started = value; + }, + isTexthookerOnlyMode: () => false, + launchTask: (label) => { + labels.push(label); + }, + createMecabTokenizerAndCheck: async () => {}, + ensureYomitanExtensionLoaded: async () => {}, + prewarmSubtitleDictionaries: async () => {}, + shouldAutoConnectJellyfinRemote: () => + shouldAutoConnectJellyfinRemote({ + enabled: true, + remoteControlEnabled: true, + remoteControlAutoConnect: true, + }), startJellyfinRemoteSession: async () => {}, });