mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
fix: reuse background tokenization warmups
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
---
|
||||
id: TASK-131
|
||||
title: Avoid duplicate tokenization warmup after background startup
|
||||
status: Done
|
||||
assignee:
|
||||
- codex
|
||||
created_date: '2026-03-08 10:12'
|
||||
updated_date: '2026-03-08 12:00'
|
||||
labels:
|
||||
- bug
|
||||
dependencies: []
|
||||
references:
|
||||
- >-
|
||||
/Users/sudacode/projects/japanese/SubMiner/src/main/runtime/composers/mpv-runtime-composer.ts
|
||||
- >-
|
||||
/Users/sudacode/projects/japanese/SubMiner/src/main/runtime/startup-warmups.ts
|
||||
- >-
|
||||
/Users/sudacode/projects/japanese/SubMiner/src/main/runtime/composers/mpv-runtime-composer.test.ts
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
When SubMiner is already running in the background and mpv is launched from the launcher or mpv plugin, the live app should reuse startup tokenization warmup state instead of re-entering the Yomitan/tokenization/annotation warmup path on first overlay use.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [x] #1 Background startup tokenization warmup is recorded in the runtime state used by later mpv/tokenization flows.
|
||||
- [x] #2 Launching mpv from the launcher or plugin against an already-running background app does not re-run duplicate Yomitan/tokenization annotation warmup work in the live process.
|
||||
- [x] #3 Regression tests cover the warmed-background path and protect against re-entering duplicate warmup work.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1. Add a regression test covering the case where background startup warmups already completed and a later tokenize call must not re-enter Yomitan/MeCab/dictionary warmups.
|
||||
2. Update mpv tokenization warmup composition so startup background warmups and on-demand tokenization share the same completion state.
|
||||
3. Run the focused composer/runtime tests and update acceptance criteria/notes with results.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Root-cause hypothesis: startup background warmups and on-demand tokenization warmups use separate state, so later mpv launch can re-enter warmup bookkeeping even though background startup already warmed dependencies.
|
||||
|
||||
Implemented shared warmup state between startup background warmups and on-demand tokenization warmups by forwarding scheduled Yomitan/tokenization promises into the mpv runtime composer. Added regression coverage for the warmed-background path. Verified with `bun run test:fast` plus focused composer/startup warmup tests.
|
||||
|
||||
Follow-up root cause from live retest: second mpv open could still pause on the startup gate because the runtime only treated full background tokenization warmup completion as reusable readiness. In practice, first-file tokenization could already be ready while slower dictionary prewarm work was still finishing, so reopening a video waited on duplicate warmup completion even though annotations were already usable.
|
||||
|
||||
Adjusted `src/main/runtime/composers/mpv-runtime-composer.ts` so autoplay reuse keys off a separate playback-ready latch. The latch flips true either when background warmups fully cover tokenization or when `onTokenizationReady` fires for a real subtitle line. `src/main.ts` already uses `isTokenizationWarmupReady()` to fast-signal `subminer-autoplay-ready` on a fresh media-path change, so reopened videos can now resume immediately once tokenization has succeeded once in the persistent app.
|
||||
|
||||
Validation update: `bun test src/core/services/cli-command.test.ts src/main/runtime/mpv-main-event-actions.test.ts src/main/runtime/composers/mpv-runtime-composer.test.ts launcher/mpv.test.ts launcher/smoke.e2e.test.ts` passed, `lua scripts/test-plugin-start-gate.lua` passed, and `bun run typecheck` passed.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Background startup tokenization warmups now feed the same in-memory warmup state used by later mpv tokenization. When the app is already running and warmed in the background, launcher/plugin-driven mpv startup reuses that state instead of re-entering Yomitan/tokenization annotation warmups. Added a regression test for the warmed-background path and verified with `bun run test:fast`.
|
||||
|
||||
A later follow-up fixed the remaining second-open delay: autoplay reuse no longer waits for the entire background dictionary warmup pipeline to finish. After the persistent app has produced one tokenization-ready event, later mpv reconnects reuse that readiness immediately, so reopening the same or another video does not pause again on duplicate warmup bookkeeping.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
@@ -212,6 +212,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
assert.equal(typeof composed.createMecabTokenizerAndCheck, 'function');
|
||||
assert.equal(typeof composed.prewarmSubtitleDictionaries, 'function');
|
||||
assert.equal(typeof composed.startTokenizationWarmups, 'function');
|
||||
assert.equal(typeof composed.isTokenizationWarmupReady, 'function');
|
||||
assert.equal(typeof composed.launchBackgroundWarmupTask, 'function');
|
||||
assert.equal(typeof composed.startBackgroundWarmups, 'function');
|
||||
|
||||
@@ -219,7 +220,9 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
assert.equal(client.connected, true);
|
||||
|
||||
composed.updateMpvSubtitleRenderMetrics({ subPos: 90 });
|
||||
assert.equal(composed.isTokenizationWarmupReady(), false);
|
||||
await composed.startTokenizationWarmups();
|
||||
assert.equal(composed.isTokenizationWarmupReady(), true);
|
||||
const tokenized = await composed.tokenizeSubtitle('subtitle text');
|
||||
await composed.createMecabTokenizerAndCheck();
|
||||
await composed.prewarmSubtitleDictionaries();
|
||||
@@ -789,9 +792,11 @@ test('composeMpvRuntimeHandlers shows annotation loading OSD after tokenization-
|
||||
const warmupPromise = composed.startTokenizationWarmups();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
assert.deepEqual(osdMessages, []);
|
||||
assert.equal(composed.isTokenizationWarmupReady(), false);
|
||||
|
||||
await composed.tokenizeSubtitle('first line');
|
||||
assert.deepEqual(osdMessages, ['Loading subtitle annotations |']);
|
||||
assert.equal(composed.isTokenizationWarmupReady(), true);
|
||||
|
||||
jlptDeferred.resolve();
|
||||
frequencyDeferred.resolve();
|
||||
@@ -800,3 +805,154 @@ test('composeMpvRuntimeHandlers shows annotation loading OSD after tokenization-
|
||||
|
||||
assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Subtitle annotations loaded']);
|
||||
});
|
||||
|
||||
test('composeMpvRuntimeHandlers reuses completed background tokenization warmups for later tokenize calls', async () => {
|
||||
let started = false;
|
||||
let yomitanWarmupCalls = 0;
|
||||
let mecabWarmupCalls = 0;
|
||||
let jlptWarmupCalls = 0;
|
||||
let frequencyWarmupCalls = 0;
|
||||
let mecabTokenizer: { tokenize: () => Promise<never[]> } | null = null;
|
||||
|
||||
const composed = composeMpvRuntimeHandlers<
|
||||
{ connect: () => void; on: () => void },
|
||||
{ isKnownWord: () => boolean },
|
||||
{ text: string }
|
||||
>({
|
||||
bindMpvMainEventHandlersMainDeps: {
|
||||
appState: {
|
||||
initialArgs: null,
|
||||
overlayRuntimeInitialized: true,
|
||||
mpvClient: null,
|
||||
immersionTracker: null,
|
||||
subtitleTimingTracker: null,
|
||||
currentSubText: '',
|
||||
currentSubAssText: '',
|
||||
playbackPaused: null,
|
||||
previousSecondarySubVisibility: null,
|
||||
},
|
||||
getQuitOnDisconnectArmed: () => false,
|
||||
scheduleQuitCheck: () => {},
|
||||
quitApp: () => {},
|
||||
reportJellyfinRemoteStopped: () => {},
|
||||
syncOverlayMpvSubtitleSuppression: () => {},
|
||||
maybeRunAnilistPostWatchUpdate: async () => {},
|
||||
logSubtitleTimingError: () => {},
|
||||
broadcastToOverlayWindows: () => {},
|
||||
onSubtitleChange: () => {},
|
||||
refreshDiscordPresence: () => {},
|
||||
ensureImmersionTrackerInitialized: () => {},
|
||||
updateCurrentMediaPath: () => {},
|
||||
restoreMpvSubVisibility: () => {},
|
||||
getCurrentAnilistMediaKey: () => null,
|
||||
resetAnilistMediaTracking: () => {},
|
||||
maybeProbeAnilistDuration: () => {},
|
||||
ensureAnilistMediaGuess: () => {},
|
||||
syncImmersionMediaState: () => {},
|
||||
updateCurrentMediaTitle: () => {},
|
||||
resetAnilistMediaGuessState: () => {},
|
||||
reportJellyfinRemoteProgress: () => {},
|
||||
updateSubtitleRenderMetrics: () => {},
|
||||
},
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||
createClient: class {
|
||||
connect(): void {}
|
||||
on(): void {}
|
||||
},
|
||||
getSocketPath: () => '/tmp/mpv.sock',
|
||||
getResolvedConfig: () => ({ auto_start_overlay: false }),
|
||||
isAutoStartOverlayEnabled: () => false,
|
||||
setOverlayVisible: () => {},
|
||||
isVisibleOverlayVisible: () => false,
|
||||
getReconnectTimer: () => null,
|
||||
setReconnectTimer: () => {},
|
||||
},
|
||||
updateMpvSubtitleRenderMetricsMainDeps: {
|
||||
getCurrentMetrics: () => BASE_METRICS,
|
||||
setCurrentMetrics: () => {},
|
||||
applyPatch: (current, patch) => ({ next: { ...current, ...patch }, changed: true }),
|
||||
broadcastMetrics: () => {},
|
||||
},
|
||||
tokenizer: {
|
||||
buildTokenizerDepsMainDeps: {
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => {},
|
||||
getYomitanParserReadyPromise: () => null,
|
||||
setYomitanParserReadyPromise: () => {},
|
||||
getYomitanParserInitPromise: () => null,
|
||||
setYomitanParserInitPromise: () => {},
|
||||
isKnownWord: () => false,
|
||||
recordLookup: () => {},
|
||||
getKnownWordMatchMode: () => 'headword',
|
||||
getNPlusOneEnabled: () => true,
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => null,
|
||||
getJlptEnabled: () => true,
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyDictionaryMatchMode: () => 'headword',
|
||||
getFrequencyRank: () => null,
|
||||
getYomitanGroupDebugEnabled: () => false,
|
||||
getMecabTokenizer: () => mecabTokenizer,
|
||||
},
|
||||
createTokenizerRuntimeDeps: () => ({ isKnownWord: () => false }),
|
||||
tokenizeSubtitle: async (text) => ({ text }),
|
||||
createMecabTokenizerAndCheckMainDeps: {
|
||||
getMecabTokenizer: () => mecabTokenizer,
|
||||
setMecabTokenizer: (next) => {
|
||||
mecabTokenizer = next as { tokenize: () => Promise<never[]> };
|
||||
},
|
||||
createMecabTokenizer: () => ({ tokenize: async () => [] }),
|
||||
checkAvailability: async () => {
|
||||
mecabWarmupCalls += 1;
|
||||
},
|
||||
},
|
||||
prewarmSubtitleDictionariesMainDeps: {
|
||||
ensureJlptDictionaryLookup: async () => {
|
||||
jlptWarmupCalls += 1;
|
||||
},
|
||||
ensureFrequencyDictionaryLookup: async () => {
|
||||
frequencyWarmupCalls += 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
warmups: {
|
||||
launchBackgroundWarmupTaskMainDeps: {
|
||||
now: () => 0,
|
||||
logDebug: () => {},
|
||||
logWarn: () => {},
|
||||
},
|
||||
startBackgroundWarmupsMainDeps: {
|
||||
getStarted: () => started,
|
||||
setStarted: (next) => {
|
||||
started = next;
|
||||
},
|
||||
isTexthookerOnlyMode: () => false,
|
||||
ensureYomitanExtensionLoaded: async () => {
|
||||
yomitanWarmupCalls += 1;
|
||||
},
|
||||
shouldWarmupMecab: () => true,
|
||||
shouldWarmupYomitanExtension: () => true,
|
||||
shouldWarmupSubtitleDictionaries: () => true,
|
||||
shouldWarmupJellyfinRemoteSession: () => false,
|
||||
shouldAutoConnectJellyfinRemote: () => false,
|
||||
startJellyfinRemoteSession: async () => {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
composed.startBackgroundWarmups();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.equal(yomitanWarmupCalls, 1);
|
||||
assert.equal(mecabWarmupCalls, 1);
|
||||
assert.equal(jlptWarmupCalls, 1);
|
||||
assert.equal(frequencyWarmupCalls, 1);
|
||||
|
||||
await composed.tokenizeSubtitle('first line after background warmup');
|
||||
|
||||
assert.equal(yomitanWarmupCalls, 1);
|
||||
assert.equal(mecabWarmupCalls, 1);
|
||||
assert.equal(jlptWarmupCalls, 1);
|
||||
assert.equal(frequencyWarmupCalls, 1);
|
||||
});
|
||||
|
||||
@@ -88,6 +88,7 @@ export type MpvRuntimeComposerResult<
|
||||
createMecabTokenizerAndCheck: () => Promise<void>;
|
||||
prewarmSubtitleDictionaries: () => Promise<void>;
|
||||
startTokenizationWarmups: () => Promise<void>;
|
||||
isTokenizationWarmupReady: () => boolean;
|
||||
launchBackgroundWarmupTask: ReturnType<typeof createLaunchBackgroundWarmupTaskFromStartup>;
|
||||
startBackgroundWarmups: ReturnType<typeof createStartBackgroundWarmupsFromStartup>;
|
||||
}>;
|
||||
@@ -151,6 +152,36 @@ export function composeMpvRuntimeHandlers<
|
||||
let tokenizationPrerequisiteWarmupInFlight: Promise<void> | null = null;
|
||||
let tokenizationPrerequisiteWarmupCompleted = false;
|
||||
let tokenizationWarmupCompleted = false;
|
||||
let tokenizationPlaybackReady = false;
|
||||
const markTokenizationPrerequisiteWarmupCompleted = (): void => {
|
||||
tokenizationPrerequisiteWarmupCompleted = true;
|
||||
};
|
||||
const markTokenizationPlaybackReady = (): void => {
|
||||
tokenizationPlaybackReady = true;
|
||||
};
|
||||
const markTokenizationWarmupCompleted = (): void => {
|
||||
tokenizationPrerequisiteWarmupCompleted = true;
|
||||
tokenizationWarmupCompleted = true;
|
||||
tokenizationPlaybackReady = true;
|
||||
};
|
||||
const backgroundWarmupCoversOnDemandTokenization = (): boolean => {
|
||||
if (!options.warmups.startBackgroundWarmupsMainDeps.shouldWarmupYomitanExtension()) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
shouldInitializeMecabForAnnotations() &&
|
||||
!options.warmups.startBackgroundWarmupsMainDeps.shouldWarmupMecab()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
shouldWarmupAnnotationDictionaries() &&
|
||||
!options.warmups.startBackgroundWarmupsMainDeps.shouldWarmupSubtitleDictionaries()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const ensureTokenizationPrerequisites = (): Promise<void> => {
|
||||
if (tokenizationPrerequisiteWarmupCompleted) {
|
||||
return Promise.resolve();
|
||||
@@ -159,7 +190,7 @@ export function composeMpvRuntimeHandlers<
|
||||
tokenizationPrerequisiteWarmupInFlight = options.warmups.startBackgroundWarmupsMainDeps
|
||||
.ensureYomitanExtensionLoaded()
|
||||
.then(() => {
|
||||
tokenizationPrerequisiteWarmupCompleted = true;
|
||||
markTokenizationPrerequisiteWarmupCompleted();
|
||||
})
|
||||
.finally(() => {
|
||||
tokenizationPrerequisiteWarmupInFlight = null;
|
||||
@@ -184,7 +215,7 @@ export function composeMpvRuntimeHandlers<
|
||||
warmupTasks.push(prewarmSubtitleDictionaries().catch(() => {}));
|
||||
}
|
||||
await Promise.all(warmupTasks);
|
||||
tokenizationWarmupCompleted = true;
|
||||
markTokenizationWarmupCompleted();
|
||||
})().finally(() => {
|
||||
tokenizationWarmupInFlight = null;
|
||||
});
|
||||
@@ -198,6 +229,7 @@ export function composeMpvRuntimeHandlers<
|
||||
if (shouldWarmupAnnotationDictionaries()) {
|
||||
const onTokenizationReady = tokenizerMainDeps.onTokenizationReady;
|
||||
tokenizerMainDeps.onTokenizationReady = (tokenizedText: string): void => {
|
||||
markTokenizationPlaybackReady();
|
||||
onTokenizationReady?.(tokenizedText);
|
||||
if (!tokenizationWarmupCompleted) {
|
||||
void prewarmSubtitleDictionaries({ showLoadingOsd: true }).catch(() => {});
|
||||
@@ -221,6 +253,36 @@ export function composeMpvRuntimeHandlers<
|
||||
launchTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||
onYomitanExtensionWarmupScheduled: (promise) => {
|
||||
if (tokenizationPrerequisiteWarmupCompleted) {
|
||||
return;
|
||||
}
|
||||
const finalizedPromise = promise
|
||||
.then(() => {
|
||||
markTokenizationPrerequisiteWarmupCompleted();
|
||||
})
|
||||
.finally(() => {
|
||||
if (tokenizationPrerequisiteWarmupInFlight === finalizedPromise) {
|
||||
tokenizationPrerequisiteWarmupInFlight = null;
|
||||
}
|
||||
});
|
||||
tokenizationPrerequisiteWarmupInFlight = finalizedPromise;
|
||||
},
|
||||
onTokenizationWarmupScheduled: (promise) => {
|
||||
if (tokenizationWarmupCompleted || !backgroundWarmupCoversOnDemandTokenization()) {
|
||||
return;
|
||||
}
|
||||
const finalizedPromise = promise
|
||||
.then(() => {
|
||||
markTokenizationWarmupCompleted();
|
||||
})
|
||||
.finally(() => {
|
||||
if (tokenizationWarmupInFlight === finalizedPromise) {
|
||||
tokenizationWarmupInFlight = null;
|
||||
}
|
||||
});
|
||||
tokenizationWarmupInFlight = finalizedPromise;
|
||||
},
|
||||
})(),
|
||||
);
|
||||
|
||||
@@ -232,6 +294,7 @@ export function composeMpvRuntimeHandlers<
|
||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||
startTokenizationWarmups,
|
||||
isTokenizationWarmupReady: () => tokenizationPlaybackReady,
|
||||
launchBackgroundWarmupTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||
};
|
||||
|
||||
@@ -35,6 +35,18 @@ export function createBuildStartBackgroundWarmupsMainDepsHandler(
|
||||
shouldWarmupJellyfinRemoteSession: () => deps.shouldWarmupJellyfinRemoteSession(),
|
||||
shouldAutoConnectJellyfinRemote: () => deps.shouldAutoConnectJellyfinRemote(),
|
||||
startJellyfinRemoteSession: () => deps.startJellyfinRemoteSession(),
|
||||
...(deps.onYomitanExtensionWarmupScheduled
|
||||
? {
|
||||
onYomitanExtensionWarmupScheduled: (promise: Promise<void>) =>
|
||||
deps.onYomitanExtensionWarmupScheduled!(promise),
|
||||
}
|
||||
: {}),
|
||||
...(deps.onTokenizationWarmupScheduled
|
||||
? {
|
||||
onTokenizationWarmupScheduled: (promise: Promise<void>) =>
|
||||
deps.onTokenizationWarmupScheduled!(promise),
|
||||
}
|
||||
: {}),
|
||||
logDebug: deps.logDebug,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ export function createStartBackgroundWarmupsHandler(deps: {
|
||||
shouldWarmupJellyfinRemoteSession: () => boolean;
|
||||
shouldAutoConnectJellyfinRemote: () => boolean;
|
||||
startJellyfinRemoteSession: () => Promise<void>;
|
||||
onYomitanExtensionWarmupScheduled?: (promise: Promise<void>) => void;
|
||||
onTokenizationWarmupScheduled?: (promise: Promise<void>) => void;
|
||||
logDebug?: (message: string) => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
@@ -46,9 +48,7 @@ export function createStartBackgroundWarmupsHandler(deps: {
|
||||
const shouldWarmupTokenization =
|
||||
warmupMecab || warmupYomitanExtension || warmupSubtitleDictionaries;
|
||||
if (shouldWarmupTokenization) {
|
||||
deps.launchTask('subtitle-tokenization', async () => {
|
||||
await Promise.all([
|
||||
warmupYomitanExtension
|
||||
const yomitanWarmupPromise = warmupYomitanExtension
|
||||
? (async () => {
|
||||
deps.logDebug?.('[startup-warmup] stage start: yomitan-extension');
|
||||
await deps.ensureYomitanExtensionLoaded();
|
||||
@@ -56,7 +56,13 @@ export function createStartBackgroundWarmupsHandler(deps: {
|
||||
})()
|
||||
: Promise.resolve().then(() => {
|
||||
deps.logDebug?.('[startup-warmup] stage skipped: yomitan-extension');
|
||||
}),
|
||||
});
|
||||
if (warmupYomitanExtension) {
|
||||
deps.onYomitanExtensionWarmupScheduled?.(yomitanWarmupPromise);
|
||||
}
|
||||
|
||||
const tokenizationWarmupPromise = Promise.all([
|
||||
yomitanWarmupPromise,
|
||||
warmupMecab
|
||||
? (async () => {
|
||||
deps.logDebug?.('[startup-warmup] stage start: mecab');
|
||||
@@ -75,8 +81,9 @@ export function createStartBackgroundWarmupsHandler(deps: {
|
||||
: Promise.resolve().then(() => {
|
||||
deps.logDebug?.('[startup-warmup] stage skipped: subtitle-dictionaries');
|
||||
}),
|
||||
]);
|
||||
});
|
||||
]).then(() => {});
|
||||
deps.onTokenizationWarmupScheduled?.(tokenizationWarmupPromise);
|
||||
deps.launchTask('subtitle-tokenization', () => tokenizationWarmupPromise);
|
||||
}
|
||||
if (warmupJellyfinRemoteSession && autoConnectJellyfinRemote) {
|
||||
deps.launchTask('jellyfin-remote-session', async () => {
|
||||
|
||||
Reference in New Issue
Block a user