refactor(main): extract anilist/mpv runtime composers

This commit is contained in:
2026-02-20 21:01:24 -08:00
parent 4fc34ec787
commit f8db9e7119
12 changed files with 967 additions and 225 deletions

View File

@@ -0,0 +1,237 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { AnilistMediaGuess } from '../../../core/services/anilist/anilist-updater';
import { composeAnilistTrackingHandlers } from './anilist-tracking-composer';
test('composeAnilistTrackingHandlers returns callable handlers and forwards calls to deps', async () => {
const refreshSavedTokens: string[] = [];
let refreshCachedToken: string | null = null;
let mediaKeyState: string | null = 'media-key';
let mediaDurationSecState: number | null = null;
let mediaGuessState: AnilistMediaGuess | null = null;
let mediaGuessPromiseState: Promise<AnilistMediaGuess | null> | null = null;
let lastDurationProbeAtMsState = 0;
let requestMpvDurationCalls = 0;
let guessAnilistMediaInfoCalls = 0;
let retryUpdateCalls = 0;
let maybeRunUpdateCalls = 0;
const composed = composeAnilistTrackingHandlers({
refreshClientSecretMainDeps: {
getResolvedConfig: () => ({ anilist: { accessToken: 'refresh-token' } }),
isAnilistTrackingEnabled: () => true,
getCachedAccessToken: () => refreshCachedToken,
setCachedAccessToken: (token) => {
refreshCachedToken = token;
},
saveStoredToken: (token) => {
refreshSavedTokens.push(token);
},
loadStoredToken: () => null,
setClientSecretState: () => {},
getAnilistSetupPageOpened: () => false,
setAnilistSetupPageOpened: () => {},
openAnilistSetupWindow: () => {},
now: () => 100,
},
getCurrentMediaKeyMainDeps: {
getCurrentMediaPath: () => ' media-key ',
},
resetMediaTrackingMainDeps: {
setMediaKey: (value) => {
mediaKeyState = value;
},
setMediaDurationSec: (value) => {
mediaDurationSecState = value;
},
setMediaGuess: (value) => {
mediaGuessState = value;
},
setMediaGuessPromise: (value) => {
mediaGuessPromiseState = value;
},
setLastDurationProbeAtMs: (value) => {
lastDurationProbeAtMsState = value;
},
},
getMediaGuessRuntimeStateMainDeps: {
getMediaKey: () => mediaKeyState,
getMediaDurationSec: () => mediaDurationSecState,
getMediaGuess: () => mediaGuessState,
getMediaGuessPromise: () => mediaGuessPromiseState,
getLastDurationProbeAtMs: () => lastDurationProbeAtMsState,
},
setMediaGuessRuntimeStateMainDeps: {
setMediaKey: (value) => {
mediaKeyState = value;
},
setMediaDurationSec: (value) => {
mediaDurationSecState = value;
},
setMediaGuess: (value) => {
mediaGuessState = value;
},
setMediaGuessPromise: (value) => {
mediaGuessPromiseState = value;
},
setLastDurationProbeAtMs: (value) => {
lastDurationProbeAtMsState = value;
},
},
resetMediaGuessStateMainDeps: {
setMediaGuess: (value) => {
mediaGuessState = value;
},
setMediaGuessPromise: (value) => {
mediaGuessPromiseState = value;
},
},
maybeProbeDurationMainDeps: {
getState: () => ({
mediaKey: mediaKeyState,
mediaDurationSec: mediaDurationSecState,
mediaGuess: mediaGuessState,
mediaGuessPromise: mediaGuessPromiseState,
lastDurationProbeAtMs: lastDurationProbeAtMsState,
}),
setState: (state) => {
mediaKeyState = state.mediaKey;
mediaDurationSecState = state.mediaDurationSec;
mediaGuessState = state.mediaGuess;
mediaGuessPromiseState = state.mediaGuessPromise;
lastDurationProbeAtMsState = state.lastDurationProbeAtMs;
},
durationRetryIntervalMs: 0,
now: () => 1000,
requestMpvDuration: async () => {
requestMpvDurationCalls += 1;
return 120;
},
logWarn: () => {},
},
ensureMediaGuessMainDeps: {
getState: () => ({
mediaKey: mediaKeyState,
mediaDurationSec: mediaDurationSecState,
mediaGuess: mediaGuessState,
mediaGuessPromise: mediaGuessPromiseState,
lastDurationProbeAtMs: lastDurationProbeAtMsState,
}),
setState: (state) => {
mediaKeyState = state.mediaKey;
mediaDurationSecState = state.mediaDurationSec;
mediaGuessState = state.mediaGuess;
mediaGuessPromiseState = state.mediaGuessPromise;
lastDurationProbeAtMsState = state.lastDurationProbeAtMs;
},
resolveMediaPathForJimaku: (value) => value,
getCurrentMediaPath: () => '/tmp/media.mkv',
getCurrentMediaTitle: () => 'Episode title',
guessAnilistMediaInfo: async () => {
guessAnilistMediaInfoCalls += 1;
return { title: 'Episode title', episode: 7, source: 'guessit' };
},
},
processNextRetryUpdateMainDeps: {
nextReady: () => ({ key: 'retry-key', title: 'Retry title', episode: 1 }),
refreshRetryQueueState: () => {},
setLastAttemptAt: () => {},
setLastError: () => {},
refreshAnilistClientSecretState: async () => 'retry-token',
updateAnilistPostWatchProgress: async () => {
retryUpdateCalls += 1;
return { status: 'updated', message: 'ok' };
},
markSuccess: () => {},
rememberAttemptedUpdateKey: () => {},
markFailure: () => {},
logInfo: () => {},
now: () => 1,
},
maybeRunPostWatchUpdateMainDeps: {
getInFlight: () => false,
setInFlight: () => {},
getResolvedConfig: () => ({ tracking: true }),
isAnilistTrackingEnabled: () => true,
getCurrentMediaKey: () => 'media-key',
hasMpvClient: () => true,
getTrackedMediaKey: () => 'media-key',
resetTrackedMedia: () => {},
getWatchedSeconds: () => 500,
maybeProbeAnilistDuration: async () => 600,
ensureAnilistMediaGuess: async () => ({
title: 'Episode title',
episode: 2,
source: 'guessit',
}),
hasAttemptedUpdateKey: () => false,
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
refreshAnilistClientSecretState: async () => 'run-token',
enqueueRetry: () => {},
markRetryFailure: () => {},
markRetrySuccess: () => {},
refreshRetryQueueState: () => {},
updateAnilistPostWatchProgress: async () => {
maybeRunUpdateCalls += 1;
return { status: 'updated', message: 'updated from maybeRun' };
},
rememberAttemptedUpdateKey: () => {},
showMpvOsd: () => {},
logInfo: () => {},
logWarn: () => {},
minWatchSeconds: 10,
minWatchRatio: 0.5,
},
});
assert.equal(typeof composed.refreshAnilistClientSecretState, 'function');
assert.equal(typeof composed.getCurrentAnilistMediaKey, 'function');
assert.equal(typeof composed.resetAnilistMediaTracking, 'function');
assert.equal(typeof composed.getAnilistMediaGuessRuntimeState, 'function');
assert.equal(typeof composed.setAnilistMediaGuessRuntimeState, 'function');
assert.equal(typeof composed.resetAnilistMediaGuessState, 'function');
assert.equal(typeof composed.maybeProbeAnilistDuration, 'function');
assert.equal(typeof composed.ensureAnilistMediaGuess, 'function');
assert.equal(typeof composed.processNextAnilistRetryUpdate, 'function');
assert.equal(typeof composed.maybeRunAnilistPostWatchUpdate, 'function');
const refreshed = await composed.refreshAnilistClientSecretState({ force: true });
assert.equal(refreshed, 'refresh-token');
assert.deepEqual(refreshSavedTokens, ['refresh-token']);
assert.equal(composed.getCurrentAnilistMediaKey(), 'media-key');
composed.resetAnilistMediaTracking('next-key');
assert.equal(mediaKeyState, 'next-key');
assert.equal(mediaDurationSecState, null);
composed.setAnilistMediaGuessRuntimeState({
mediaKey: 'media-key',
mediaDurationSec: 90,
mediaGuess: { title: 'Known', episode: 3, source: 'fallback' },
mediaGuessPromise: null,
lastDurationProbeAtMs: 11,
});
assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaDurationSec, 90);
composed.resetAnilistMediaGuessState();
assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaGuess, null);
mediaKeyState = 'media-key';
mediaDurationSecState = null;
const probedDuration = await composed.maybeProbeAnilistDuration('media-key');
assert.equal(probedDuration, 120);
assert.equal(requestMpvDurationCalls, 1);
mediaGuessState = null;
await composed.ensureAnilistMediaGuess('media-key');
assert.equal(guessAnilistMediaInfoCalls, 1);
const retryResult = await composed.processNextAnilistRetryUpdate();
assert.deepEqual(retryResult, { ok: true, message: 'ok' });
assert.equal(retryUpdateCalls, 1);
await composed.maybeRunAnilistPostWatchUpdate();
assert.equal(maybeRunUpdateCalls, 1);
});

