Fix child-process arg warning

This commit is contained in:
2026-02-16 23:33:03 -08:00
parent 1cd1cdb11d
commit 4d28efabd0
24 changed files with 1951 additions and 33 deletions

View File

@@ -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 () => {

File diff suppressed because it is too large Load Diff

View File

@@ -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";

View File

@@ -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);
});

View File

@@ -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}`);

View File

@@ -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,

View File

@@ -84,6 +84,8 @@ function createDeps(overrides: Partial<MpvProtocolHandleMessageDeps> = {}): {
setPendingPauseAtSubEnd: () => {},
getPauseAtTime: () => null,
setPauseAtTime: () => {},
emitTimePosChange: () => {},
emitPauseChange: () => {},
autoLoadSecondarySubTrack: () => {},
setCurrentVideoPath: () => {},
emitSecondarySubtitleVisibility: (payload) => state.events.push(payload),

View File

@@ -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) {

View File

@@ -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);
},

View File

@@ -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) {