mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract app-ready startup orchestration service
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
"docs:build": "vitepress build docs",
|
"docs:build": "vitepress build docs",
|
||||||
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
"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: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",
|
"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",
|
"generate:config-example": "pnpm run build && node dist/generate-config-example.js",
|
||||||
"start": "pnpm run build && electron . --start",
|
"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,
|
getOverlayWindowsRuntimeService,
|
||||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||||
} from "./core/services/overlay-broadcast-runtime-service";
|
} from "./core/services/overlay-broadcast-runtime-service";
|
||||||
|
import { runAppReadyRuntimeService } from "./core/services/app-ready-runtime-service";
|
||||||
import {
|
import {
|
||||||
runSubsyncManualFromIpcRuntimeService,
|
runSubsyncManualFromIpcRuntimeService,
|
||||||
triggerSubsyncFromConfigRuntimeService,
|
triggerSubsyncFromConfigRuntimeService,
|
||||||
@@ -487,76 +488,80 @@ if (initialArgs.generateConfig && !shouldStartApp(initialArgs)) {
|
|||||||
},
|
},
|
||||||
isDarwinPlatform: () => process.platform === "darwin",
|
isDarwinPlatform: () => process.platform === "darwin",
|
||||||
onReady: async () => {
|
onReady: async () => {
|
||||||
loadSubtitlePosition();
|
await runAppReadyRuntimeService({
|
||||||
keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
loadSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
|
resolveKeybindings: () => {
|
||||||
mpvClient = new MpvIpcClient(mpvSocketPath, {
|
keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
||||||
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();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
createMpvClient: () => {
|
||||||
secondarySubMode = config.secondarySub?.defaultMode ?? "hover";
|
mpvClient = new MpvIpcClient(mpvSocketPath, {
|
||||||
const wsConfig = config.websocket || {};
|
getResolvedConfig: () => getResolvedConfig(), autoStartOverlay,
|
||||||
const wsEnabled = wsConfig.enabled ?? "auto";
|
setOverlayVisible: (visible) => setOverlayVisible(visible),
|
||||||
const wsPort = wsConfig.port || DEFAULT_CONFIG.websocket.port;
|
shouldBindVisibleOverlayToMpvSubVisibility: () => shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||||
|
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||||
if (
|
getReconnectTimer: () => reconnectTimer, setReconnectTimer: (timer) => { reconnectTimer = timer; },
|
||||||
wsEnabled === true ||
|
getCurrentSubText: () => currentSubText, setCurrentSubText: (text) => { currentSubText = text; }, setCurrentSubAssText: (text) => { currentSubAssText = text; },
|
||||||
(wsEnabled === "auto" && !hasMpvWebsocketPlugin())
|
getSubtitleTimingTracker: () => subtitleTimingTracker, subtitleWsBroadcast: (text) => { subtitleWsService.broadcast(text); },
|
||||||
) {
|
getOverlayWindowsCount: () => getOverlayWindows().length, tokenizeSubtitle: (text) => tokenizeSubtitle(text),
|
||||||
subtitleWsService.start(wsPort, () => currentSubText);
|
broadcastToOverlayWindows: (channel, ...args) => { broadcastToOverlayWindows(channel, ...args); },
|
||||||
} else if (wsEnabled === "auto") {
|
updateCurrentMediaPath: (mediaPath) => { updateCurrentMediaPath(mediaPath); }, updateMpvSubtitleRenderMetrics: (patch) => { updateMpvSubtitleRenderMetrics(patch); },
|
||||||
console.log(
|
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, setPreviousSecondarySubVisibility: (value) => { previousSecondarySubVisibility = value; }, showMpvOsd: (text) => { showMpvOsd(text); },
|
||||||
"mpv_websocket detected, skipping built-in WebSocket server",
|
});
|
||||||
);
|
},
|
||||||
}
|
reloadConfig: () => {
|
||||||
|
configService.reloadConfig();
|
||||||
mecabTokenizer = new MecabTokenizer();
|
},
|
||||||
await mecabTokenizer.checkAvailability();
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
|
getConfigWarnings: () => configService.getWarnings(),
|
||||||
subtitleTimingTracker = new SubtitleTimingTracker();
|
logConfigWarning: (warning) => {
|
||||||
|
console.warn(
|
||||||
await loadYomitanExtension();
|
`[config] ${warning.path}: ${warning.message} value=${JSON.stringify(warning.value)} fallback=${JSON.stringify(warning.fallback)}`,
|
||||||
if (texthookerOnlyMode) {
|
);
|
||||||
console.log("Texthooker-only mode enabled; skipping overlay window.");
|
},
|
||||||
} else if (shouldAutoInitializeOverlayRuntimeFromConfig()) {
|
initRuntimeOptionsManager: () => {
|
||||||
initializeOverlayRuntime();
|
runtimeOptionsManager = new RuntimeOptionsManager(
|
||||||
} else {
|
() => configService.getConfig().ankiConnect,
|
||||||
console.log(
|
{
|
||||||
"Overlay runtime deferred: waiting for explicit overlay command.",
|
applyAnkiPatch: (patch) => {
|
||||||
);
|
if (ankiIntegration) {
|
||||||
}
|
ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||||
handleInitialArgs();
|
}
|
||||||
|
},
|
||||||
|
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: () => {
|
onWillQuitCleanup: () => {
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
|
|||||||
Reference in New Issue
Block a user