View File

@@ -0,0 +1,128 @@
import {
createBuildEnsureAnilistMediaGuessMainDepsHandler,
createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler,
createBuildGetCurrentAnilistMediaKeyMainDepsHandler,
createBuildMaybeProbeAnilistDurationMainDepsHandler,
createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler,
createBuildProcessNextAnilistRetryUpdateMainDepsHandler,
createBuildRefreshAnilistClientSecretStateMainDepsHandler,
createBuildResetAnilistMediaGuessStateMainDepsHandler,
createBuildResetAnilistMediaTrackingMainDepsHandler,
createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler,
createEnsureAnilistMediaGuessHandler,
createGetAnilistMediaGuessRuntimeStateHandler,
createGetCurrentAnilistMediaKeyHandler,
createMaybeProbeAnilistDurationHandler,
createMaybeRunAnilistPostWatchUpdateHandler,
createProcessNextAnilistRetryUpdateHandler,
createRefreshAnilistClientSecretStateHandler,
createResetAnilistMediaGuessStateHandler,
createResetAnilistMediaTrackingHandler,
createSetAnilistMediaGuessRuntimeStateHandler,
} from '../domains/anilist';
export type AnilistTrackingComposerOptions = {
refreshClientSecretMainDeps: Parameters<
typeof createBuildRefreshAnilistClientSecretStateMainDepsHandler
>[0];
getCurrentMediaKeyMainDeps: Parameters<
typeof createBuildGetCurrentAnilistMediaKeyMainDepsHandler
>[0];
resetMediaTrackingMainDeps: Parameters<
typeof createBuildResetAnilistMediaTrackingMainDepsHandler
>[0];
getMediaGuessRuntimeStateMainDeps: Parameters<
typeof createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler
>[0];
setMediaGuessRuntimeStateMainDeps: Parameters<
typeof createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler
>[0];
resetMediaGuessStateMainDeps: Parameters<
typeof createBuildResetAnilistMediaGuessStateMainDepsHandler
>[0];
maybeProbeDurationMainDeps: Parameters<
typeof createBuildMaybeProbeAnilistDurationMainDepsHandler
>[0];
ensureMediaGuessMainDeps: Parameters<typeof createBuildEnsureAnilistMediaGuessMainDepsHandler>[0];
processNextRetryUpdateMainDeps: Parameters<
typeof createBuildProcessNextAnilistRetryUpdateMainDepsHandler
>[0];
maybeRunPostWatchUpdateMainDeps: Parameters<
typeof createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler
>[0];
};
export type AnilistTrackingComposerResult = {
refreshAnilistClientSecretState: ReturnType<typeof createRefreshAnilistClientSecretStateHandler>;
getCurrentAnilistMediaKey: ReturnType<typeof createGetCurrentAnilistMediaKeyHandler>;
resetAnilistMediaTracking: ReturnType<typeof createResetAnilistMediaTrackingHandler>;
getAnilistMediaGuessRuntimeState: ReturnType<
typeof createGetAnilistMediaGuessRuntimeStateHandler
>;
setAnilistMediaGuessRuntimeState: ReturnType<
typeof createSetAnilistMediaGuessRuntimeStateHandler
>;
resetAnilistMediaGuessState: ReturnType<typeof createResetAnilistMediaGuessStateHandler>;
maybeProbeAnilistDuration: ReturnType<typeof createMaybeProbeAnilistDurationHandler>;
ensureAnilistMediaGuess: ReturnType<typeof createEnsureAnilistMediaGuessHandler>;
processNextAnilistRetryUpdate: ReturnType<typeof createProcessNextAnilistRetryUpdateHandler>;
maybeRunAnilistPostWatchUpdate: ReturnType<typeof createMaybeRunAnilistPostWatchUpdateHandler>;
};
export function composeAnilistTrackingHandlers(
options: AnilistTrackingComposerOptions,
): AnilistTrackingComposerResult {
const refreshAnilistClientSecretState = createRefreshAnilistClientSecretStateHandler(
createBuildRefreshAnilistClientSecretStateMainDepsHandler(
options.refreshClientSecretMainDeps,
)(),
);
const getCurrentAnilistMediaKey = createGetCurrentAnilistMediaKeyHandler(
createBuildGetCurrentAnilistMediaKeyMainDepsHandler(options.getCurrentMediaKeyMainDeps)(),
);
const resetAnilistMediaTracking = createResetAnilistMediaTrackingHandler(
createBuildResetAnilistMediaTrackingMainDepsHandler(options.resetMediaTrackingMainDeps)(),
);
const getAnilistMediaGuessRuntimeState = createGetAnilistMediaGuessRuntimeStateHandler(
createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler(
options.getMediaGuessRuntimeStateMainDeps,
)(),
);
const setAnilistMediaGuessRuntimeState = createSetAnilistMediaGuessRuntimeStateHandler(
createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler(
options.setMediaGuessRuntimeStateMainDeps,
)(),
);
const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler(
createBuildResetAnilistMediaGuessStateMainDepsHandler(options.resetMediaGuessStateMainDeps)(),
);
const maybeProbeAnilistDuration = createMaybeProbeAnilistDurationHandler(
createBuildMaybeProbeAnilistDurationMainDepsHandler(options.maybeProbeDurationMainDeps)(),
);
const ensureAnilistMediaGuess = createEnsureAnilistMediaGuessHandler(
createBuildEnsureAnilistMediaGuessMainDepsHandler(options.ensureMediaGuessMainDeps)(),
);
const processNextAnilistRetryUpdate = createProcessNextAnilistRetryUpdateHandler(
createBuildProcessNextAnilistRetryUpdateMainDepsHandler(
options.processNextRetryUpdateMainDeps,
)(),
);
const maybeRunAnilistPostWatchUpdate = createMaybeRunAnilistPostWatchUpdateHandler(
createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler(
options.maybeRunPostWatchUpdateMainDeps,
)(),
);
return {
refreshAnilistClientSecretState,
getCurrentAnilistMediaKey,
resetAnilistMediaTracking,
getAnilistMediaGuessRuntimeState,
setAnilistMediaGuessRuntimeState,
resetAnilistMediaGuessState,
maybeProbeAnilistDuration,
ensureAnilistMediaGuess,
processNextAnilistRetryUpdate,
maybeRunAnilistPostWatchUpdate,
};
}

