From 1d67b1202818288b17fcb7f494c4530c98a59d36 Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 27 Feb 2026 21:06:12 -0800 Subject: [PATCH] feat: make startup warmups configurable with low-power mode --- ...ble-warmup-vs-defer-with-low-power-mode.md | 51 +++++++++++++ src/config/config.test.ts | 72 +++++++++++++++++++ src/config/definitions.ts | 2 + src/config/definitions/defaults-core.ts | 8 +++ .../definitions/domain-registry.test.ts | 2 + src/config/definitions/options-core.ts | 30 ++++++++ src/config/definitions/template-sections.ts | 9 +++ src/config/resolve/core-domains.ts | 24 +++++++ src/main.ts | 27 +++++-- .../composers/mpv-runtime-composer.test.ts | 20 +++++- .../runtime/composers/mpv-runtime-composer.ts | 4 ++ .../runtime/startup-warmups-main-deps.test.ts | 8 +++ src/main/runtime/startup-warmups-main-deps.ts | 4 ++ src/main/runtime/startup-warmups.test.ts | 51 ++++++++++++- src/main/runtime/startup-warmups.ts | 30 +++++--- src/types.ts | 16 +++++ 16 files changed, 338 insertions(+), 20 deletions(-) create mode 100644 backlog/tasks/task-74 - Startup-warmups-configurable-warmup-vs-defer-with-low-power-mode.md diff --git a/backlog/tasks/task-74 - Startup-warmups-configurable-warmup-vs-defer-with-low-power-mode.md b/backlog/tasks/task-74 - Startup-warmups-configurable-warmup-vs-defer-with-low-power-mode.md new file mode 100644 index 0000000..0f22df7 --- /dev/null +++ b/backlog/tasks/task-74 - Startup-warmups-configurable-warmup-vs-defer-with-low-power-mode.md @@ -0,0 +1,51 @@ +--- +id: TASK-74 +title: 'Startup warmups: configurable warmup vs defer with low-power mode' +status: In Progress +assignee: [] +created_date: '2026-02-27 21:05' +labels: [] +dependencies: [] +references: + - src/types.ts + - src/config/definitions/defaults-core.ts + - src/config/definitions/options-core.ts + - src/config/definitions/template-sections.ts + - src/config/resolve/core-domains.ts + - src/main/runtime/startup-warmups.ts + - src/main/runtime/startup-warmups-main-deps.ts + - src/main/runtime/composers/mpv-runtime-composer.ts + - src/main.ts + - src/config/config.test.ts + - src/main/runtime/startup-warmups.test.ts +priority: medium +--- + +## Description + + +Add startup warmup controls to allow per-integration warmup or deferred first-use loading. + +Scope: +- New config section `startupWarmups` with toggles for `mecab`, `yomitanExtension`, `subtitleDictionaries`, and `jellyfinRemoteSession`. +- New `startupWarmups.lowPowerMode` policy: defer everything except Yomitan extension. +- Keep default behavior as full warmup. +- Ensure deferred integrations lazy-load on first real usage path. +- Add test coverage for config parsing/defaults and warmup scheduling behavior. + + +## Final Summary + + +Implemented: +- Added `startupWarmups` to config types/defaults/options/template/resolve. +- Warmup scheduler now uses per-integration gating functions. +- Low-power mode now defers MeCab, subtitle dictionaries, and Jellyfin remote session warmups while still warming Yomitan extension. +- Tokenization path guarantees lazy first-use init for deferred dependencies (Yomitan extension, MeCab when missing, subtitle dictionaries). +- Added/updated tests across config and runtime warmup modules. + +Validation: +- `bun run test:config:src` +- `bun run test:core:src` +- `tsc --noEmit` + diff --git a/src/config/config.test.ts b/src/config/config.test.ts index eef58e3..5753c14 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -23,6 +23,11 @@ test('loads defaults when config is missing', () => { assert.equal(config.jellyfin.remoteControlAutoConnect, true); assert.equal(config.jellyfin.autoAnnounce, false); assert.equal(config.jellyfin.remoteControlDeviceName, 'SubMiner'); + assert.equal(config.startupWarmups.lowPowerMode, false); + assert.equal(config.startupWarmups.mecab, true); + assert.equal(config.startupWarmups.yomitanExtension, true); + assert.equal(config.startupWarmups.subtitleDictionaries, true); + assert.equal(config.startupWarmups.jellyfinRemoteSession, true); assert.equal(config.discordPresence.enabled, false); assert.equal(config.discordPresence.updateIntervalMs, 3_000); assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)'); @@ -294,6 +299,72 @@ test('parses jellyfin.enabled and remoteControlEnabled disabled combinations', ( ); }); +test('parses startup warmup toggles and low-power mode', () => { + const dir = makeTempDir(); + fs.writeFileSync( + path.join(dir, 'config.jsonc'), + `{ + "startupWarmups": { + "lowPowerMode": true, + "mecab": false, + "yomitanExtension": true, + "subtitleDictionaries": false, + "jellyfinRemoteSession": false + } + }`, + 'utf-8', + ); + + const service = new ConfigService(dir); + const config = service.getConfig(); + assert.equal(config.startupWarmups.lowPowerMode, true); + assert.equal(config.startupWarmups.mecab, false); + assert.equal(config.startupWarmups.yomitanExtension, true); + assert.equal(config.startupWarmups.subtitleDictionaries, false); + assert.equal(config.startupWarmups.jellyfinRemoteSession, false); +}); + +test('invalid startup warmup values warn and keep defaults', () => { + const dir = makeTempDir(); + fs.writeFileSync( + path.join(dir, 'config.jsonc'), + `{ + "startupWarmups": { + "lowPowerMode": "yes", + "mecab": 1, + "yomitanExtension": null, + "subtitleDictionaries": "no", + "jellyfinRemoteSession": [] + } + }`, + 'utf-8', + ); + + const service = new ConfigService(dir); + const config = service.getConfig(); + const warnings = service.getWarnings(); + + assert.equal(config.startupWarmups.lowPowerMode, DEFAULT_CONFIG.startupWarmups.lowPowerMode); + assert.equal(config.startupWarmups.mecab, DEFAULT_CONFIG.startupWarmups.mecab); + assert.equal( + config.startupWarmups.yomitanExtension, + DEFAULT_CONFIG.startupWarmups.yomitanExtension, + ); + assert.equal( + config.startupWarmups.subtitleDictionaries, + DEFAULT_CONFIG.startupWarmups.subtitleDictionaries, + ); + assert.equal( + config.startupWarmups.jellyfinRemoteSession, + DEFAULT_CONFIG.startupWarmups.jellyfinRemoteSession, + ); + assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.lowPowerMode')); + assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.mecab')); + assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.yomitanExtension')); + assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.subtitleDictionaries')); + assert.ok(warnings.some((warning) => warning.path === 'startupWarmups.jellyfinRemoteSession')); +}); + test('parses discordPresence fields and warns for invalid types', () => { const dir = makeTempDir(); fs.writeFileSync( @@ -1135,6 +1206,7 @@ test('template generator includes known keys', () => { assert.match(output, /"logging":/); assert.match(output, /"websocket":/); assert.match(output, /"discordPresence":/); + assert.match(output, /"startupWarmups":/); assert.match(output, /"youtubeSubgen":/); assert.match(output, /"preserveLineBreaks": false/); assert.match(output, /"nPlusOne"\s*:\s*\{/); diff --git a/src/config/definitions.ts b/src/config/definitions.ts index 09f05d5..b9a08b4 100644 --- a/src/config/definitions.ts +++ b/src/config/definitions.ts @@ -27,6 +27,7 @@ const { shortcuts, secondarySub, subsync, + startupWarmups, auto_start_overlay, } = CORE_DEFAULT_CONFIG; const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } = @@ -44,6 +45,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = { shortcuts, secondarySub, subsync, + startupWarmups, subtitleStyle, auto_start_overlay, jimaku, diff --git a/src/config/definitions/defaults-core.ts b/src/config/definitions/defaults-core.ts index c79dc22..c0b434c 100644 --- a/src/config/definitions/defaults-core.ts +++ b/src/config/definitions/defaults-core.ts @@ -10,6 +10,7 @@ export const CORE_DEFAULT_CONFIG: Pick< | 'shortcuts' | 'secondarySub' | 'subsync' + | 'startupWarmups' | 'auto_start_overlay' > = { subtitlePosition: { yPercent: 10 }, @@ -50,5 +51,12 @@ export const CORE_DEFAULT_CONFIG: Pick< ffsubsync_path: '', ffmpeg_path: '', }, + startupWarmups: { + lowPowerMode: false, + mecab: true, + yomitanExtension: true, + subtitleDictionaries: true, + jellyfinRemoteSession: true, + }, auto_start_overlay: false, }; diff --git a/src/config/definitions/domain-registry.test.ts b/src/config/definitions/domain-registry.test.ts index 40f32ac..254e41f 100644 --- a/src/config/definitions/domain-registry.test.ts +++ b/src/config/definitions/domain-registry.test.ts @@ -17,6 +17,7 @@ test('config option registry includes critical paths and has unique entries', () for (const requiredPath of [ 'logging.level', + 'startupWarmups.lowPowerMode', 'subtitleStyle.enableJlpt', 'ankiConnect.enabled', 'immersionTracking.enabled', @@ -31,6 +32,7 @@ test('config template sections include expected domains and unique keys', () => const keys = CONFIG_TEMPLATE_SECTIONS.map((section) => section.key); const requiredKeys: (typeof keys)[number][] = [ 'websocket', + 'startupWarmups', 'subtitleStyle', 'ankiConnect', 'immersionTracking', diff --git a/src/config/definitions/options-core.ts b/src/config/definitions/options-core.ts index 4a611a3..4b55199 100644 --- a/src/config/definitions/options-core.ts +++ b/src/config/definitions/options-core.ts @@ -32,6 +32,36 @@ export function buildCoreConfigOptionRegistry( defaultValue: defaultConfig.subsync.defaultMode, description: 'Subsync default mode.', }, + { + path: 'startupWarmups.lowPowerMode', + kind: 'boolean', + defaultValue: defaultConfig.startupWarmups.lowPowerMode, + description: 'Defer startup warmups except Yomitan extension.', + }, + { + path: 'startupWarmups.mecab', + kind: 'boolean', + defaultValue: defaultConfig.startupWarmups.mecab, + description: 'Warm up MeCab tokenizer at startup.', + }, + { + path: 'startupWarmups.yomitanExtension', + kind: 'boolean', + defaultValue: defaultConfig.startupWarmups.yomitanExtension, + description: 'Warm up Yomitan extension at startup.', + }, + { + path: 'startupWarmups.subtitleDictionaries', + kind: 'boolean', + defaultValue: defaultConfig.startupWarmups.subtitleDictionaries, + description: 'Warm up subtitle dictionaries at startup.', + }, + { + path: 'startupWarmups.jellyfinRemoteSession', + kind: 'boolean', + defaultValue: defaultConfig.startupWarmups.jellyfinRemoteSession, + description: 'Warm up Jellyfin remote session at startup.', + }, { path: 'shortcuts.multiCopyTimeoutMs', kind: 'number', diff --git a/src/config/definitions/template-sections.ts b/src/config/definitions/template-sections.ts index 9e69e24..78d202e 100644 --- a/src/config/definitions/template-sections.ts +++ b/src/config/definitions/template-sections.ts @@ -26,6 +26,15 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [ description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'], key: 'logging', }, + { + title: 'Startup Warmups', + description: [ + 'Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.', + 'Disable individual warmups to defer load until first real usage.', + 'lowPowerMode defers all warmups except Yomitan extension.', + ], + key: 'startupWarmups', + }, { title: 'Keyboard Shortcuts', description: ['Overlay keyboard shortcuts. Set a shortcut to null to disable.'], diff --git a/src/config/resolve/core-domains.ts b/src/config/resolve/core-domains.ts index ab03386..41c6ca9 100644 --- a/src/config/resolve/core-domains.ts +++ b/src/config/resolve/core-domains.ts @@ -74,6 +74,30 @@ export function applyCoreDomainConfig(context: ResolveContext): void { ); } + if (isObject(src.startupWarmups)) { + const startupWarmupBooleanKeys = [ + 'lowPowerMode', + 'mecab', + 'yomitanExtension', + 'subtitleDictionaries', + 'jellyfinRemoteSession', + ] as const; + + for (const key of startupWarmupBooleanKeys) { + const value = asBoolean(src.startupWarmups[key]); + if (value !== undefined) { + resolved.startupWarmups[key] = value as (typeof resolved.startupWarmups)[typeof key]; + } else if (src.startupWarmups[key] !== undefined) { + warn( + `startupWarmups.${key}`, + src.startupWarmups[key], + resolved.startupWarmups[key], + 'Expected boolean.', + ); + } + } + } + if (isObject(src.shortcuts)) { const shortcutKeys = [ 'toggleVisibleOverlayGlobal', diff --git a/src/main.ts b/src/main.ts index 211b130..aa86595 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1936,10 +1936,6 @@ const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppR logInfo: (message) => appLogger.logInfo(message), logWarning: (message) => appLogger.logWarning(message), showDesktopNotification: (title, options) => showDesktopNotification(title, options), - showConfigWarningsDialog: - process.platform === 'darwin' - ? (title, details) => dialog.showErrorBox(title, details) - : undefined, startConfigHotReload: () => configHotReloadRuntime.start(), refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options), failHandlers: { @@ -2250,6 +2246,7 @@ const { getKnownWordMatchMode: () => appState.ankiIntegration?.getKnownWordMatchMode() ?? getResolvedConfig().ankiConnect.nPlusOne.matchMode, + getNPlusOneEnabled: () => getResolvedConfig().ankiConnect.nPlusOne.highlightEnabled, getMinSentenceWordsForNPlusOne: () => getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords, getJlptLevel: (text) => appState.jlptLevelLookup(text), @@ -2290,6 +2287,28 @@ const { }, isTexthookerOnlyMode: () => appState.texthookerOnlyMode, ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}), + shouldWarmupMecab: () => { + const startupWarmups = getResolvedConfig().startupWarmups; + if (startupWarmups.lowPowerMode) { + return false; + } + return startupWarmups.mecab; + }, + shouldWarmupYomitanExtension: () => getResolvedConfig().startupWarmups.yomitanExtension, + shouldWarmupSubtitleDictionaries: () => { + const startupWarmups = getResolvedConfig().startupWarmups; + if (startupWarmups.lowPowerMode) { + return false; + } + return startupWarmups.subtitleDictionaries; + }, + shouldWarmupJellyfinRemoteSession: () => { + const startupWarmups = getResolvedConfig().startupWarmups; + if (startupWarmups.lowPowerMode) { + return false; + } + return startupWarmups.jellyfinRemoteSession; + }, shouldAutoConnectJellyfinRemote: () => { const jellyfin = getResolvedConfig().jellyfin; return ( diff --git a/src/main/runtime/composers/mpv-runtime-composer.test.ts b/src/main/runtime/composers/mpv-runtime-composer.test.ts index 9cf3d59..6309051 100644 --- a/src/main/runtime/composers/mpv-runtime-composer.test.ts +++ b/src/main/runtime/composers/mpv-runtime-composer.test.ts @@ -26,6 +26,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject const calls: string[] = []; let started = false; let metrics = BASE_METRICS; + let mecabTokenizer: { id: string } | null = null; class FakeMpvClient { connected = false; @@ -140,9 +141,15 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject return { text }; }, createMecabTokenizerAndCheckMainDeps: { - getMecabTokenizer: () => ({ id: 'mecab' }), - setMecabTokenizer: () => {}, - createMecabTokenizer: () => ({ id: 'mecab' }), + getMecabTokenizer: () => mecabTokenizer, + setMecabTokenizer: (next) => { + mecabTokenizer = next as { id: string }; + calls.push('set-mecab'); + }, + createMecabTokenizer: () => { + calls.push('create-mecab'); + return { id: 'mecab' }; + }, checkAvailability: async () => { calls.push('check-mecab'); }, @@ -176,6 +183,10 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject ensureYomitanExtensionLoaded: async () => { calls.push('warmup-yomitan'); }, + shouldWarmupMecab: () => true, + shouldWarmupYomitanExtension: () => true, + shouldWarmupSubtitleDictionaries: () => true, + shouldWarmupJellyfinRemoteSession: () => true, shouldAutoConnectJellyfinRemote: () => false, startJellyfinRemoteSession: async () => { calls.push('warmup-jellyfin'); @@ -212,9 +223,12 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject assert.ok(calls.includes('broadcast-metrics')); assert.ok(calls.includes('create-tokenizer-runtime-deps')); assert.ok(calls.includes('tokenize:subtitle text')); + assert.ok(calls.includes('create-mecab')); + assert.ok(calls.includes('set-mecab')); assert.ok(calls.includes('check-mecab')); assert.ok(calls.includes('prewarm-jlpt')); assert.ok(calls.includes('prewarm-frequency')); assert.ok(calls.includes('set-started:true')); assert.ok(calls.includes('warmup-yomitan')); + assert.ok(calls.indexOf('create-mecab') < calls.indexOf('set-started:true')); }); diff --git a/src/main/runtime/composers/mpv-runtime-composer.ts b/src/main/runtime/composers/mpv-runtime-composer.ts index 9093d6e..d825de7 100644 --- a/src/main/runtime/composers/mpv-runtime-composer.ts +++ b/src/main/runtime/composers/mpv-runtime-composer.ts @@ -133,6 +133,10 @@ export function composeMpvRuntimeHandlers< options.tokenizer.prewarmSubtitleDictionariesMainDeps, ); const tokenizeSubtitle = async (text: string): Promise => { + await options.warmups.startBackgroundWarmupsMainDeps.ensureYomitanExtensionLoaded(); + if (!options.tokenizer.createMecabTokenizerAndCheckMainDeps.getMecabTokenizer()) { + await createMecabTokenizerAndCheck().catch(() => {}); + } await prewarmSubtitleDictionaries(); return options.tokenizer.tokenizeSubtitle( text, diff --git a/src/main/runtime/startup-warmups-main-deps.test.ts b/src/main/runtime/startup-warmups-main-deps.test.ts index aca4ba8..2825e2f 100644 --- a/src/main/runtime/startup-warmups-main-deps.test.ts +++ b/src/main/runtime/startup-warmups-main-deps.test.ts @@ -34,6 +34,10 @@ test('startup warmups main deps builders map callbacks', async () => { prewarmSubtitleDictionaries: async () => { calls.push('dict'); }, + shouldWarmupMecab: () => false, + shouldWarmupYomitanExtension: () => true, + shouldWarmupSubtitleDictionaries: () => false, + shouldWarmupJellyfinRemoteSession: () => true, shouldAutoConnectJellyfinRemote: () => true, startJellyfinRemoteSession: async () => { calls.push('jellyfin'); @@ -48,6 +52,10 @@ test('startup warmups main deps builders map callbacks', async () => { await start.createMecabTokenizerAndCheck(); await start.ensureYomitanExtensionLoaded(); await start.prewarmSubtitleDictionaries(); + assert.equal(start.shouldWarmupMecab(), false); + assert.equal(start.shouldWarmupYomitanExtension(), true); + assert.equal(start.shouldWarmupSubtitleDictionaries(), false); + assert.equal(start.shouldWarmupJellyfinRemoteSession(), true); assert.equal(start.shouldAutoConnectJellyfinRemote(), true); await start.startJellyfinRemoteSession(); diff --git a/src/main/runtime/startup-warmups-main-deps.ts b/src/main/runtime/startup-warmups-main-deps.ts index 23795a4..5e4ff60 100644 --- a/src/main/runtime/startup-warmups-main-deps.ts +++ b/src/main/runtime/startup-warmups-main-deps.ts @@ -25,6 +25,10 @@ export function createBuildStartBackgroundWarmupsMainDepsHandler(deps: StartBack createMecabTokenizerAndCheck: () => deps.createMecabTokenizerAndCheck(), ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(), prewarmSubtitleDictionaries: () => deps.prewarmSubtitleDictionaries(), + shouldWarmupMecab: () => deps.shouldWarmupMecab(), + shouldWarmupYomitanExtension: () => deps.shouldWarmupYomitanExtension(), + shouldWarmupSubtitleDictionaries: () => deps.shouldWarmupSubtitleDictionaries(), + shouldWarmupJellyfinRemoteSession: () => deps.shouldWarmupJellyfinRemoteSession(), shouldAutoConnectJellyfinRemote: () => deps.shouldAutoConnectJellyfinRemote(), startJellyfinRemoteSession: () => deps.startJellyfinRemoteSession(), }); diff --git a/src/main/runtime/startup-warmups.test.ts b/src/main/runtime/startup-warmups.test.ts index 3d30162..5ab47e7 100644 --- a/src/main/runtime/startup-warmups.test.ts +++ b/src/main/runtime/startup-warmups.test.ts @@ -41,6 +41,10 @@ test('startBackgroundWarmups no-ops when already started', () => { createMecabTokenizerAndCheck: async () => {}, ensureYomitanExtensionLoaded: async () => {}, prewarmSubtitleDictionaries: async () => {}, + shouldWarmupMecab: () => true, + shouldWarmupYomitanExtension: () => true, + shouldWarmupSubtitleDictionaries: () => true, + shouldWarmupJellyfinRemoteSession: () => true, shouldAutoConnectJellyfinRemote: () => false, startJellyfinRemoteSession: async () => {}, }); @@ -49,7 +53,7 @@ test('startBackgroundWarmups no-ops when already started', () => { assert.equal(launches, 0); }); -test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.enabled is false', () => { +test('startBackgroundWarmups respects per-integration warmup toggles', () => { const labels: string[] = []; let started = false; const startWarmups = createStartBackgroundWarmupsHandler({ @@ -64,9 +68,13 @@ test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.ena createMecabTokenizerAndCheck: async () => {}, ensureYomitanExtensionLoaded: async () => {}, prewarmSubtitleDictionaries: async () => {}, + shouldWarmupMecab: () => false, + shouldWarmupYomitanExtension: () => true, + shouldWarmupSubtitleDictionaries: () => false, + shouldWarmupJellyfinRemoteSession: () => false, shouldAutoConnectJellyfinRemote: () => shouldAutoConnectJellyfinRemote({ - enabled: false, + enabled: true, remoteControlEnabled: true, remoteControlAutoConnect: true, }), @@ -75,7 +83,7 @@ test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.ena startWarmups(); assert.equal(started, true); - assert.deepEqual(labels, ['mecab', 'yomitan-extension', 'subtitle-dictionaries']); + assert.deepEqual(labels, ['yomitan-extension']); }); test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags are enabled', () => { @@ -93,6 +101,10 @@ test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags a createMecabTokenizerAndCheck: async () => {}, ensureYomitanExtensionLoaded: async () => {}, prewarmSubtitleDictionaries: async () => {}, + shouldWarmupMecab: () => true, + shouldWarmupYomitanExtension: () => true, + shouldWarmupSubtitleDictionaries: () => true, + shouldWarmupJellyfinRemoteSession: () => true, shouldAutoConnectJellyfinRemote: () => shouldAutoConnectJellyfinRemote({ enabled: true, @@ -111,3 +123,36 @@ test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags a 'jellyfin-remote-session', ]); }); + +test('startBackgroundWarmups skips jellyfin warmup when warmup is deferred', () => { + 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 () => {}, + shouldWarmupMecab: () => false, + shouldWarmupYomitanExtension: () => true, + shouldWarmupSubtitleDictionaries: () => false, + shouldWarmupJellyfinRemoteSession: () => false, + shouldAutoConnectJellyfinRemote: () => + shouldAutoConnectJellyfinRemote({ + enabled: true, + remoteControlEnabled: true, + remoteControlAutoConnect: true, + }), + startJellyfinRemoteSession: async () => {}, + }); + + startWarmups(); + assert.equal(started, true); + assert.deepEqual(labels, ['yomitan-extension']); +}); diff --git a/src/main/runtime/startup-warmups.ts b/src/main/runtime/startup-warmups.ts index 87996ac..c62509a 100644 --- a/src/main/runtime/startup-warmups.ts +++ b/src/main/runtime/startup-warmups.ts @@ -24,6 +24,10 @@ export function createStartBackgroundWarmupsHandler(deps: { createMecabTokenizerAndCheck: () => Promise; ensureYomitanExtensionLoaded: () => Promise; prewarmSubtitleDictionaries: () => Promise; + shouldWarmupMecab: () => boolean; + shouldWarmupYomitanExtension: () => boolean; + shouldWarmupSubtitleDictionaries: () => boolean; + shouldWarmupJellyfinRemoteSession: () => boolean; shouldAutoConnectJellyfinRemote: () => boolean; startJellyfinRemoteSession: () => Promise; }) { @@ -32,16 +36,22 @@ export function createStartBackgroundWarmupsHandler(deps: { if (deps.isTexthookerOnlyMode()) return; deps.setStarted(true); - deps.launchTask('mecab', async () => { - await deps.createMecabTokenizerAndCheck(); - }); - deps.launchTask('yomitan-extension', async () => { - await deps.ensureYomitanExtensionLoaded(); - }); - deps.launchTask('subtitle-dictionaries', async () => { - await deps.prewarmSubtitleDictionaries(); - }); - if (deps.shouldAutoConnectJellyfinRemote()) { + if (deps.shouldWarmupMecab()) { + deps.launchTask('mecab', async () => { + await deps.createMecabTokenizerAndCheck(); + }); + } + if (deps.shouldWarmupYomitanExtension()) { + deps.launchTask('yomitan-extension', async () => { + await deps.ensureYomitanExtensionLoaded(); + }); + } + if (deps.shouldWarmupSubtitleDictionaries()) { + deps.launchTask('subtitle-dictionaries', async () => { + await deps.prewarmSubtitleDictionaries(); + }); + } + if (deps.shouldWarmupJellyfinRemoteSession() && deps.shouldAutoConnectJellyfinRemote()) { deps.launchTask('jellyfin-remote-session', async () => { await deps.startJellyfinRemoteSession(); }); diff --git a/src/types.ts b/src/types.ts index 516f576..71350cb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -99,6 +99,14 @@ export interface SubsyncConfig { ffmpeg_path?: string; } +export interface StartupWarmupsConfig { + lowPowerMode?: boolean; + mecab?: boolean; + yomitanExtension?: boolean; + subtitleDictionaries?: boolean; + jellyfinRemoteSession?: boolean; +} + export interface WebSocketConfig { enabled?: boolean | 'auto'; port?: number; @@ -417,6 +425,7 @@ export interface Config { shortcuts?: ShortcutsConfig; secondarySub?: SecondarySubConfig; subsync?: SubsyncConfig; + startupWarmups?: StartupWarmupsConfig; subtitleStyle?: SubtitleStyleConfig; auto_start_overlay?: boolean; jimaku?: JimakuConfig; @@ -513,6 +522,13 @@ export interface ResolvedConfig { shortcuts: Required; secondarySub: Required; subsync: Required; + startupWarmups: { + lowPowerMode: boolean; + mecab: boolean; + yomitanExtension: boolean; + subtitleDictionaries: boolean; + jellyfinRemoteSession: boolean; + }; subtitleStyle: Required> & { secondary: Required>; frequencyDictionary: {