mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor: extract app-ready startup orchestration service
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
||||
"test:config": "pnpm run build && node --test dist/config/config.test.js",
|
||||
"test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js",
|
||||
"test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js",
|
||||
"test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js",
|
||||
"generate:config-example": "pnpm run build && node dist/generate-config-example.js",
|
||||
"start": "pnpm run build && electron . --start",
|
||||
|
||||
57
src/core/services/app-ready-runtime-service.test.ts
Normal file
57
src/core/services/app-ready-runtime-service.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { AppReadyRuntimeDeps, runAppReadyRuntimeService } from "./app-ready-runtime-service";
|
||||
|
||||
function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
|
||||
const calls: string[] = [];
|
||||
const deps: AppReadyRuntimeDeps = {
|
||||
loadSubtitlePosition: () => calls.push("loadSubtitlePosition"),
|
||||
resolveKeybindings: () => calls.push("resolveKeybindings"),
|
||||
createMpvClient: () => calls.push("createMpvClient"),
|
||||
reloadConfig: () => calls.push("reloadConfig"),
|
||||
getResolvedConfig: () => ({ websocket: { enabled: "auto" }, secondarySub: {} }),
|
||||
getConfigWarnings: () => [],
|
||||
logConfigWarning: () => calls.push("logConfigWarning"),
|
||||
initRuntimeOptionsManager: () => calls.push("initRuntimeOptionsManager"),
|
||||
setSecondarySubMode: (mode) => calls.push(`setSecondarySubMode:${mode}`),
|
||||
defaultSecondarySubMode: "hover",
|
||||
defaultWebsocketPort: 9001,
|
||||
hasMpvWebsocketPlugin: () => true,
|
||||
startSubtitleWebsocket: (port) => calls.push(`startSubtitleWebsocket:${port}`),
|
||||
log: (message) => calls.push(`log:${message}`),
|
||||
createMecabTokenizerAndCheck: async () => {
|
||||
calls.push("createMecabTokenizerAndCheck");
|
||||
},
|
||||
createSubtitleTimingTracker: () => calls.push("createSubtitleTimingTracker"),
|
||||
loadYomitanExtension: async () => {
|
||||
calls.push("loadYomitanExtension");
|
||||
},
|
||||
texthookerOnlyMode: false,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => true,
|
||||
initializeOverlayRuntime: () => calls.push("initializeOverlayRuntime"),
|
||||
handleInitialArgs: () => calls.push("handleInitialArgs"),
|
||||
...overrides,
|
||||
};
|
||||
return { deps, calls };
|
||||
}
|
||||
|
||||
test("runAppReadyRuntimeService starts websocket in auto mode when plugin missing", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
hasMpvWebsocketPlugin: () => false,
|
||||
});
|
||||
await runAppReadyRuntimeService(deps);
|
||||
assert.ok(calls.includes("startSubtitleWebsocket:9001"));
|
||||
assert.ok(calls.includes("initializeOverlayRuntime"));
|
||||
});
|
||||
|
||||
test("runAppReadyRuntimeService logs defer message when overlay not auto-started", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
|
||||
});
|
||||
await runAppReadyRuntimeService(deps);
|
||||
assert.ok(
|
||||
calls.includes(
|
||||
"log:Overlay runtime deferred: waiting for explicit overlay command.",
|
||||
),
|
||||
);
|
||||
});
|
||||
77
src/core/services/app-ready-runtime-service.ts
Normal file
77
src/core/services/app-ready-runtime-service.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ConfigValidationWarning, SecondarySubMode } from "../../types";
|
||||
|
||||
interface AppReadyConfigLike {
|
||||
secondarySub?: {
|
||||
defaultMode?: SecondarySubMode;
|
||||
};
|
||||
websocket?: {
|
||||
enabled?: boolean | "auto";
|
||||
port?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppReadyRuntimeDeps {
|
||||
loadSubtitlePosition: () => void;
|
||||
resolveKeybindings: () => void;
|
||||
createMpvClient: () => void;
|
||||
reloadConfig: () => void;
|
||||
getResolvedConfig: () => AppReadyConfigLike;
|
||||
getConfigWarnings: () => ConfigValidationWarning[];
|
||||
logConfigWarning: (warning: ConfigValidationWarning) => void;
|
||||
initRuntimeOptionsManager: () => void;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
defaultSecondarySubMode: SecondarySubMode;
|
||||
defaultWebsocketPort: number;
|
||||
hasMpvWebsocketPlugin: () => boolean;
|
||||
startSubtitleWebsocket: (port: number) => void;
|
||||
log: (message: string) => void;
|
||||
createMecabTokenizerAndCheck: () => Promise<void>;
|
||||
createSubtitleTimingTracker: () => void;
|
||||
loadYomitanExtension: () => Promise<void>;
|
||||
texthookerOnlyMode: boolean;
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean;
|
||||
initializeOverlayRuntime: () => void;
|
||||
handleInitialArgs: () => void;
|
||||
}
|
||||
|
||||
export async function runAppReadyRuntimeService(
|
||||
deps: AppReadyRuntimeDeps,
|
||||
): Promise<void> {
|
||||
deps.loadSubtitlePosition();
|
||||
deps.resolveKeybindings();
|
||||
deps.createMpvClient();
|
||||
|
||||
deps.reloadConfig();
|
||||
const config = deps.getResolvedConfig();
|
||||
for (const warning of deps.getConfigWarnings()) {
|
||||
deps.logConfigWarning(warning);
|
||||
}
|
||||
deps.initRuntimeOptionsManager();
|
||||
deps.setSecondarySubMode(
|
||||
config.secondarySub?.defaultMode ?? deps.defaultSecondarySubMode,
|
||||
);
|
||||
|
||||
const wsConfig = config.websocket || {};
|
||||
const wsEnabled = wsConfig.enabled ?? "auto";
|
||||
const wsPort = wsConfig.port || deps.defaultWebsocketPort;
|
||||
|
||||
if (wsEnabled === true || (wsEnabled === "auto" && !deps.hasMpvWebsocketPlugin())) {
|
||||
deps.startSubtitleWebsocket(wsPort);
|
||||
} else if (wsEnabled === "auto") {
|
||||
deps.log("mpv_websocket detected, skipping built-in WebSocket server");
|
||||
}
|
||||
|
||||
await deps.createMecabTokenizerAndCheck();
|
||||
deps.createSubtitleTimingTracker();
|
||||
await deps.loadYomitanExtension();
|
||||
|
||||
if (deps.texthookerOnlyMode) {
|
||||
deps.log("Texthooker-only mode enabled; skipping overlay window.");
|
||||
} else if (deps.shouldAutoInitializeOverlayRuntimeFromConfig()) {
|
||||
deps.initializeOverlayRuntime();
|
||||
} else {
|
||||
deps.log("Overlay runtime deferred: waiting for explicit overlay command.");
|
||||
}
|
||||
|
||||
deps.handleInitialArgs();
|
||||
}
|
||||
143
src/main.ts
143
src/main.ts
@@ -201,6 +201,7 @@ import {
|
||||
getOverlayWindowsRuntimeService,
|
||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||
} from "./core/services/overlay-broadcast-runtime-service";
|
||||
import { runAppReadyRuntimeService } from "./core/services/app-ready-runtime-service";
|
||||
import {
|
||||
runSubsyncManualFromIpcRuntimeService,
|
||||
triggerSubsyncFromConfigRuntimeService,
|
||||
@@ -487,76 +488,80 @@ if (initialArgs.generateConfig && !shouldStartApp(initialArgs)) {
|
||||
},
|
||||
isDarwinPlatform: () => process.platform === "darwin",
|
||||
onReady: async () => {
|
||||
loadSubtitlePosition();
|
||||
keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
||||
|
||||
mpvClient = new MpvIpcClient(mpvSocketPath, {
|
||||
getResolvedConfig: () => getResolvedConfig(), autoStartOverlay,
|
||||
setOverlayVisible: (visible) => setOverlayVisible(visible),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getReconnectTimer: () => reconnectTimer, setReconnectTimer: (timer) => { reconnectTimer = timer; },
|
||||
getCurrentSubText: () => currentSubText, setCurrentSubText: (text) => { currentSubText = text; }, setCurrentSubAssText: (text) => { currentSubAssText = text; },
|
||||
getSubtitleTimingTracker: () => subtitleTimingTracker, subtitleWsBroadcast: (text) => { subtitleWsService.broadcast(text); },
|
||||
getOverlayWindowsCount: () => getOverlayWindows().length, tokenizeSubtitle: (text) => tokenizeSubtitle(text),
|
||||
broadcastToOverlayWindows: (channel, ...args) => { broadcastToOverlayWindows(channel, ...args); },
|
||||
updateCurrentMediaPath: (mediaPath) => { updateCurrentMediaPath(mediaPath); }, updateMpvSubtitleRenderMetrics: (patch) => { updateMpvSubtitleRenderMetrics(patch); },
|
||||
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, setPreviousSecondarySubVisibility: (value) => { previousSecondarySubVisibility = value; }, showMpvOsd: (text) => { showMpvOsd(text); },
|
||||
});
|
||||
|
||||
configService.reloadConfig();
|
||||
const config = getResolvedConfig();
|
||||
for (const warning of configService.getWarnings()) {
|
||||
console.warn(
|
||||
`[config] ${warning.path}: ${warning.message} value=${JSON.stringify(warning.value)} fallback=${JSON.stringify(warning.fallback)}`,
|
||||
);
|
||||
}
|
||||
runtimeOptionsManager = new RuntimeOptionsManager(
|
||||
() => configService.getConfig().ankiConnect,
|
||||
{
|
||||
applyAnkiPatch: (patch) => {
|
||||
if (ankiIntegration) {
|
||||
ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||
}
|
||||
},
|
||||
onOptionsChanged: () => {
|
||||
broadcastRuntimeOptionsChanged();
|
||||
refreshOverlayShortcuts();
|
||||
},
|
||||
await runAppReadyRuntimeService({
|
||||
loadSubtitlePosition: () => loadSubtitlePosition(),
|
||||
resolveKeybindings: () => {
|
||||
keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
||||
},
|
||||
);
|
||||
secondarySubMode = config.secondarySub?.defaultMode ?? "hover";
|
||||
const wsConfig = config.websocket || {};
|
||||
const wsEnabled = wsConfig.enabled ?? "auto";
|
||||
const wsPort = wsConfig.port || DEFAULT_CONFIG.websocket.port;
|
||||
|
||||
if (
|
||||
wsEnabled === true ||
|
||||
(wsEnabled === "auto" && !hasMpvWebsocketPlugin())
|
||||
) {
|
||||
subtitleWsService.start(wsPort, () => currentSubText);
|
||||
} else if (wsEnabled === "auto") {
|
||||
console.log(
|
||||
"mpv_websocket detected, skipping built-in WebSocket server",
|
||||
);
|
||||
}
|
||||
|
||||
mecabTokenizer = new MecabTokenizer();
|
||||
await mecabTokenizer.checkAvailability();
|
||||
|
||||
subtitleTimingTracker = new SubtitleTimingTracker();
|
||||
|
||||
await loadYomitanExtension();
|
||||
if (texthookerOnlyMode) {
|
||||
console.log("Texthooker-only mode enabled; skipping overlay window.");
|
||||
} else if (shouldAutoInitializeOverlayRuntimeFromConfig()) {
|
||||
initializeOverlayRuntime();
|
||||
} else {
|
||||
console.log(
|
||||
"Overlay runtime deferred: waiting for explicit overlay command.",
|
||||
);
|
||||
}
|
||||
handleInitialArgs();
|
||||
createMpvClient: () => {
|
||||
mpvClient = new MpvIpcClient(mpvSocketPath, {
|
||||
getResolvedConfig: () => getResolvedConfig(), autoStartOverlay,
|
||||
setOverlayVisible: (visible) => setOverlayVisible(visible),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getReconnectTimer: () => reconnectTimer, setReconnectTimer: (timer) => { reconnectTimer = timer; },
|
||||
getCurrentSubText: () => currentSubText, setCurrentSubText: (text) => { currentSubText = text; }, setCurrentSubAssText: (text) => { currentSubAssText = text; },
|
||||
getSubtitleTimingTracker: () => subtitleTimingTracker, subtitleWsBroadcast: (text) => { subtitleWsService.broadcast(text); },
|
||||
getOverlayWindowsCount: () => getOverlayWindows().length, tokenizeSubtitle: (text) => tokenizeSubtitle(text),
|
||||
broadcastToOverlayWindows: (channel, ...args) => { broadcastToOverlayWindows(channel, ...args); },
|
||||
updateCurrentMediaPath: (mediaPath) => { updateCurrentMediaPath(mediaPath); }, updateMpvSubtitleRenderMetrics: (patch) => { updateMpvSubtitleRenderMetrics(patch); },
|
||||
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, setPreviousSecondarySubVisibility: (value) => { previousSecondarySubVisibility = value; }, showMpvOsd: (text) => { showMpvOsd(text); },
|
||||
});
|
||||
},
|
||||
reloadConfig: () => {
|
||||
configService.reloadConfig();
|
||||
},
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
logConfigWarning: (warning) => {
|
||||
console.warn(
|
||||
`[config] ${warning.path}: ${warning.message} value=${JSON.stringify(warning.value)} fallback=${JSON.stringify(warning.fallback)}`,
|
||||
);
|
||||
},
|
||||
initRuntimeOptionsManager: () => {
|
||||
runtimeOptionsManager = new RuntimeOptionsManager(
|
||||
() => configService.getConfig().ankiConnect,
|
||||
{
|
||||
applyAnkiPatch: (patch) => {
|
||||
if (ankiIntegration) {
|
||||
ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||
}
|
||||
},
|
||||
onOptionsChanged: () => {
|
||||
broadcastRuntimeOptionsChanged();
|
||||
refreshOverlayShortcuts();
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
setSecondarySubMode: (mode) => {
|
||||
secondarySubMode = mode;
|
||||
},
|
||||
defaultSecondarySubMode: "hover",
|
||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
|
||||
startSubtitleWebsocket: (port) => {
|
||||
subtitleWsService.start(port, () => currentSubText);
|
||||
},
|
||||
log: (message) => {
|
||||
console.log(message);
|
||||
},
|
||||
createMecabTokenizerAndCheck: async () => {
|
||||
mecabTokenizer = new MecabTokenizer();
|
||||
await mecabTokenizer.checkAvailability();
|
||||
},
|
||||
createSubtitleTimingTracker: () => {
|
||||
subtitleTimingTracker = new SubtitleTimingTracker();
|
||||
},
|
||||
loadYomitanExtension: async () => {
|
||||
await loadYomitanExtension();
|
||||
},
|
||||
texthookerOnlyMode,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () =>
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig(),
|
||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||
handleInitialArgs: () => handleInitialArgs(),
|
||||
});
|
||||
},
|
||||
onWillQuitCleanup: () => {
|
||||
globalShortcut.unregisterAll();
|
||||
|
||||
Reference in New Issue
Block a user