View File

@@ -0,0 +1,8 @@
export * from './anilist-setup-composer';
export * from './anilist-tracking-composer';
export * from './app-ready-composer';
export * from './ipc-runtime-composer';
export * from './jellyfin-remote-composer';
export * from './mpv-runtime-composer';
export * from './shortcuts-runtime-composer';
export * from './startup-lifecycle-composer';

View File

@@ -0,0 +1,216 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { MpvSubtitleRenderMetrics } from '../../../types';
import { composeMpvRuntimeHandlers } from './mpv-runtime-composer';
const BASE_METRICS: MpvSubtitleRenderMetrics = {
subPos: 100,
subFontSize: 36,
subScale: 1,
subMarginY: 0,
subMarginX: 0,
subFont: '',
subSpacing: 0,
subBold: false,
subItalic: false,
subBorderSize: 0,
subShadowOffset: 0,
subAssOverride: 'yes',
subScaleByWindow: true,
subUseMargins: true,
osdHeight: 0,
osdDimensions: null,
};
test('composeMpvRuntimeHandlers returns callable handlers and forwards to injected deps', async () => {
const calls: string[] = [];
let started = false;
let metrics = BASE_METRICS;
class FakeMpvClient {
connected = false;
constructor(
public socketPath: string,
public options: unknown,
) {
const autoStartOverlay = (options as { autoStartOverlay: boolean }).autoStartOverlay;
calls.push(`create-client:${socketPath}`);
calls.push(`auto-start:${String(autoStartOverlay)}`);
}
on(): void {}
connect(): void {
this.connected = true;
calls.push('client-connect');
}
}
const composed = composeMpvRuntimeHandlers<
{ isKnownWord: (text: string) => boolean },
{ text: string }
>({
bindMpvMainEventHandlersMainDeps: {
appState: {
initialArgs: null,
overlayRuntimeInitialized: true,
mpvClient: null,
immersionTracker: null,
subtitleTimingTracker: null,
currentSubText: '',
currentSubAssText: '',
previousSecondarySubVisibility: null,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
quitApp: () => {},
reportJellyfinRemoteStopped: () => {},
maybeRunAnilistPostWatchUpdate: async () => {},
logSubtitleTimingError: () => {},
broadcastToOverlayWindows: () => {},
onSubtitleChange: () => {},
updateCurrentMediaPath: () => {},
getCurrentAnilistMediaKey: () => null,
resetAnilistMediaTracking: () => {},
maybeProbeAnilistDuration: () => {},
ensureAnilistMediaGuess: () => {},
syncImmersionMediaState: () => {},
updateCurrentMediaTitle: () => {},
resetAnilistMediaGuessState: () => {},
reportJellyfinRemoteProgress: () => {},
updateSubtitleRenderMetrics: () => {},
},
mpvClientRuntimeServiceFactoryMainDeps: {
createClient: FakeMpvClient,
getSocketPath: () => '/tmp/mpv.sock',
getResolvedConfig: () => ({}) as never,
isAutoStartOverlayEnabled: () => true,
setOverlayVisible: () => {},
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => false,
getReconnectTimer: () => null,
setReconnectTimer: () => {},
},
updateMpvSubtitleRenderMetricsMainDeps: {
getCurrentMetrics: () => metrics,
setCurrentMetrics: (next) => {
metrics = next;
calls.push('set-metrics');
},
applyPatch: (current, patch) => {
calls.push('apply-metrics-patch');
return { next: { ...current, ...patch }, changed: true };
},
broadcastMetrics: () => {
calls.push('broadcast-metrics');
},
},
tokenizer: {
buildTokenizerDepsMainDeps: {
getYomitanExt: () => null,
getYomitanParserWindow: () => null,
setYomitanParserWindow: () => {},
getYomitanParserReadyPromise: () => null,
setYomitanParserReadyPromise: () => {},
getYomitanParserInitPromise: () => null,
setYomitanParserInitPromise: () => {},
isKnownWord: (text) => text === 'known',
recordLookup: () => {},
getKnownWordMatchMode: () => 'exact',
getMinSentenceWordsForNPlusOne: () => 3,
getJlptLevel: () => null,
getJlptEnabled: () => true,
getFrequencyDictionaryEnabled: () => true,
getFrequencyRank: () => null,
getYomitanGroupDebugEnabled: () => false,
getMecabTokenizer: () => null,
},
createTokenizerRuntimeDeps: (deps) => {
calls.push('create-tokenizer-runtime-deps');
return { isKnownWord: (text: string) => deps.isKnownWord(text) };
},
tokenizeSubtitle: async (text, deps) => {
calls.push(`tokenize:${text}`);
deps.isKnownWord('known');
return { text };
},
createMecabTokenizerAndCheckMainDeps: {
getMecabTokenizer: () => ({ id: 'mecab' }),
setMecabTokenizer: () => {},
createMecabTokenizer: () => ({ id: 'mecab' }),
checkAvailability: async () => {
calls.push('check-mecab');
},
},
prewarmSubtitleDictionariesMainDeps: {
ensureJlptDictionaryLookup: async () => {
calls.push('prewarm-jlpt');
},
ensureFrequencyDictionaryLookup: async () => {
calls.push('prewarm-frequency');
},
},
},
warmups: {
launchBackgroundWarmupTaskMainDeps: {
now: () => 100,
logDebug: () => {
calls.push('warmup-debug');
},
logWarn: () => {
calls.push('warmup-warn');
},
},
startBackgroundWarmupsMainDeps: {
getStarted: () => started,
setStarted: (next) => {
started = next;
calls.push(`set-started:${String(next)}`);
},
isTexthookerOnlyMode: () => false,
ensureYomitanExtensionLoaded: async () => {
calls.push('warmup-yomitan');
},
shouldAutoConnectJellyfinRemote: () => false,
startJellyfinRemoteSession: async () => {
calls.push('warmup-jellyfin');
},
},
},
});
assert.equal(typeof composed.bindMpvClientEventHandlers, 'function');
assert.equal(typeof composed.createMpvClientRuntimeService, 'function');
assert.equal(typeof composed.updateMpvSubtitleRenderMetrics, 'function');
assert.equal(typeof composed.tokenizeSubtitle, 'function');
assert.equal(typeof composed.createMecabTokenizerAndCheck, 'function');
assert.equal(typeof composed.prewarmSubtitleDictionaries, 'function');
assert.equal(typeof composed.launchBackgroundWarmupTask, 'function');
assert.equal(typeof composed.startBackgroundWarmups, 'function');
const client = composed.createMpvClientRuntimeService() as FakeMpvClient;
assert.equal(client.connected, true);
composed.updateMpvSubtitleRenderMetrics({ subPos: 90 });
const tokenized = await composed.tokenizeSubtitle('subtitle text');
await composed.createMecabTokenizerAndCheck();
await composed.prewarmSubtitleDictionaries();
composed.startBackgroundWarmups();
assert.deepEqual(tokenized, { text: 'subtitle text' });
assert.equal(metrics.subPos, 90);
assert.ok(calls.includes('create-client:/tmp/mpv.sock'));
assert.ok(calls.includes('auto-start:true'));
assert.ok(calls.includes('client-connect'));
assert.ok(calls.includes('apply-metrics-patch'));
assert.ok(calls.includes('set-metrics'));
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('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'));
});

