mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
fix: lazy initialize immersion tracker
This commit is contained in:
@@ -58,9 +58,12 @@ test('runAppReadyRuntime starts websocket in auto mode when plugin missing', asy
|
||||
await runAppReadyRuntime(deps);
|
||||
assert.ok(calls.includes('startSubtitleWebsocket:9001'));
|
||||
assert.ok(calls.includes('initializeOverlayRuntime'));
|
||||
assert.ok(calls.includes('createImmersionTracker'));
|
||||
assert.ok(calls.includes('startBackgroundWarmups'));
|
||||
assert.ok(calls.includes('log:Runtime ready: invoking createImmersionTracker.'));
|
||||
assert.ok(
|
||||
calls.includes(
|
||||
'log:Runtime ready: immersion tracker startup deferred until first media activity.',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('runAppReadyRuntime skips Jellyfin remote startup when dependency is not wired', async () => {
|
||||
@@ -86,23 +89,7 @@ test('runAppReadyRuntime logs when createImmersionTracker dependency is missing'
|
||||
createImmersionTracker: undefined,
|
||||
});
|
||||
await runAppReadyRuntime(deps);
|
||||
assert.ok(calls.includes('log:Runtime ready: createImmersionTracker dependency is missing.'));
|
||||
});
|
||||
|
||||
test('runAppReadyRuntime logs and continues when createImmersionTracker throws', async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
createImmersionTracker: () => {
|
||||
calls.push('createImmersionTracker');
|
||||
throw new Error('immersion init failed');
|
||||
},
|
||||
});
|
||||
await runAppReadyRuntime(deps);
|
||||
assert.ok(calls.includes('createImmersionTracker'));
|
||||
assert.ok(
|
||||
calls.includes('log:Runtime ready: createImmersionTracker failed: immersion init failed'),
|
||||
);
|
||||
assert.ok(calls.includes('initializeOverlayRuntime'));
|
||||
assert.ok(calls.includes('handleInitialArgs'));
|
||||
assert.ok(calls.includes('log:Runtime ready: immersion tracker dependency is missing.'));
|
||||
});
|
||||
|
||||
test('runAppReadyRuntime logs defer message when overlay not auto-started', async () => {
|
||||
|
||||
@@ -203,14 +203,9 @@ export async function runAppReadyRuntime(deps: AppReadyRuntimeDeps): Promise<voi
|
||||
|
||||
deps.createSubtitleTimingTracker();
|
||||
if (deps.createImmersionTracker) {
|
||||
deps.log('Runtime ready: invoking createImmersionTracker.');
|
||||
try {
|
||||
deps.createImmersionTracker();
|
||||
} catch (error) {
|
||||
deps.log(`Runtime ready: createImmersionTracker failed: ${(error as Error).message}`);
|
||||
}
|
||||
deps.log('Runtime ready: immersion tracker startup deferred until first media activity.');
|
||||
} else {
|
||||
deps.log('Runtime ready: createImmersionTracker dependency is missing.');
|
||||
deps.log('Runtime ready: immersion tracker dependency is missing.');
|
||||
}
|
||||
|
||||
if (deps.texthookerOnlyMode) {
|
||||
|
||||
99
src/main.ts
99
src/main.ts
@@ -365,6 +365,8 @@ import {
|
||||
triggerFieldGrouping as triggerFieldGroupingCore,
|
||||
updateLastCardFromClipboard as updateLastCardFromClipboardCore,
|
||||
} from './core/services';
|
||||
import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup';
|
||||
import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/immersion-startup-main-deps';
|
||||
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
||||
import {
|
||||
guessAnilistMediaInfo,
|
||||
@@ -437,7 +439,9 @@ import { resolveConfigDir } from './config/path-resolution';
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal');
|
||||
const passwordStore = normalizePasswordStoreArg(getPasswordStoreArg(process.argv) ?? getDefaultPasswordStore());
|
||||
const passwordStore = normalizePasswordStoreArg(
|
||||
getPasswordStoreArg(process.argv) ?? getDefaultPasswordStore(),
|
||||
);
|
||||
app.commandLine.appendSwitch('password-store', passwordStore);
|
||||
console.debug(`[main] Applied --password-store ${passwordStore}`);
|
||||
}
|
||||
@@ -683,8 +687,13 @@ function refreshDiscordPresenceMediaDuration(): void {
|
||||
}
|
||||
|
||||
function publishDiscordPresence(): void {
|
||||
const discordPresenceService = appState.discordPresenceService;
|
||||
if (!discordPresenceService || getResolvedConfig().discordPresence.enabled !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshDiscordPresenceMediaDuration();
|
||||
appState.discordPresenceService?.publish({
|
||||
discordPresenceService.publish({
|
||||
mediaTitle: appState.currentMediaTitle,
|
||||
mediaPath: appState.currentMediaPath,
|
||||
subtitleText: appState.currentSubText,
|
||||
@@ -718,6 +727,11 @@ function createDiscordRpcClient() {
|
||||
}
|
||||
|
||||
async function initializeDiscordPresenceService(): Promise<void> {
|
||||
if (getResolvedConfig().discordPresence.enabled !== true) {
|
||||
appState.discordPresenceService = null;
|
||||
return;
|
||||
}
|
||||
|
||||
appState.discordPresenceService = createDiscordPresenceService({
|
||||
config: getResolvedConfig().discordPresence,
|
||||
createClient: () => createDiscordRpcClient(),
|
||||
@@ -1802,6 +1816,15 @@ const {
|
||||
},
|
||||
});
|
||||
|
||||
function refreshAnilistClientSecretStateIfEnabled(options?: {
|
||||
force?: boolean;
|
||||
}): Promise<string | null> {
|
||||
if (!isAnilistTrackingEnabled(getResolvedConfig())) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return refreshAnilistClientSecretState(options);
|
||||
}
|
||||
|
||||
const rememberAnilistAttemptedUpdate = (key: string): void => {
|
||||
rememberAnilistAttemptedUpdateKey(
|
||||
anilistAttemptedUpdateKeys,
|
||||
@@ -1930,6 +1953,35 @@ const {
|
||||
});
|
||||
registerProtocolUrlHandlersHandler();
|
||||
|
||||
const immersionTrackerStartupMainDeps: Parameters<
|
||||
typeof createBuildImmersionTrackerStartupMainDepsHandler
|
||||
>[0] = {
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(),
|
||||
createTrackerService: (params) => new ImmersionTrackerService(params),
|
||||
setTracker: (tracker) => {
|
||||
appState.immersionTracker = tracker as ImmersionTrackerService | null;
|
||||
},
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
seedTrackerFromCurrentMedia: () => {
|
||||
void immersionMediaRuntime.seedFromCurrentMedia();
|
||||
},
|
||||
logInfo: (message) => logger.info(message),
|
||||
logDebug: (message) => logger.debug(message),
|
||||
logWarn: (message, details) => logger.warn(message, details),
|
||||
};
|
||||
const createImmersionTrackerStartup = createImmersionTrackerStartupHandler(
|
||||
createBuildImmersionTrackerStartupMainDepsHandler(immersionTrackerStartupMainDeps)(),
|
||||
);
|
||||
let hasAttemptedImmersionTrackerStartup = false;
|
||||
const ensureImmersionTrackerStarted = (): void => {
|
||||
if (hasAttemptedImmersionTrackerStartup || appState.immersionTracker) {
|
||||
return;
|
||||
}
|
||||
hasAttemptedImmersionTrackerStartup = true;
|
||||
createImmersionTrackerStartup();
|
||||
};
|
||||
|
||||
const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppReadyRuntime({
|
||||
reloadConfigMainDeps: {
|
||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||
@@ -1937,7 +1989,7 @@ const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppR
|
||||
logWarning: (message) => appLogger.logWarning(message),
|
||||
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
||||
startConfigHotReload: () => configHotReloadRuntime.start(),
|
||||
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options),
|
||||
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretStateIfEnabled(options),
|
||||
failHandlers: {
|
||||
logError: (details) => logger.error(details),
|
||||
showErrorBox: (title, details) => dialog.showErrorBox(title, details),
|
||||
@@ -2016,26 +2068,15 @@ const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppR
|
||||
: configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(),
|
||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||
handleInitialArgs: () => handleInitialArgs(),
|
||||
createImmersionTracker: () => {
|
||||
ensureImmersionTrackerStarted();
|
||||
},
|
||||
logDebug: (message: string) => {
|
||||
logger.debug(message);
|
||||
},
|
||||
now: () => Date.now(),
|
||||
},
|
||||
immersionTrackerStartupMainDeps: {
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(),
|
||||
createTrackerService: (params) => new ImmersionTrackerService(params),
|
||||
setTracker: (tracker) => {
|
||||
appState.immersionTracker = tracker as ImmersionTrackerService | null;
|
||||
},
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
seedTrackerFromCurrentMedia: () => {
|
||||
void immersionMediaRuntime.seedFromCurrentMedia();
|
||||
},
|
||||
logInfo: (message) => logger.info(message),
|
||||
logDebug: (message) => logger.debug(message),
|
||||
logWarn: (message, details) => logger.warn(message, details),
|
||||
},
|
||||
immersionTrackerStartupMainDeps,
|
||||
});
|
||||
|
||||
const { appLifecycleRuntimeRunner, runAndApplyStartupState } =
|
||||
@@ -2099,8 +2140,10 @@ const { appLifecycleRuntimeRunner, runAndApplyStartupState } =
|
||||
});
|
||||
|
||||
runAndApplyStartupState();
|
||||
void refreshAnilistClientSecretState({ force: true });
|
||||
anilistStateRuntime.refreshRetryQueueState();
|
||||
if (isAnilistTrackingEnabled(getResolvedConfig())) {
|
||||
void refreshAnilistClientSecretStateIfEnabled({ force: true });
|
||||
anilistStateRuntime.refreshRetryQueueState();
|
||||
}
|
||||
void initializeDiscordPresenceService();
|
||||
|
||||
const handleCliCommand = createCliCommandRuntimeHandler({
|
||||
@@ -2167,7 +2210,13 @@ const {
|
||||
refreshDiscordPresence: () => {
|
||||
publishDiscordPresence();
|
||||
},
|
||||
ensureImmersionTrackerInitialized: () => {
|
||||
ensureImmersionTrackerStarted();
|
||||
},
|
||||
updateCurrentMediaPath: (path) => {
|
||||
if (path) {
|
||||
ensureImmersionTrackerStarted();
|
||||
}
|
||||
mediaRuntime.updateCurrentMediaPath(path);
|
||||
},
|
||||
restoreMpvSubVisibility: () => {
|
||||
@@ -2241,6 +2290,7 @@ const {
|
||||
},
|
||||
isKnownWord: (text) => Boolean(appState.ankiIntegration?.isKnownWord(text)),
|
||||
recordLookup: (hit) => {
|
||||
ensureImmersionTrackerStarted();
|
||||
appState.immersionTracker?.recordLookup(hit);
|
||||
},
|
||||
getKnownWordMatchMode: () =>
|
||||
@@ -2662,6 +2712,7 @@ const buildMineSentenceCardMainDepsHandler = createBuildMineSentenceCardMainDeps
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
mineSentenceCardCore,
|
||||
recordCardsMined: (count) => {
|
||||
ensureImmersionTrackerStarted();
|
||||
appState.immersionTracker?.recordCardsMined(count);
|
||||
},
|
||||
});
|
||||
@@ -2697,6 +2748,7 @@ const buildHandleMineSentenceDigitMainDepsHandler =
|
||||
logger.error(message, err);
|
||||
},
|
||||
onCardsMined: (cards) => {
|
||||
ensureImmersionTrackerStarted();
|
||||
appState.immersionTracker?.recordCardsMined(cards);
|
||||
},
|
||||
handleMineSentenceDigitCore,
|
||||
@@ -2910,9 +2962,7 @@ const {
|
||||
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled),
|
||||
isOverlayVisible: (windowKind) =>
|
||||
windowKind === 'visible'
|
||||
? overlayManager.getVisibleOverlayVisible()
|
||||
: false,
|
||||
windowKind === 'visible' ? overlayManager.getVisibleOverlayVisible() : false,
|
||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
||||
onWindowClosed: (windowKind) => {
|
||||
@@ -3013,7 +3063,8 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
||||
},
|
||||
createMainWindow: () => createMainWindow(),
|
||||
registerGlobalShortcuts: () => registerGlobalShortcuts(),
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => updateVisibleOverlayBounds(geometry),
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
updateVisibleOverlayBounds(geometry),
|
||||
getOverlayWindows: () => getOverlayWindows(),
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
showDesktopNotification,
|
||||
|
||||
@@ -42,11 +42,13 @@ export function composeAppReadyRuntime(options: AppReadyComposerOptions): AppRea
|
||||
createBuildAppReadyRuntimeMainDepsHandler({
|
||||
...options.appReadyRuntimeMainDeps,
|
||||
reloadConfig,
|
||||
createImmersionTracker: createImmersionTrackerStartupHandler(
|
||||
createBuildImmersionTrackerStartupMainDepsHandler(
|
||||
options.immersionTrackerStartupMainDeps,
|
||||
)(),
|
||||
),
|
||||
createImmersionTracker:
|
||||
options.appReadyRuntimeMainDeps.createImmersionTracker ??
|
||||
createImmersionTrackerStartupHandler(
|
||||
createBuildImmersionTrackerStartupMainDepsHandler(
|
||||
options.immersionTrackerStartupMainDeps,
|
||||
)(),
|
||||
),
|
||||
onCriticalConfigErrors: criticalConfigError,
|
||||
})(),
|
||||
);
|
||||
|
||||
@@ -75,6 +75,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
broadcastToOverlayWindows: () => {},
|
||||
onSubtitleChange: () => {},
|
||||
refreshDiscordPresence: () => {},
|
||||
ensureImmersionTrackerInitialized: () => {},
|
||||
updateCurrentMediaPath: () => {},
|
||||
restoreMpvSubVisibility: () => {},
|
||||
getCurrentAnilistMediaKey: () => null,
|
||||
|
||||
@@ -40,6 +40,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
||||
broadcastToOverlayWindows: (channel, payload) =>
|
||||
calls.push(`broadcast:${channel}:${String(payload)}`),
|
||||
onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`),
|
||||
ensureImmersionTrackerInitialized: () => calls.push('ensure-immersion'),
|
||||
updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
|
||||
restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
|
||||
getCurrentAnilistMediaKey: () => 'media-key',
|
||||
@@ -97,6 +98,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
||||
assert.ok(calls.includes('remote-stopped'));
|
||||
assert.ok(calls.includes('sync-overlay-mpv-sub'));
|
||||
assert.ok(calls.includes('anilist-post-watch'));
|
||||
assert.ok(calls.includes('ensure-immersion'));
|
||||
assert.ok(calls.includes('sync-immersion'));
|
||||
assert.ok(calls.includes('metrics'));
|
||||
assert.ok(calls.includes('presence-refresh'));
|
||||
|
||||
@@ -38,6 +38,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
reportJellyfinRemoteProgress: (forceImmediate: boolean) => void;
|
||||
updateSubtitleRenderMetrics: (patch: Record<string, unknown>) => void;
|
||||
refreshDiscordPresence: () => void;
|
||||
ensureImmersionTrackerInitialized: () => void;
|
||||
}) {
|
||||
return () => ({
|
||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||
@@ -48,8 +49,10 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
scheduleQuitCheck: (callback: () => void) => deps.scheduleQuitCheck(callback),
|
||||
isMpvConnected: () => Boolean(deps.appState.mpvClient?.connected),
|
||||
quitApp: () => deps.quitApp(),
|
||||
recordImmersionSubtitleLine: (text: string, start: number, end: number) =>
|
||||
deps.appState.immersionTracker?.recordSubtitleLine?.(text, start, end),
|
||||
recordImmersionSubtitleLine: (text: string, start: number, end: number) => {
|
||||
deps.ensureImmersionTrackerInitialized();
|
||||
deps.appState.immersionTracker?.recordSubtitleLine?.(text, start, end);
|
||||
},
|
||||
hasSubtitleTimingTracker: () => Boolean(deps.appState.subtitleTimingTracker),
|
||||
recordSubtitleTiming: (text: string, start: number, end: number) =>
|
||||
deps.appState.subtitleTimingTracker?.recordSubtitle?.(text, start, end),
|
||||
@@ -71,8 +74,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
broadcastSecondarySubtitle: (text: string) =>
|
||||
deps.broadcastToOverlayWindows('secondary-subtitle:set', text),
|
||||
updateCurrentMediaPath: (path: string) => deps.updateCurrentMediaPath(path),
|
||||
restoreMpvSubVisibility: () =>
|
||||
deps.restoreMpvSubVisibility(),
|
||||
restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(),
|
||||
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
|
||||
resetAnilistMediaTracking: (mediaKey: string | null) =>
|
||||
deps.resetAnilistMediaTracking(mediaKey),
|
||||
@@ -81,14 +83,19 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
syncImmersionMediaState: () => deps.syncImmersionMediaState(),
|
||||
updateCurrentMediaTitle: (title: string) => deps.updateCurrentMediaTitle(title),
|
||||
resetAnilistMediaGuessState: () => deps.resetAnilistMediaGuessState(),
|
||||
notifyImmersionTitleUpdate: (title: string) =>
|
||||
deps.appState.immersionTracker?.handleMediaTitleUpdate?.(title),
|
||||
recordPlaybackPosition: (time: number) =>
|
||||
deps.appState.immersionTracker?.recordPlaybackPosition?.(time),
|
||||
notifyImmersionTitleUpdate: (title: string) => {
|
||||
deps.ensureImmersionTrackerInitialized();
|
||||
deps.appState.immersionTracker?.handleMediaTitleUpdate?.(title);
|
||||
},
|
||||
recordPlaybackPosition: (time: number) => {
|
||||
deps.ensureImmersionTrackerInitialized();
|
||||
deps.appState.immersionTracker?.recordPlaybackPosition?.(time);
|
||||
},
|
||||
reportJellyfinRemoteProgress: (forceImmediate: boolean) =>
|
||||
deps.reportJellyfinRemoteProgress(forceImmediate),
|
||||
recordPauseState: (paused: boolean) => {
|
||||
deps.appState.playbackPaused = paused;
|
||||
deps.ensureImmersionTrackerInitialized();
|
||||
deps.appState.immersionTracker?.recordPauseState?.(paused);
|
||||
},
|
||||
updateSubtitleRenderMetrics: (patch: Record<string, unknown>) =>
|
||||
|
||||
Reference in New Issue
Block a user