feat: make startup warmups configurable with low-power mode

This commit is contained in:
2026-02-27 21:06:12 -08:00
parent 3a1d746a2e
commit 1e645f961b
16 changed files with 338 additions and 20 deletions

View File

@@ -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
<!-- SECTION:DESCRIPTION:BEGIN -->
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.
<!-- SECTION:DESCRIPTION:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
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`
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -23,6 +23,11 @@ test('loads defaults when config is missing', () => {
assert.equal(config.jellyfin.remoteControlAutoConnect, true); assert.equal(config.jellyfin.remoteControlAutoConnect, true);
assert.equal(config.jellyfin.autoAnnounce, false); assert.equal(config.jellyfin.autoAnnounce, false);
assert.equal(config.jellyfin.remoteControlDeviceName, 'SubMiner'); 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.enabled, false);
assert.equal(config.discordPresence.updateIntervalMs, 3_000); assert.equal(config.discordPresence.updateIntervalMs, 3_000);
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)'); 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', () => { test('parses discordPresence fields and warns for invalid types', () => {
const dir = makeTempDir(); const dir = makeTempDir();
fs.writeFileSync( fs.writeFileSync(
@@ -1135,6 +1206,7 @@ test('template generator includes known keys', () => {
assert.match(output, /"logging":/); assert.match(output, /"logging":/);
assert.match(output, /"websocket":/); assert.match(output, /"websocket":/);
assert.match(output, /"discordPresence":/); assert.match(output, /"discordPresence":/);
assert.match(output, /"startupWarmups":/);
assert.match(output, /"youtubeSubgen":/); assert.match(output, /"youtubeSubgen":/);
assert.match(output, /"preserveLineBreaks": false/); assert.match(output, /"preserveLineBreaks": false/);
assert.match(output, /"nPlusOne"\s*:\s*\{/); assert.match(output, /"nPlusOne"\s*:\s*\{/);

View File

@@ -27,6 +27,7 @@ const {
shortcuts, shortcuts,
secondarySub, secondarySub,
subsync, subsync,
startupWarmups,
auto_start_overlay, auto_start_overlay,
} = CORE_DEFAULT_CONFIG; } = CORE_DEFAULT_CONFIG;
const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } = const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } =
@@ -44,6 +45,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
shortcuts, shortcuts,
secondarySub, secondarySub,
subsync, subsync,
startupWarmups,
subtitleStyle, subtitleStyle,
auto_start_overlay, auto_start_overlay,
jimaku, jimaku,

View File

@@ -10,6 +10,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
| 'shortcuts' | 'shortcuts'
| 'secondarySub' | 'secondarySub'
| 'subsync' | 'subsync'
| 'startupWarmups'
| 'auto_start_overlay' | 'auto_start_overlay'
> = { > = {
subtitlePosition: { yPercent: 10 }, subtitlePosition: { yPercent: 10 },
@@ -50,5 +51,12 @@ export const CORE_DEFAULT_CONFIG: Pick<
ffsubsync_path: '', ffsubsync_path: '',
ffmpeg_path: '', ffmpeg_path: '',
}, },
startupWarmups: {
lowPowerMode: false,
mecab: true,
yomitanExtension: true,
subtitleDictionaries: true,
jellyfinRemoteSession: true,
},
auto_start_overlay: false, auto_start_overlay: false,
}; };

View File

@@ -17,6 +17,7 @@ test('config option registry includes critical paths and has unique entries', ()
for (const requiredPath of [ for (const requiredPath of [
'logging.level', 'logging.level',
'startupWarmups.lowPowerMode',
'subtitleStyle.enableJlpt', 'subtitleStyle.enableJlpt',
'ankiConnect.enabled', 'ankiConnect.enabled',
'immersionTracking.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 keys = CONFIG_TEMPLATE_SECTIONS.map((section) => section.key);
const requiredKeys: (typeof keys)[number][] = [ const requiredKeys: (typeof keys)[number][] = [
'websocket', 'websocket',
'startupWarmups',
'subtitleStyle', 'subtitleStyle',
'ankiConnect', 'ankiConnect',
'immersionTracking', 'immersionTracking',

View File

@@ -32,6 +32,36 @@ export function buildCoreConfigOptionRegistry(
defaultValue: defaultConfig.subsync.defaultMode, defaultValue: defaultConfig.subsync.defaultMode,
description: 'Subsync default mode.', 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', path: 'shortcuts.multiCopyTimeoutMs',
kind: 'number', kind: 'number',

View File

@@ -26,6 +26,15 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'], description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'],
key: 'logging', 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', title: 'Keyboard Shortcuts',
description: ['Overlay keyboard shortcuts. Set a shortcut to null to disable.'], description: ['Overlay keyboard shortcuts. Set a shortcut to null to disable.'],

View File

@@ -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)) { if (isObject(src.shortcuts)) {
const shortcutKeys = [ const shortcutKeys = [
'toggleVisibleOverlayGlobal', 'toggleVisibleOverlayGlobal',

View File

@@ -1936,10 +1936,6 @@ const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppR
logInfo: (message) => appLogger.logInfo(message), logInfo: (message) => appLogger.logInfo(message),
logWarning: (message) => appLogger.logWarning(message), logWarning: (message) => appLogger.logWarning(message),
showDesktopNotification: (title, options) => showDesktopNotification(title, options), showDesktopNotification: (title, options) => showDesktopNotification(title, options),
showConfigWarningsDialog:
process.platform === 'darwin'
? (title, details) => dialog.showErrorBox(title, details)
: undefined,
startConfigHotReload: () => configHotReloadRuntime.start(), startConfigHotReload: () => configHotReloadRuntime.start(),
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options), refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options),
failHandlers: { failHandlers: {
@@ -2250,6 +2246,7 @@ const {
getKnownWordMatchMode: () => getKnownWordMatchMode: () =>
appState.ankiIntegration?.getKnownWordMatchMode() ?? appState.ankiIntegration?.getKnownWordMatchMode() ??
getResolvedConfig().ankiConnect.nPlusOne.matchMode, getResolvedConfig().ankiConnect.nPlusOne.matchMode,
getNPlusOneEnabled: () => getResolvedConfig().ankiConnect.nPlusOne.highlightEnabled,
getMinSentenceWordsForNPlusOne: () => getMinSentenceWordsForNPlusOne: () =>
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords, getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
getJlptLevel: (text) => appState.jlptLevelLookup(text), getJlptLevel: (text) => appState.jlptLevelLookup(text),
@@ -2290,6 +2287,28 @@ const {
}, },
isTexthookerOnlyMode: () => appState.texthookerOnlyMode, isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => {}), 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: () => { shouldAutoConnectJellyfinRemote: () => {
const jellyfin = getResolvedConfig().jellyfin; const jellyfin = getResolvedConfig().jellyfin;
return ( return (

View File

@@ -26,6 +26,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
const calls: string[] = []; const calls: string[] = [];
let started = false; let started = false;
let metrics = BASE_METRICS; let metrics = BASE_METRICS;
let mecabTokenizer: { id: string } | null = null;
class FakeMpvClient { class FakeMpvClient {
connected = false; connected = false;
@@ -140,9 +141,15 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
return { text }; return { text };
}, },
createMecabTokenizerAndCheckMainDeps: { createMecabTokenizerAndCheckMainDeps: {
getMecabTokenizer: () => ({ id: 'mecab' }), getMecabTokenizer: () => mecabTokenizer,
setMecabTokenizer: () => {}, setMecabTokenizer: (next) => {
createMecabTokenizer: () => ({ id: 'mecab' }), mecabTokenizer = next as { id: string };
calls.push('set-mecab');
},
createMecabTokenizer: () => {
calls.push('create-mecab');
return { id: 'mecab' };
},
checkAvailability: async () => { checkAvailability: async () => {
calls.push('check-mecab'); calls.push('check-mecab');
}, },
@@ -176,6 +183,10 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
ensureYomitanExtensionLoaded: async () => { ensureYomitanExtensionLoaded: async () => {
calls.push('warmup-yomitan'); calls.push('warmup-yomitan');
}, },
shouldWarmupMecab: () => true,
shouldWarmupYomitanExtension: () => true,
shouldWarmupSubtitleDictionaries: () => true,
shouldWarmupJellyfinRemoteSession: () => true,
shouldAutoConnectJellyfinRemote: () => false, shouldAutoConnectJellyfinRemote: () => false,
startJellyfinRemoteSession: async () => { startJellyfinRemoteSession: async () => {
calls.push('warmup-jellyfin'); 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('broadcast-metrics'));
assert.ok(calls.includes('create-tokenizer-runtime-deps')); assert.ok(calls.includes('create-tokenizer-runtime-deps'));
assert.ok(calls.includes('tokenize:subtitle text')); 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('check-mecab'));
assert.ok(calls.includes('prewarm-jlpt')); assert.ok(calls.includes('prewarm-jlpt'));
assert.ok(calls.includes('prewarm-frequency')); assert.ok(calls.includes('prewarm-frequency'));
assert.ok(calls.includes('set-started:true')); assert.ok(calls.includes('set-started:true'));
assert.ok(calls.includes('warmup-yomitan')); assert.ok(calls.includes('warmup-yomitan'));
assert.ok(calls.indexOf('create-mecab') < calls.indexOf('set-started:true'));
}); });

View File

@@ -133,6 +133,10 @@ export function composeMpvRuntimeHandlers<
options.tokenizer.prewarmSubtitleDictionariesMainDeps, options.tokenizer.prewarmSubtitleDictionariesMainDeps,
); );
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => { const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
await options.warmups.startBackgroundWarmupsMainDeps.ensureYomitanExtensionLoaded();
if (!options.tokenizer.createMecabTokenizerAndCheckMainDeps.getMecabTokenizer()) {
await createMecabTokenizerAndCheck().catch(() => {});
}
await prewarmSubtitleDictionaries(); await prewarmSubtitleDictionaries();
return options.tokenizer.tokenizeSubtitle( return options.tokenizer.tokenizeSubtitle(
text, text,

View File

@@ -34,6 +34,10 @@ test('startup warmups main deps builders map callbacks', async () => {
prewarmSubtitleDictionaries: async () => { prewarmSubtitleDictionaries: async () => {
calls.push('dict'); calls.push('dict');
}, },
shouldWarmupMecab: () => false,
shouldWarmupYomitanExtension: () => true,
shouldWarmupSubtitleDictionaries: () => false,
shouldWarmupJellyfinRemoteSession: () => true,
shouldAutoConnectJellyfinRemote: () => true, shouldAutoConnectJellyfinRemote: () => true,
startJellyfinRemoteSession: async () => { startJellyfinRemoteSession: async () => {
calls.push('jellyfin'); calls.push('jellyfin');
@@ -48,6 +52,10 @@ test('startup warmups main deps builders map callbacks', async () => {
await start.createMecabTokenizerAndCheck(); await start.createMecabTokenizerAndCheck();
await start.ensureYomitanExtensionLoaded(); await start.ensureYomitanExtensionLoaded();
await start.prewarmSubtitleDictionaries(); 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); assert.equal(start.shouldAutoConnectJellyfinRemote(), true);
await start.startJellyfinRemoteSession(); await start.startJellyfinRemoteSession();

View File

@@ -25,6 +25,10 @@ export function createBuildStartBackgroundWarmupsMainDepsHandler(deps: StartBack
createMecabTokenizerAndCheck: () => deps.createMecabTokenizerAndCheck(), createMecabTokenizerAndCheck: () => deps.createMecabTokenizerAndCheck(),
ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(), ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(),
prewarmSubtitleDictionaries: () => deps.prewarmSubtitleDictionaries(), prewarmSubtitleDictionaries: () => deps.prewarmSubtitleDictionaries(),
shouldWarmupMecab: () => deps.shouldWarmupMecab(),
shouldWarmupYomitanExtension: () => deps.shouldWarmupYomitanExtension(),
shouldWarmupSubtitleDictionaries: () => deps.shouldWarmupSubtitleDictionaries(),
shouldWarmupJellyfinRemoteSession: () => deps.shouldWarmupJellyfinRemoteSession(),
shouldAutoConnectJellyfinRemote: () => deps.shouldAutoConnectJellyfinRemote(), shouldAutoConnectJellyfinRemote: () => deps.shouldAutoConnectJellyfinRemote(),
startJellyfinRemoteSession: () => deps.startJellyfinRemoteSession(), startJellyfinRemoteSession: () => deps.startJellyfinRemoteSession(),
}); });

View File

@@ -41,6 +41,10 @@ test('startBackgroundWarmups no-ops when already started', () => {
createMecabTokenizerAndCheck: async () => {}, createMecabTokenizerAndCheck: async () => {},
ensureYomitanExtensionLoaded: async () => {}, ensureYomitanExtensionLoaded: async () => {},
prewarmSubtitleDictionaries: async () => {}, prewarmSubtitleDictionaries: async () => {},
shouldWarmupMecab: () => true,
shouldWarmupYomitanExtension: () => true,
shouldWarmupSubtitleDictionaries: () => true,
shouldWarmupJellyfinRemoteSession: () => true,
shouldAutoConnectJellyfinRemote: () => false, shouldAutoConnectJellyfinRemote: () => false,
startJellyfinRemoteSession: async () => {}, startJellyfinRemoteSession: async () => {},
}); });
@@ -49,7 +53,7 @@ test('startBackgroundWarmups no-ops when already started', () => {
assert.equal(launches, 0); 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[] = []; const labels: string[] = [];
let started = false; let started = false;
const startWarmups = createStartBackgroundWarmupsHandler({ const startWarmups = createStartBackgroundWarmupsHandler({
@@ -64,9 +68,13 @@ test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.ena
createMecabTokenizerAndCheck: async () => {}, createMecabTokenizerAndCheck: async () => {},
ensureYomitanExtensionLoaded: async () => {}, ensureYomitanExtensionLoaded: async () => {},
prewarmSubtitleDictionaries: async () => {}, prewarmSubtitleDictionaries: async () => {},
shouldWarmupMecab: () => false,
shouldWarmupYomitanExtension: () => true,
shouldWarmupSubtitleDictionaries: () => false,
shouldWarmupJellyfinRemoteSession: () => false,
shouldAutoConnectJellyfinRemote: () => shouldAutoConnectJellyfinRemote: () =>
shouldAutoConnectJellyfinRemote({ shouldAutoConnectJellyfinRemote({
enabled: false, enabled: true,
remoteControlEnabled: true, remoteControlEnabled: true,
remoteControlAutoConnect: true, remoteControlAutoConnect: true,
}), }),
@@ -75,7 +83,7 @@ test('startBackgroundWarmups does not schedule jellyfin warmup when jellyfin.ena
startWarmups(); startWarmups();
assert.equal(started, true); 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', () => { 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 () => {}, createMecabTokenizerAndCheck: async () => {},
ensureYomitanExtensionLoaded: async () => {}, ensureYomitanExtensionLoaded: async () => {},
prewarmSubtitleDictionaries: async () => {}, prewarmSubtitleDictionaries: async () => {},
shouldWarmupMecab: () => true,
shouldWarmupYomitanExtension: () => true,
shouldWarmupSubtitleDictionaries: () => true,
shouldWarmupJellyfinRemoteSession: () => true,
shouldAutoConnectJellyfinRemote: () => shouldAutoConnectJellyfinRemote: () =>
shouldAutoConnectJellyfinRemote({ shouldAutoConnectJellyfinRemote({
enabled: true, enabled: true,
@@ -111,3 +123,36 @@ test('startBackgroundWarmups schedules jellyfin warmup when all jellyfin flags a
'jellyfin-remote-session', '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']);
});

View File

@@ -24,6 +24,10 @@ export function createStartBackgroundWarmupsHandler(deps: {
createMecabTokenizerAndCheck: () => Promise<void>; createMecabTokenizerAndCheck: () => Promise<void>;
ensureYomitanExtensionLoaded: () => Promise<void>; ensureYomitanExtensionLoaded: () => Promise<void>;
prewarmSubtitleDictionaries: () => Promise<void>; prewarmSubtitleDictionaries: () => Promise<void>;
shouldWarmupMecab: () => boolean;
shouldWarmupYomitanExtension: () => boolean;
shouldWarmupSubtitleDictionaries: () => boolean;
shouldWarmupJellyfinRemoteSession: () => boolean;
shouldAutoConnectJellyfinRemote: () => boolean; shouldAutoConnectJellyfinRemote: () => boolean;
startJellyfinRemoteSession: () => Promise<void>; startJellyfinRemoteSession: () => Promise<void>;
}) { }) {
@@ -32,16 +36,22 @@ export function createStartBackgroundWarmupsHandler(deps: {
if (deps.isTexthookerOnlyMode()) return; if (deps.isTexthookerOnlyMode()) return;
deps.setStarted(true); deps.setStarted(true);
deps.launchTask('mecab', async () => { if (deps.shouldWarmupMecab()) {
await deps.createMecabTokenizerAndCheck(); deps.launchTask('mecab', async () => {
}); await deps.createMecabTokenizerAndCheck();
deps.launchTask('yomitan-extension', async () => { });
await deps.ensureYomitanExtensionLoaded(); }
}); if (deps.shouldWarmupYomitanExtension()) {
deps.launchTask('subtitle-dictionaries', async () => { deps.launchTask('yomitan-extension', async () => {
await deps.prewarmSubtitleDictionaries(); await deps.ensureYomitanExtensionLoaded();
}); });
if (deps.shouldAutoConnectJellyfinRemote()) { }
if (deps.shouldWarmupSubtitleDictionaries()) {
deps.launchTask('subtitle-dictionaries', async () => {
await deps.prewarmSubtitleDictionaries();
});
}
if (deps.shouldWarmupJellyfinRemoteSession() && deps.shouldAutoConnectJellyfinRemote()) {
deps.launchTask('jellyfin-remote-session', async () => { deps.launchTask('jellyfin-remote-session', async () => {
await deps.startJellyfinRemoteSession(); await deps.startJellyfinRemoteSession();
}); });

View File

@@ -99,6 +99,14 @@ export interface SubsyncConfig {
ffmpeg_path?: string; ffmpeg_path?: string;
} }
export interface StartupWarmupsConfig {
lowPowerMode?: boolean;
mecab?: boolean;
yomitanExtension?: boolean;
subtitleDictionaries?: boolean;
jellyfinRemoteSession?: boolean;
}
export interface WebSocketConfig { export interface WebSocketConfig {
enabled?: boolean | 'auto'; enabled?: boolean | 'auto';
port?: number; port?: number;
@@ -417,6 +425,7 @@ export interface Config {
shortcuts?: ShortcutsConfig; shortcuts?: ShortcutsConfig;
secondarySub?: SecondarySubConfig; secondarySub?: SecondarySubConfig;
subsync?: SubsyncConfig; subsync?: SubsyncConfig;
startupWarmups?: StartupWarmupsConfig;
subtitleStyle?: SubtitleStyleConfig; subtitleStyle?: SubtitleStyleConfig;
auto_start_overlay?: boolean; auto_start_overlay?: boolean;
jimaku?: JimakuConfig; jimaku?: JimakuConfig;
@@ -513,6 +522,13 @@ export interface ResolvedConfig {
shortcuts: Required<ShortcutsConfig>; shortcuts: Required<ShortcutsConfig>;
secondarySub: Required<SecondarySubConfig>; secondarySub: Required<SecondarySubConfig>;
subsync: Required<SubsyncConfig>; subsync: Required<SubsyncConfig>;
startupWarmups: {
lowPowerMode: boolean;
mecab: boolean;
yomitanExtension: boolean;
subtitleDictionaries: boolean;
jellyfinRemoteSession: boolean;
};
subtitleStyle: Required<Omit<SubtitleStyleConfig, 'secondary' | 'frequencyDictionary'>> & { subtitleStyle: Required<Omit<SubtitleStyleConfig, 'secondary' | 'frequencyDictionary'>> & {
secondary: Required<NonNullable<SubtitleStyleConfig['secondary']>>; secondary: Required<NonNullable<SubtitleStyleConfig['secondary']>>;
frequencyDictionary: { frequencyDictionary: {