View File

@@ -0,0 +1,142 @@
import { createBindMpvMainEventHandlersHandler } from '../mpv-main-event-bindings';
import { createBuildBindMpvMainEventHandlersMainDepsHandler } from '../mpv-main-event-main-deps';
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from '../mpv-client-runtime-service-main-deps';
import { createMpvClientRuntimeServiceFactory } from '../mpv-client-runtime-service';
import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from '../mpv-subtitle-render-metrics-main-deps';
import { createUpdateMpvSubtitleRenderMetricsHandler } from '../mpv-subtitle-render-metrics';
import {
createBuildTokenizerDepsMainHandler,
createCreateMecabTokenizerAndCheckMainHandler,
createPrewarmSubtitleDictionariesMainHandler,
} from '../subtitle-tokenization-main-deps';
import {
createBuildLaunchBackgroundWarmupTaskMainDepsHandler,
createBuildStartBackgroundWarmupsMainDepsHandler,
} from '../startup-warmups-main-deps';
import {
createLaunchBackgroundWarmupTaskHandler as createLaunchBackgroundWarmupTaskFromStartup,
createStartBackgroundWarmupsHandler as createStartBackgroundWarmupsFromStartup,
} from '../startup-warmups';
type BindMpvMainEventHandlersMainDeps = Parameters<
typeof createBuildBindMpvMainEventHandlersMainDepsHandler
>[0];
type MpvClientRuntimeServiceFactoryMainDeps = Omit<
Parameters<typeof createBuildMpvClientRuntimeServiceFactoryDepsHandler>[0],
'bindEventHandlers'
>;
type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<
typeof createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler
>[0];
type BuildTokenizerDepsMainDeps = Parameters<typeof createBuildTokenizerDepsMainHandler>[0];
type TokenizerMainDeps = ReturnType<ReturnType<typeof createBuildTokenizerDepsMainHandler>>;
type CreateMecabTokenizerAndCheckMainDeps = Parameters<
typeof createCreateMecabTokenizerAndCheckMainHandler
>[0];
type PrewarmSubtitleDictionariesMainDeps = Parameters<
typeof createPrewarmSubtitleDictionariesMainHandler
>[0];
type LaunchBackgroundWarmupTaskMainDeps = Parameters<
typeof createBuildLaunchBackgroundWarmupTaskMainDepsHandler
>[0];
type StartBackgroundWarmupsMainDeps = Omit<
Parameters<typeof createBuildStartBackgroundWarmupsMainDepsHandler>[0],
'launchTask' | 'createMecabTokenizerAndCheck' | 'prewarmSubtitleDictionaries'
>;
export type MpvRuntimeComposerOptions<TTokenizerRuntimeDeps, TTokenizedSubtitle> = {
bindMpvMainEventHandlersMainDeps: BindMpvMainEventHandlersMainDeps;
mpvClientRuntimeServiceFactoryMainDeps: MpvClientRuntimeServiceFactoryMainDeps;
updateMpvSubtitleRenderMetricsMainDeps: UpdateMpvSubtitleRenderMetricsMainDeps;
tokenizer: {
buildTokenizerDepsMainDeps: BuildTokenizerDepsMainDeps;
createTokenizerRuntimeDeps: (deps: TokenizerMainDeps) => TTokenizerRuntimeDeps;
tokenizeSubtitle: (text: string, deps: TTokenizerRuntimeDeps) => Promise<TTokenizedSubtitle>;
createMecabTokenizerAndCheckMainDeps: CreateMecabTokenizerAndCheckMainDeps;
prewarmSubtitleDictionariesMainDeps: PrewarmSubtitleDictionariesMainDeps;
};
warmups: {
launchBackgroundWarmupTaskMainDeps: LaunchBackgroundWarmupTaskMainDeps;
startBackgroundWarmupsMainDeps: StartBackgroundWarmupsMainDeps;
};
};
export type MpvRuntimeComposerResult<TTokenizedSubtitle> = {
bindMpvClientEventHandlers: ReturnType<typeof createBindMpvMainEventHandlersHandler>;
createMpvClientRuntimeService: () => unknown;
updateMpvSubtitleRenderMetrics: ReturnType<typeof createUpdateMpvSubtitleRenderMetricsHandler>;
tokenizeSubtitle: (text: string) => Promise<TTokenizedSubtitle>;
createMecabTokenizerAndCheck: () => Promise<void>;
prewarmSubtitleDictionaries: () => Promise<void>;
launchBackgroundWarmupTask: ReturnType<typeof createLaunchBackgroundWarmupTaskFromStartup>;
startBackgroundWarmups: ReturnType<typeof createStartBackgroundWarmupsFromStartup>;
};
export function composeMpvRuntimeHandlers<TTokenizerRuntimeDeps, TTokenizedSubtitle>(
options: MpvRuntimeComposerOptions<TTokenizerRuntimeDeps, TTokenizedSubtitle>,
): MpvRuntimeComposerResult<TTokenizedSubtitle> {
const bindMpvMainEventHandlersMainDeps = createBuildBindMpvMainEventHandlersMainDepsHandler(
options.bindMpvMainEventHandlersMainDeps,
)();
const bindMpvClientEventHandlers = createBindMpvMainEventHandlersHandler(
bindMpvMainEventHandlersMainDeps,
);
const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
createBuildMpvClientRuntimeServiceFactoryDepsHandler({
...options.mpvClientRuntimeServiceFactoryMainDeps,
bindEventHandlers: (client) => bindMpvClientEventHandlers(client as never),
});
const createMpvClientRuntimeService = (): unknown =>
createMpvClientRuntimeServiceFactory(
buildMpvClientRuntimeServiceFactoryMainDepsHandler() as never,
)();
const updateMpvSubtitleRenderMetrics = createUpdateMpvSubtitleRenderMetricsHandler(
createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler(
options.updateMpvSubtitleRenderMetricsMainDeps,
)(),
);
const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler(
options.tokenizer.buildTokenizerDepsMainDeps,
);
const createMecabTokenizerAndCheck = createCreateMecabTokenizerAndCheckMainHandler(
options.tokenizer.createMecabTokenizerAndCheckMainDeps,
);
const prewarmSubtitleDictionaries = createPrewarmSubtitleDictionariesMainHandler(
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
);
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
await prewarmSubtitleDictionaries();
return options.tokenizer.tokenizeSubtitle(
text,
options.tokenizer.createTokenizerRuntimeDeps(buildTokenizerDepsHandler()),
);
};
const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskFromStartup(
createBuildLaunchBackgroundWarmupTaskMainDepsHandler(
options.warmups.launchBackgroundWarmupTaskMainDeps,
)(),
);
const startBackgroundWarmups = createStartBackgroundWarmupsFromStartup(
createBuildStartBackgroundWarmupsMainDepsHandler({
...options.warmups.startBackgroundWarmupsMainDeps,
launchTask: (label, task) => launchBackgroundWarmupTask(label, task),
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
})(),
);
return {
bindMpvClientEventHandlers: (client) => bindMpvClientEventHandlers(client),
createMpvClientRuntimeService,
updateMpvSubtitleRenderMetrics: (patch) => updateMpvSubtitleRenderMetrics(patch),
tokenizeSubtitle,
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
launchBackgroundWarmupTask: (label, task) => launchBackgroundWarmupTask(label, task),
startBackgroundWarmups: () => startBackgroundWarmups(),
};
}