mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
Fix child-process arg warning
This commit is contained in:
@@ -24,6 +24,7 @@ function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
|
||||
calls.push("createMecabTokenizerAndCheck");
|
||||
},
|
||||
createSubtitleTimingTracker: () => calls.push("createSubtitleTimingTracker"),
|
||||
createImmersionTracker: () => calls.push("createImmersionTracker"),
|
||||
loadYomitanExtension: async () => {
|
||||
calls.push("loadYomitanExtension");
|
||||
},
|
||||
@@ -43,6 +44,22 @@ test("runAppReadyRuntimeService starts websocket in auto mode when plugin missin
|
||||
await runAppReadyRuntimeService(deps);
|
||||
assert.ok(calls.includes("startSubtitleWebsocket:9001"));
|
||||
assert.ok(calls.includes("initializeOverlayRuntime"));
|
||||
assert.ok(calls.includes("createImmersionTracker"));
|
||||
assert.ok(
|
||||
calls.includes("log:Runtime ready: invoking createImmersionTracker."),
|
||||
);
|
||||
});
|
||||
|
||||
test("runAppReadyRuntimeService logs when createImmersionTracker dependency is missing", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
createImmersionTracker: undefined,
|
||||
});
|
||||
await runAppReadyRuntimeService(deps);
|
||||
assert.ok(
|
||||
calls.includes(
|
||||
"log:Runtime ready: createImmersionTracker dependency is missing.",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("runAppReadyRuntimeService logs defer message when overlay not auto-started", async () => {
|
||||
|
||||
1413
src/core/services/immersion-tracker-service.ts
Normal file
1413
src/core/services/immersion-tracker-service.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -89,6 +89,7 @@ export {
|
||||
} from "./mpv-render-metrics-service";
|
||||
export { createOverlayContentMeasurementStoreService } from "./overlay-content-measurement-service";
|
||||
export { handleMpvCommandFromIpcService } from "./ipc-command-service";
|
||||
export { ImmersionTrackerService } from "./immersion-tracker-service";
|
||||
export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-service";
|
||||
export { createNumericShortcutRuntimeService } from "./numeric-shortcut-service";
|
||||
export { runStartupBootstrapRuntimeService } from "./startup-service";
|
||||
|
||||
@@ -52,19 +52,23 @@ test("copyCurrentSubtitleService copies current subtitle text", () => {
|
||||
test("mineSentenceCardService handles missing integration and disconnected mpv", async () => {
|
||||
const osd: string[] = [];
|
||||
|
||||
await mineSentenceCardService({
|
||||
assert.equal(
|
||||
await mineSentenceCardService({
|
||||
ankiIntegration: null,
|
||||
mpvClient: null,
|
||||
showMpvOsd: (text) => osd.push(text),
|
||||
});
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(osd.at(-1), "AnkiConnect integration not enabled");
|
||||
|
||||
await mineSentenceCardService({
|
||||
assert.equal(
|
||||
await mineSentenceCardService({
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
createSentenceCard: async () => {},
|
||||
createSentenceCard: async () => false,
|
||||
},
|
||||
mpvClient: {
|
||||
connected: false,
|
||||
@@ -73,7 +77,9 @@ test("mineSentenceCardService handles missing integration and disconnected mpv",
|
||||
currentSubEnd: 2,
|
||||
},
|
||||
showMpvOsd: (text) => osd.push(text),
|
||||
});
|
||||
}),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(osd.at(-1), "MPV not connected");
|
||||
});
|
||||
@@ -86,13 +92,14 @@ test("mineSentenceCardService creates sentence card from mpv subtitle state", as
|
||||
secondarySub?: string;
|
||||
}> = [];
|
||||
|
||||
await mineSentenceCardService({
|
||||
const createdCard = await mineSentenceCardService({
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
createSentenceCard: async (sentence, startTime, endTime, secondarySub) => {
|
||||
created.push({ sentence, startTime, endTime, secondarySub });
|
||||
return true;
|
||||
},
|
||||
},
|
||||
mpvClient: {
|
||||
@@ -105,6 +112,7 @@ test("mineSentenceCardService creates sentence card from mpv subtitle state", as
|
||||
showMpvOsd: () => {},
|
||||
});
|
||||
|
||||
assert.equal(createdCard, true);
|
||||
assert.deepEqual(created, [
|
||||
{
|
||||
sentence: "subtitle line",
|
||||
@@ -136,6 +144,7 @@ test("handleMultiCopyDigitService copies available history and reports truncatio
|
||||
test("handleMineSentenceDigitService reports async create failures", async () => {
|
||||
const osd: string[] = [];
|
||||
const logs: Array<{ message: string; err: unknown }> = [];
|
||||
let cardsMined = 0;
|
||||
|
||||
handleMineSentenceDigitService(2, {
|
||||
subtitleTimingTracker: {
|
||||
@@ -157,6 +166,9 @@ test("handleMineSentenceDigitService reports async create failures", async () =>
|
||||
getCurrentSecondarySubText: () => "sub2",
|
||||
showMpvOsd: (text) => osd.push(text),
|
||||
logError: (message, err) => logs.push({ message, err }),
|
||||
onCardsMined: (count) => {
|
||||
cardsMined += count;
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
@@ -165,4 +177,37 @@ test("handleMineSentenceDigitService reports async create failures", async () =>
|
||||
assert.equal(logs[0]?.message, "mineSentenceMultiple failed:");
|
||||
assert.equal((logs[0]?.err as Error).message, "mine boom");
|
||||
assert.ok(osd.some((entry) => entry.includes("Mine sentence failed: mine boom")));
|
||||
assert.equal(cardsMined, 0);
|
||||
});
|
||||
|
||||
test("handleMineSentenceDigitService increments successful card count", async () => {
|
||||
const osd: string[] = [];
|
||||
let cardsMined = 0;
|
||||
|
||||
handleMineSentenceDigitService(2, {
|
||||
subtitleTimingTracker: {
|
||||
getRecentBlocks: () => ["one", "two"],
|
||||
getCurrentSubtitle: () => null,
|
||||
findTiming: (text) =>
|
||||
text === "one"
|
||||
? { startTime: 1, endTime: 3 }
|
||||
: { startTime: 4, endTime: 7 },
|
||||
},
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
createSentenceCard: async () => true,
|
||||
},
|
||||
getCurrentSecondarySubText: () => "sub2",
|
||||
showMpvOsd: (text) => osd.push(text),
|
||||
logError: () => {},
|
||||
onCardsMined: (count) => {
|
||||
cardsMined += count;
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.equal(cardsMined, 1);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ interface AnkiIntegrationLike {
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
secondarySub?: string,
|
||||
) => Promise<void>;
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
interface MpvClientLike {
|
||||
@@ -111,21 +111,21 @@ export async function mineSentenceCardService(deps: {
|
||||
ankiIntegration: AnkiIntegrationLike | null;
|
||||
mpvClient: MpvClientLike | null;
|
||||
showMpvOsd: (text: string) => void;
|
||||
}): Promise<void> {
|
||||
}): Promise<boolean> {
|
||||
const anki = requireAnkiIntegration(deps.ankiIntegration, deps.showMpvOsd);
|
||||
if (!anki) return;
|
||||
if (!anki) return false;
|
||||
|
||||
const mpvClient = deps.mpvClient;
|
||||
if (!mpvClient || !mpvClient.connected) {
|
||||
deps.showMpvOsd("MPV not connected");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (!mpvClient.currentSubText) {
|
||||
deps.showMpvOsd("No current subtitle");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
await anki.createSentenceCard(
|
||||
return await anki.createSentenceCard(
|
||||
mpvClient.currentSubText,
|
||||
mpvClient.currentSubStart,
|
||||
mpvClient.currentSubEnd,
|
||||
@@ -141,6 +141,7 @@ export function handleMineSentenceDigitService(
|
||||
getCurrentSecondarySubText: () => string | undefined;
|
||||
showMpvOsd: (text: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
onCardsMined?: (count: number) => void;
|
||||
},
|
||||
): void {
|
||||
if (!deps.subtitleTimingTracker || !deps.ankiIntegration) return;
|
||||
@@ -165,6 +166,7 @@ export function handleMineSentenceDigitService(
|
||||
const rangeStart = Math.min(...timings.map((t) => t.startTime));
|
||||
const rangeEnd = Math.max(...timings.map((t) => t.endTime));
|
||||
const sentence = blocks.join(" ");
|
||||
const cardsToMine = 1;
|
||||
deps.ankiIntegration
|
||||
.createSentenceCard(
|
||||
sentence,
|
||||
@@ -172,6 +174,11 @@ export function handleMineSentenceDigitService(
|
||||
rangeEnd,
|
||||
deps.getCurrentSecondarySubText(),
|
||||
)
|
||||
.then((created) => {
|
||||
if (created) {
|
||||
deps.onCardsMined?.(cardsToMine);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
deps.logError("mineSentenceMultiple failed:", err);
|
||||
deps.showMpvOsd(`Mine sentence failed: ${(err as Error).message}`);
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
MPV_REQUEST_ID_SUBTEXT,
|
||||
MPV_REQUEST_ID_SUBTEXT_ASS,
|
||||
MPV_REQUEST_ID_SUB_USE_MARGINS,
|
||||
MPV_REQUEST_ID_PAUSE,
|
||||
} from "./mpv-protocol";
|
||||
|
||||
type MpvProtocolCommand = {
|
||||
@@ -57,6 +58,7 @@ const MPV_SUBTITLE_PROPERTY_OBSERVATIONS: string[] = [
|
||||
"sub-shadow-offset",
|
||||
"sub-ass-override",
|
||||
"sub-use-margins",
|
||||
"pause",
|
||||
"media-title",
|
||||
];
|
||||
|
||||
@@ -76,6 +78,10 @@ const MPV_INITIAL_PROPERTY_REQUESTS: Array<MpvProtocolCommand> = [
|
||||
{
|
||||
command: ["get_property", "media-title"],
|
||||
},
|
||||
{
|
||||
command: ["get_property", "pause"],
|
||||
request_id: MPV_REQUEST_ID_PAUSE,
|
||||
},
|
||||
{
|
||||
command: ["get_property", "secondary-sub-text"],
|
||||
request_id: MPV_REQUEST_ID_SECONDARY_SUBTEXT,
|
||||
|
||||
@@ -84,6 +84,8 @@ function createDeps(overrides: Partial<MpvProtocolHandleMessageDeps> = {}): {
|
||||
setPendingPauseAtSubEnd: () => {},
|
||||
getPauseAtTime: () => null,
|
||||
setPauseAtTime: () => {},
|
||||
emitTimePosChange: () => {},
|
||||
emitPauseChange: () => {},
|
||||
autoLoadSecondarySubTrack: () => {},
|
||||
setCurrentVideoPath: () => {},
|
||||
emitSecondarySubtitleVisibility: (payload) => state.events.push(payload),
|
||||
|
||||
@@ -30,6 +30,7 @@ export const MPV_REQUEST_ID_SUB_BORDER_SIZE = 119;
|
||||
export const MPV_REQUEST_ID_SUB_SHADOW_OFFSET = 120;
|
||||
export const MPV_REQUEST_ID_SUB_ASS_OVERRIDE = 121;
|
||||
export const MPV_REQUEST_ID_SUB_USE_MARGINS = 122;
|
||||
export const MPV_REQUEST_ID_PAUSE = 123;
|
||||
export const MPV_REQUEST_ID_TRACK_LIST_SECONDARY = 200;
|
||||
export const MPV_REQUEST_ID_TRACK_LIST_AUDIO = 201;
|
||||
|
||||
@@ -60,6 +61,8 @@ export interface MpvProtocolHandleMessageDeps {
|
||||
getCurrentSubEnd: () => number;
|
||||
emitMediaPathChange: (payload: { path: string }) => void;
|
||||
emitMediaTitleChange: (payload: { title: string | null }) => void;
|
||||
emitTimePosChange: (payload: { time: number }) => void;
|
||||
emitPauseChange: (payload: { paused: boolean }) => void;
|
||||
emitSubtitleMetricsChange: (payload: Partial<MpvSubtitleRenderMetrics>) => void;
|
||||
setCurrentSecondarySubText: (text: string) => void;
|
||||
resolvePendingRequest: (requestId: number, message: MpvMessage) => boolean;
|
||||
@@ -160,6 +163,7 @@ export async function dispatchMpvProtocolMessage(
|
||||
);
|
||||
deps.syncCurrentAudioStreamIndex();
|
||||
} else if (msg.name === "time-pos") {
|
||||
deps.emitTimePosChange({ time: (msg.data as number) || 0 });
|
||||
deps.setCurrentTimePos((msg.data as number) || 0);
|
||||
if (
|
||||
deps.getPauseAtTime() !== null &&
|
||||
@@ -168,6 +172,8 @@ export async function dispatchMpvProtocolMessage(
|
||||
deps.setPauseAtTime(null);
|
||||
deps.sendCommand({ command: ["set_property", "pause", true] });
|
||||
}
|
||||
} else if (msg.name === "pause") {
|
||||
deps.emitPauseChange({ paused: asBoolean(msg.data, false) });
|
||||
} else if (msg.name === "media-title") {
|
||||
deps.emitMediaTitleChange({
|
||||
title: typeof msg.data === "string" ? msg.data.trim() : null,
|
||||
@@ -348,6 +354,8 @@ export async function dispatchMpvProtocolMessage(
|
||||
deps.getSubtitleMetrics().subUseMargins,
|
||||
),
|
||||
});
|
||||
} else if (msg.request_id === MPV_REQUEST_ID_PAUSE) {
|
||||
deps.emitPauseChange({ paused: asBoolean(msg.data, false) });
|
||||
} else if (msg.request_id === MPV_REQUEST_ID_OSD_HEIGHT) {
|
||||
deps.emitSubtitleMetricsChange({ osdHeight: msg.data as number });
|
||||
} else if (msg.request_id === MPV_REQUEST_ID_OSD_DIMENSIONS) {
|
||||
|
||||
@@ -117,6 +117,8 @@ export interface MpvIpcClientEventMap {
|
||||
"subtitle-change": { text: string; isOverlayVisible: boolean };
|
||||
"subtitle-ass-change": { text: string };
|
||||
"subtitle-timing": { text: string; start: number; end: number };
|
||||
"time-pos-change": { time: number };
|
||||
"pause-change": { paused: boolean };
|
||||
"secondary-subtitle-change": { text: string };
|
||||
"media-path-change": { path: string };
|
||||
"media-title-change": { title: string | null };
|
||||
@@ -258,9 +260,13 @@ export class MpvIpcClient implements MpvClient {
|
||||
|
||||
connect(): void {
|
||||
if (this.connected || this.connecting) {
|
||||
logger.debug(
|
||||
`MPV IPC connect request skipped; connected=${this.connected}, connecting=${this.connecting}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("MPV IPC connect requested.");
|
||||
this.connecting = true;
|
||||
this.transport.connect();
|
||||
}
|
||||
@@ -313,6 +319,12 @@ export class MpvIpcClient implements MpvClient {
|
||||
emitSubtitleTiming: (payload) => {
|
||||
this.emit("subtitle-timing", payload);
|
||||
},
|
||||
emitTimePosChange: (payload) => {
|
||||
this.emit("time-pos-change", payload);
|
||||
},
|
||||
emitPauseChange: (payload) => {
|
||||
this.emit("pause-change", payload);
|
||||
},
|
||||
emitSecondarySubtitleChange: (payload) => {
|
||||
this.emit("secondary-subtitle-change", payload);
|
||||
},
|
||||
|
||||
@@ -99,6 +99,7 @@ export interface AppReadyRuntimeDeps {
|
||||
log: (message: string) => void;
|
||||
createMecabTokenizerAndCheck: () => Promise<void>;
|
||||
createSubtitleTimingTracker: () => void;
|
||||
createImmersionTracker?: () => void;
|
||||
loadYomitanExtension: () => Promise<void>;
|
||||
texthookerOnlyMode: boolean;
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean;
|
||||
@@ -173,6 +174,12 @@ export async function runAppReadyRuntimeService(
|
||||
}
|
||||
|
||||
deps.createSubtitleTimingTracker();
|
||||
if (deps.createImmersionTracker) {
|
||||
deps.log("Runtime ready: invoking createImmersionTracker.");
|
||||
deps.createImmersionTracker();
|
||||
} else {
|
||||
deps.log("Runtime ready: createImmersionTracker dependency is missing.");
|
||||
}
|
||||
await deps.loadYomitanExtension();
|
||||
|
||||
if (deps.texthookerOnlyMode) {
|
||||
|
||||
Reference in New Issue
Block a user