mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
Refactor startup/logging service wiring and related test/config updates
This commit is contained in:
@@ -36,6 +36,46 @@ test("parses jsonc and warns/falls back on invalid value", () => {
|
||||
assert.ok(service.getWarnings().some((w) => w.path === "websocket.port"));
|
||||
});
|
||||
|
||||
test("accepts valid logging.level", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
`{
|
||||
"logging": {
|
||||
"level": "warn"
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.logging.level, "warn");
|
||||
});
|
||||
|
||||
test("falls back for invalid logging.level and reports warning", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
`{
|
||||
"logging": {
|
||||
"level": "trace"
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.logging.level, DEFAULT_CONFIG.logging.level);
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "logging.level"),
|
||||
);
|
||||
});
|
||||
|
||||
test("parses invisible overlay config and new global shortcuts", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
@@ -372,6 +412,7 @@ test("falls back to default when ankiConnect n+1 deck list is invalid", () => {
|
||||
test("template generator includes known keys", () => {
|
||||
const output = generateConfigTemplate(DEFAULT_CONFIG);
|
||||
assert.match(output, /"ankiConnect":/);
|
||||
assert.match(output, /"logging":/);
|
||||
assert.match(output, /"websocket":/);
|
||||
assert.match(output, /"youtubeSubgen":/);
|
||||
assert.match(output, /"nPlusOne"\s*:\s*\{/);
|
||||
|
||||
@@ -75,6 +75,9 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
||||
enabled: "auto",
|
||||
port: 6677,
|
||||
},
|
||||
logging: {
|
||||
level: "info",
|
||||
},
|
||||
texthooker: {
|
||||
openBrowser: true,
|
||||
},
|
||||
@@ -276,6 +279,13 @@ export const RUNTIME_OPTION_REGISTRY: RuntimeOptionRegistryEntry[] = [
|
||||
];
|
||||
|
||||
export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
|
||||
{
|
||||
path: "logging.level",
|
||||
kind: "enum",
|
||||
enumValues: ["debug", "info", "warn", "error"],
|
||||
defaultValue: DEFAULT_CONFIG.logging.level,
|
||||
description: "Minimum log level for runtime logging.",
|
||||
},
|
||||
{
|
||||
path: "websocket.enabled",
|
||||
kind: "enum",
|
||||
@@ -460,6 +470,14 @@ export const CONFIG_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||
],
|
||||
key: "websocket",
|
||||
},
|
||||
{
|
||||
title: "Logging",
|
||||
description: [
|
||||
"Controls logging verbosity.",
|
||||
"Set to debug for full runtime diagnostics.",
|
||||
],
|
||||
key: "logging",
|
||||
},
|
||||
{
|
||||
title: "AnkiConnect Integration",
|
||||
description: ["Automatic Anki updates and media generation options."],
|
||||
|
||||
@@ -196,6 +196,20 @@ export class ConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(src.logging)) {
|
||||
const logLevel = asString(src.logging.level);
|
||||
if (logLevel === "debug" || logLevel === "info" || logLevel === "warn" || logLevel === "error") {
|
||||
resolved.logging.level = logLevel;
|
||||
} else if (src.logging.level !== undefined) {
|
||||
warn(
|
||||
"logging.level",
|
||||
src.logging.level,
|
||||
resolved.logging.level,
|
||||
"Expected debug, info, warn, or error.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(src.keybindings)) {
|
||||
resolved.keybindings = src.keybindings.filter(
|
||||
(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ipcMain, IpcMainEvent } from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import { createLogger } from "../../logger";
|
||||
import {
|
||||
JimakuApiResponse,
|
||||
JimakuDownloadQuery,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
KikuMergePreviewResponse,
|
||||
} from "../../types";
|
||||
|
||||
const logger = createLogger("main:anki-jimaku-ipc");
|
||||
|
||||
export interface AnkiJimakuIpcDeps {
|
||||
setAnkiConnectEnabled: (enabled: boolean) => void;
|
||||
clearAnkiHistory: () => void;
|
||||
@@ -147,7 +150,7 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
logger.info(
|
||||
`[jimaku] download-file name="${query.name}" entryId=${query.entryId}`,
|
||||
);
|
||||
const result = await deps.downloadToFile(query.url, targetPath, {
|
||||
@@ -156,10 +159,10 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
console.log(`[jimaku] download-file saved to ${result.path}`);
|
||||
logger.info(`[jimaku] download-file saved to ${result.path}`);
|
||||
deps.onDownloadedSubtitle(result.path);
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
`[jimaku] download-file failed: ${result.error?.error ?? "unknown error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../../types";
|
||||
import { sortJimakuFiles } from "../../jimaku/utils";
|
||||
import type { AnkiJimakuIpcDeps } from "./anki-jimaku-ipc-service";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
export type RegisterAnkiJimakuIpcRuntimeHandler = (
|
||||
deps: AnkiJimakuIpcDeps,
|
||||
@@ -62,6 +63,8 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
||||
) => Promise<{ ok: true; path: string } | { ok: false; error: { error: string; code?: number; retryAfter?: number } }>;
|
||||
}
|
||||
|
||||
const logger = createLogger("main:anki-jimaku");
|
||||
|
||||
export function registerAnkiJimakuIpcRuntimeService(
|
||||
options: AnkiJimakuIpcRuntimeOptions,
|
||||
registerHandlers: RegisterAnkiJimakuIpcRuntimeHandler,
|
||||
@@ -96,11 +99,11 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
);
|
||||
integration.start();
|
||||
options.setAnkiIntegration(integration);
|
||||
console.log("AnkiConnect integration enabled");
|
||||
logger.info("AnkiConnect integration enabled");
|
||||
} else if (!enabled && ankiIntegration) {
|
||||
ankiIntegration.destroy();
|
||||
options.setAnkiIntegration(null);
|
||||
console.log("AnkiConnect integration disabled");
|
||||
logger.info("AnkiConnect integration disabled");
|
||||
}
|
||||
|
||||
options.broadcastRuntimeOptionsChanged();
|
||||
@@ -109,7 +112,7 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
const subtitleTimingTracker = options.getSubtitleTimingTracker();
|
||||
if (subtitleTimingTracker) {
|
||||
subtitleTimingTracker.cleanup();
|
||||
console.log("AnkiConnect subtitle timing history cleared");
|
||||
logger.info("AnkiConnect subtitle timing history cleared");
|
||||
}
|
||||
},
|
||||
refreshKnownWords: async () => {
|
||||
@@ -139,7 +142,7 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
},
|
||||
getJimakuMediaInfo: () => options.parseMediaInfo(options.getCurrentMediaPath()),
|
||||
searchJimakuEntries: async (query) => {
|
||||
console.log(`[jimaku] search-entries query: "${query.query}"`);
|
||||
logger.info(`[jimaku] search-entries query: "${query.query}"`);
|
||||
const response = await options.jimakuFetchJson<JimakuEntry[]>(
|
||||
"/api/entries/search",
|
||||
{
|
||||
@@ -149,13 +152,13 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
);
|
||||
if (!response.ok) return response;
|
||||
const maxResults = options.getJimakuMaxEntryResults();
|
||||
console.log(
|
||||
logger.info(
|
||||
`[jimaku] search-entries returned ${response.data.length} results (capped to ${maxResults})`,
|
||||
);
|
||||
return { ok: true, data: response.data.slice(0, maxResults) };
|
||||
},
|
||||
listJimakuFiles: async (query) => {
|
||||
console.log(
|
||||
logger.info(
|
||||
`[jimaku] list-files entryId=${query.entryId} episode=${query.episode ?? "all"}`,
|
||||
);
|
||||
const response = await options.jimakuFetchJson<JimakuFileEntry[]>(
|
||||
@@ -169,7 +172,7 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
response.data,
|
||||
options.getJimakuLanguagePreference(),
|
||||
);
|
||||
console.log(`[jimaku] list-files returned ${sorted.length} files`);
|
||||
logger.info(`[jimaku] list-files returned ${sorted.length} files`);
|
||||
return { ok: true, data: sorted };
|
||||
},
|
||||
resolveJimakuApiKey: () => options.resolveJimakuApiKey(),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { CliArgs, CliCommandSource } from "../../cli/args";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:app-lifecycle");
|
||||
|
||||
export interface AppLifecycleServiceDeps {
|
||||
shouldStartApp: (args: CliArgs) => boolean;
|
||||
@@ -57,7 +60,7 @@ export function createAppLifecycleDepsRuntimeService(
|
||||
logNoRunningInstance: options.logNoRunningInstance,
|
||||
whenReady: (handler) => {
|
||||
options.app.whenReady().then(handler).catch((error) => {
|
||||
console.error("App ready handler failed:", error);
|
||||
logger.error("App ready handler failed:", error);
|
||||
});
|
||||
},
|
||||
onWindowAllClosed: (handler) => {
|
||||
@@ -91,7 +94,7 @@ export function startAppLifecycleService(
|
||||
try {
|
||||
deps.handleCliCommand(deps.parseArgs(argv), "second-instance");
|
||||
} catch (error) {
|
||||
console.error("Failed to handle second-instance CLI command:", error);
|
||||
logger.error("Failed to handle second-instance CLI command:", error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
|
||||
getResolvedConfig: () => ({ websocket: { enabled: "auto" }, secondarySub: {} }),
|
||||
getConfigWarnings: () => [],
|
||||
logConfigWarning: () => calls.push("logConfigWarning"),
|
||||
setLogLevel: (level, source) => calls.push(`setLogLevel:${level}:${source}`),
|
||||
initRuntimeOptionsManager: () => calls.push("initRuntimeOptionsManager"),
|
||||
setSecondarySubMode: (mode) => calls.push(`setSecondarySubMode:${mode}`),
|
||||
defaultSecondarySubMode: "hover",
|
||||
@@ -55,3 +56,15 @@ test("runAppReadyRuntimeService logs defer message when overlay not auto-started
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("runAppReadyRuntimeService applies config logging level during app-ready", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
getResolvedConfig: () => ({
|
||||
websocket: { enabled: "auto" },
|
||||
secondarySub: {},
|
||||
logging: { level: "warn" },
|
||||
}),
|
||||
});
|
||||
await runAppReadyRuntimeService(deps);
|
||||
assert.ok(calls.includes("setLogLevel:warn:config"));
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ export {
|
||||
} from "./overlay-shortcut-service";
|
||||
export { createOverlayShortcutRuntimeHandlers } from "./overlay-shortcut-handler";
|
||||
export { createCliCommandDepsRuntimeService, handleCliCommandService } from "./cli-command-service";
|
||||
export { cycleSecondarySubModeService } from "./secondary-subtitle-service";
|
||||
export {
|
||||
copyCurrentSubtitleService,
|
||||
handleMineSentenceDigitService,
|
||||
@@ -23,18 +22,14 @@ export {
|
||||
} from "./mining-service";
|
||||
export { createAppLifecycleDepsRuntimeService, startAppLifecycleService } from "./app-lifecycle-service";
|
||||
export {
|
||||
playNextSubtitleRuntimeService,
|
||||
replayCurrentSubtitleRuntimeService,
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-control-service";
|
||||
cycleSecondarySubModeService,
|
||||
} from "./subtitle-position-service";
|
||||
export {
|
||||
getInitialInvisibleOverlayVisibilityService,
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigService,
|
||||
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
||||
} from "./runtime-config-service";
|
||||
} from "./startup-service";
|
||||
export { openYomitanSettingsWindow } from "./yomitan-settings-service";
|
||||
export { createTokenizerDepsRuntimeService, tokenizeSubtitleService } from "./tokenizer-service";
|
||||
export { createJlptVocabularyLookupService } from "./jlpt-vocab-service";
|
||||
@@ -74,7 +69,18 @@ export {
|
||||
updateInvisibleOverlayVisibilityService,
|
||||
updateVisibleOverlayVisibilityService,
|
||||
} from "./overlay-visibility-service";
|
||||
export { MpvIpcClient, MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY } from "./mpv-service";
|
||||
export {
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
MpvIpcClient,
|
||||
MpvRuntimeClientLike,
|
||||
MpvTrackProperty,
|
||||
playNextSubtitleRuntimeService,
|
||||
replayCurrentSubtitleRuntimeService,
|
||||
resolveCurrentAudioStreamIndex,
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-service";
|
||||
export {
|
||||
applyMpvSubtitleRenderMetricsPatchService,
|
||||
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-control-service";
|
||||
} from "./mpv-service";
|
||||
|
||||
test("showMpvOsdRuntimeService sends show-text when connected", () => {
|
||||
const commands: (string | number)[][] = [];
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
export interface MpvRuntimeClientLike {
|
||||
connected: boolean;
|
||||
send: (payload: { command: (string | number)[] }) => void;
|
||||
replayCurrentSubtitle?: () => void;
|
||||
playNextSubtitle?: () => void;
|
||||
setSubVisibility?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export function showMpvOsdRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
text: string,
|
||||
fallbackLog: (text: string) => void = console.log,
|
||||
): void {
|
||||
if (mpvClient && mpvClient.connected) {
|
||||
mpvClient.send({ command: ["show-text", text, "3000"] });
|
||||
return;
|
||||
}
|
||||
fallbackLog(`OSD (MPV not connected): ${text}`);
|
||||
}
|
||||
|
||||
export function replayCurrentSubtitleRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.replayCurrentSubtitle) return;
|
||||
mpvClient.replayCurrentSubtitle();
|
||||
}
|
||||
|
||||
export function playNextSubtitleRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.playNextSubtitle) return;
|
||||
mpvClient.playNextSubtitle();
|
||||
}
|
||||
|
||||
export function sendMpvCommandRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
command: (string | number)[],
|
||||
): void {
|
||||
if (!mpvClient) return;
|
||||
mpvClient.send({ command });
|
||||
}
|
||||
|
||||
export function setMpvSubVisibilityRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
visible: boolean,
|
||||
): void {
|
||||
if (!mpvClient?.setSubVisibility) return;
|
||||
mpvClient.setSubVisibility(visible);
|
||||
}
|
||||
@@ -17,12 +17,86 @@ import {
|
||||
scheduleMpvReconnect,
|
||||
MpvSocketTransport,
|
||||
} from "./mpv-transport";
|
||||
import { resolveCurrentAudioStreamIndex } from "./mpv-state";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const isDebugLoggingEnabled = (): boolean => {
|
||||
return (process.env.SUBMINER_LOG_LEVEL || "").toLowerCase() === "debug";
|
||||
const logger = createLogger("main:mpv");
|
||||
|
||||
export type MpvTrackProperty = {
|
||||
type?: string;
|
||||
id?: number;
|
||||
selected?: boolean;
|
||||
"ff-index"?: number;
|
||||
};
|
||||
|
||||
export function resolveCurrentAudioStreamIndex(
|
||||
tracks: Array<MpvTrackProperty> | null | undefined,
|
||||
currentAudioTrackId: number | null,
|
||||
): number | null {
|
||||
if (!Array.isArray(tracks)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const audioTracks = tracks.filter((track) => track.type === "audio");
|
||||
const activeTrack =
|
||||
audioTracks.find((track) => track.id === currentAudioTrackId) ||
|
||||
audioTracks.find((track) => track.selected === true);
|
||||
|
||||
const ffIndex = activeTrack?.["ff-index"];
|
||||
return typeof ffIndex === "number" && Number.isInteger(ffIndex) && ffIndex >= 0
|
||||
? ffIndex
|
||||
: null;
|
||||
}
|
||||
|
||||
export interface MpvRuntimeClientLike {
|
||||
connected: boolean;
|
||||
send: (payload: { command: (string | number)[] }) => void;
|
||||
replayCurrentSubtitle?: () => void;
|
||||
playNextSubtitle?: () => void;
|
||||
setSubVisibility?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export function showMpvOsdRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
text: string,
|
||||
fallbackLog: (text: string) => void = (line) => logger.info(line),
|
||||
): void {
|
||||
if (mpvClient && mpvClient.connected) {
|
||||
mpvClient.send({ command: ["show-text", text, "3000"] });
|
||||
return;
|
||||
}
|
||||
fallbackLog(`OSD (MPV not connected): ${text}`);
|
||||
}
|
||||
|
||||
export function replayCurrentSubtitleRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.replayCurrentSubtitle) return;
|
||||
mpvClient.replayCurrentSubtitle();
|
||||
}
|
||||
|
||||
export function playNextSubtitleRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.playNextSubtitle) return;
|
||||
mpvClient.playNextSubtitle();
|
||||
}
|
||||
|
||||
export function sendMpvCommandRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
command: (string | number)[],
|
||||
): void {
|
||||
if (!mpvClient) return;
|
||||
mpvClient.send({ command });
|
||||
}
|
||||
|
||||
export function setMpvSubVisibilityRuntimeService(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
visible: boolean,
|
||||
): void {
|
||||
if (!mpvClient?.setSubVisibility) return;
|
||||
mpvClient.setSubVisibility(visible);
|
||||
}
|
||||
|
||||
export {
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
} from "./mpv-protocol";
|
||||
@@ -104,7 +178,7 @@ export class MpvIpcClient implements MpvClient {
|
||||
this.transport = new MpvSocketTransport({
|
||||
socketPath,
|
||||
onConnect: () => {
|
||||
console.log("Connected to MPV socket");
|
||||
logger.info("Connected to MPV socket");
|
||||
this.connected = true;
|
||||
this.connecting = false;
|
||||
this.socket = this.transport.getSocket();
|
||||
@@ -118,7 +192,7 @@ export class MpvIpcClient implements MpvClient {
|
||||
this.deps.autoStartOverlay ||
|
||||
this.deps.getResolvedConfig().auto_start_overlay === true;
|
||||
if (this.firstConnection && shouldAutoStart) {
|
||||
console.log("Auto-starting overlay, hiding mpv subtitles");
|
||||
logger.info("Auto-starting overlay, hiding mpv subtitles");
|
||||
setTimeout(() => {
|
||||
this.deps.setOverlayVisible(true);
|
||||
}, 100);
|
||||
@@ -133,15 +207,11 @@ export class MpvIpcClient implements MpvClient {
|
||||
this.processBuffer();
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
if (isDebugLoggingEnabled()) {
|
||||
console.error("MPV socket error:", err.message);
|
||||
}
|
||||
logger.debug("MPV socket error:", err.message);
|
||||
this.failPendingRequests();
|
||||
},
|
||||
onClose: () => {
|
||||
if (isDebugLoggingEnabled()) {
|
||||
console.log("MPV socket closed");
|
||||
}
|
||||
logger.debug("MPV socket closed");
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
this.socket = null;
|
||||
@@ -202,11 +272,9 @@ export class MpvIpcClient implements MpvClient {
|
||||
getReconnectTimer: () => this.deps.getReconnectTimer(),
|
||||
setReconnectTimer: (timer) => this.deps.setReconnectTimer(timer),
|
||||
onReconnectAttempt: (attempt, delay) => {
|
||||
if (isDebugLoggingEnabled()) {
|
||||
console.log(
|
||||
`Attempting to reconnect to MPV (attempt ${attempt}, delay ${delay}ms)...`,
|
||||
);
|
||||
}
|
||||
logger.debug(
|
||||
`Attempting to reconnect to MPV (attempt ${attempt}, delay ${delay}ms)...`,
|
||||
);
|
||||
},
|
||||
connect: () => {
|
||||
this.connect();
|
||||
@@ -221,7 +289,7 @@ export class MpvIpcClient implements MpvClient {
|
||||
this.handleMessage(message);
|
||||
},
|
||||
(line, error) => {
|
||||
console.error("Failed to parse MPV message:", line, error);
|
||||
logger.error("Failed to parse MPV message:", line, error);
|
||||
},
|
||||
);
|
||||
this.buffer = parsed.nextBuffer;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveCurrentAudioStreamIndex } from "./mpv-state";
|
||||
import { resolveCurrentAudioStreamIndex } from "./mpv-service";
|
||||
|
||||
test("resolveCurrentAudioStreamIndex returns selected ff-index when no current track id", () => {
|
||||
assert.equal(
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
export type MpvTrackProperty = {
|
||||
type?: string;
|
||||
id?: number;
|
||||
selected?: boolean;
|
||||
"ff-index"?: number;
|
||||
};
|
||||
|
||||
export function resolveCurrentAudioStreamIndex(
|
||||
tracks: Array<MpvTrackProperty> | null | undefined,
|
||||
currentAudioTrackId: number | null,
|
||||
): number | null {
|
||||
if (!Array.isArray(tracks)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const audioTracks = tracks.filter((track) => track.type === "audio");
|
||||
const activeTrack =
|
||||
audioTracks.find((track) => track.id === currentAudioTrackId) ||
|
||||
audioTracks.find((track) => track.selected === true);
|
||||
|
||||
const ffIndex = activeTrack?.["ff-index"];
|
||||
return typeof ffIndex === "number" && Number.isInteger(ffIndex) && ffIndex >= 0
|
||||
? ffIndex
|
||||
: null;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { OverlayContentMeasurement, OverlayContentRect, OverlayLayer } from "../../types";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-content-measurement");
|
||||
const MAX_VIEWPORT = 10000;
|
||||
const MAX_RECT_DIMENSION = 10000;
|
||||
const MAX_RECT_OFFSET = 50000;
|
||||
@@ -107,7 +110,7 @@ export function createOverlayContentMeasurementStoreService(options?: {
|
||||
warn?: (message: string) => void;
|
||||
}) {
|
||||
const now = options?.now ?? (() => Date.now());
|
||||
const warn = options?.warn ?? ((message: string) => console.warn(message));
|
||||
const warn = options?.warn ?? ((message: string) => logger.warn(message));
|
||||
const latestByLayer: OverlayMeasurementStore = {
|
||||
visible: null,
|
||||
invisible: null,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { OverlayShortcutHandlers } from "./overlay-shortcut-service";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-shortcut-handler");
|
||||
|
||||
export interface OverlayShortcutFallbackHandlers {
|
||||
openRuntimeOptions: () => void;
|
||||
@@ -38,7 +41,7 @@ function wrapAsync(
|
||||
): () => void {
|
||||
return () => {
|
||||
task().catch((err) => {
|
||||
console.error(`${logLabel} failed:`, err);
|
||||
logger.error(`${logLabel} failed:`, err);
|
||||
deps.showMpvOsd(`${osdLabel}: ${(err as Error).message}`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { globalShortcut } from "electron";
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { isGlobalShortcutRegisteredSafe } from "./shortcut-fallback-service";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-shortcut-service");
|
||||
|
||||
export interface OverlayShortcutHandlers {
|
||||
copySubtitle: () => void;
|
||||
@@ -39,7 +42,7 @@ export function registerOverlayShortcutsService(
|
||||
}
|
||||
const ok = globalShortcut.register(accelerator, handler);
|
||||
if (!ok) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to register overlay shortcut ${label}: ${accelerator}`,
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import * as path from "path";
|
||||
import { WindowGeometry } from "../../types";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-window");
|
||||
|
||||
export type OverlayWindowKind = "visible" | "invisible";
|
||||
|
||||
@@ -86,13 +89,13 @@ export function createOverlayWindowService(
|
||||
query: { layer: kind === "visible" ? "visible" : "invisible" },
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load HTML file:", err);
|
||||
logger.error("Failed to load HTML file:", err);
|
||||
});
|
||||
|
||||
window.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, errorCode, errorDescription, validatedURL) => {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Page failed to load:",
|
||||
errorCode,
|
||||
errorDescription,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigService,
|
||||
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
||||
} from "./runtime-config-service";
|
||||
} from "./startup-service";
|
||||
|
||||
const BASE_CONFIG = {
|
||||
auto_start_overlay: false,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
interface RuntimeAutoUpdateOptionManagerLike {
|
||||
getOptionValue: (id: "anki.autoUpdateNewCards") => unknown;
|
||||
}
|
||||
|
||||
interface RuntimeConfigLike {
|
||||
auto_start_overlay?: boolean;
|
||||
bind_visible_overlay_to_mpv_sub_visibility: boolean;
|
||||
invisibleOverlay: {
|
||||
startupVisibility: "visible" | "hidden" | "platform-default";
|
||||
};
|
||||
ankiConnect?: {
|
||||
behavior?: {
|
||||
autoUpdateNewCards?: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function getInitialInvisibleOverlayVisibilityService(
|
||||
config: RuntimeConfigLike,
|
||||
platform: NodeJS.Platform,
|
||||
): boolean {
|
||||
const visibility = config.invisibleOverlay.startupVisibility;
|
||||
if (visibility === "visible") return true;
|
||||
if (visibility === "hidden") return false;
|
||||
if (platform === "linux") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shouldAutoInitializeOverlayRuntimeFromConfigService(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
if (config.auto_start_overlay === true) return true;
|
||||
if (config.invisibleOverlay.startupVisibility === "visible") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldBindVisibleOverlayToMpvSubVisibilityService(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
return config.bind_visible_overlay_to_mpv_sub_visibility;
|
||||
}
|
||||
|
||||
export function isAutoUpdateEnabledRuntimeService(
|
||||
config: RuntimeConfigLike,
|
||||
runtimeOptionsManager: RuntimeAutoUpdateOptionManagerLike | null,
|
||||
): boolean {
|
||||
const value = runtimeOptionsManager?.getOptionValue("anki.autoUpdateNewCards");
|
||||
if (typeof value === "boolean") return value;
|
||||
return config.ankiConnect?.behavior?.autoUpdateNewCards !== false;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { SecondarySubMode } from "../../types";
|
||||
import { cycleSecondarySubModeService } from "./secondary-subtitle-service";
|
||||
import { cycleSecondarySubModeService } from "./subtitle-position-service";
|
||||
|
||||
test("cycleSecondarySubModeService cycles and emits broadcast + OSD", () => {
|
||||
let mode: SecondarySubMode = "hover";
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { SecondarySubMode } from "../../types";
|
||||
|
||||
export interface CycleSecondarySubModeDeps {
|
||||
getSecondarySubMode: () => SecondarySubMode;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
getLastSecondarySubToggleAtMs: () => number;
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => void;
|
||||
broadcastSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
const SECONDARY_SUB_CYCLE: SecondarySubMode[] = ["hidden", "visible", "hover"];
|
||||
const SECONDARY_SUB_TOGGLE_DEBOUNCE_MS = 120;
|
||||
|
||||
export function cycleSecondarySubModeService(
|
||||
deps: CycleSecondarySubModeDeps,
|
||||
): void {
|
||||
const now = deps.now ? deps.now() : Date.now();
|
||||
if (now - deps.getLastSecondarySubToggleAtMs() < SECONDARY_SUB_TOGGLE_DEBOUNCE_MS) {
|
||||
return;
|
||||
}
|
||||
deps.setLastSecondarySubToggleAtMs(now);
|
||||
|
||||
const currentMode = deps.getSecondarySubMode();
|
||||
const currentIndex = SECONDARY_SUB_CYCLE.indexOf(currentMode);
|
||||
const nextMode =
|
||||
SECONDARY_SUB_CYCLE[(currentIndex + 1) % SECONDARY_SUB_CYCLE.length];
|
||||
deps.setSecondarySubMode(nextMode);
|
||||
deps.broadcastSecondarySubMode(nextMode);
|
||||
deps.showMpvOsd(`Secondary subtitle: ${nextMode}`);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { BrowserWindow, globalShortcut } from "electron";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:shortcut");
|
||||
|
||||
export interface GlobalShortcutConfig {
|
||||
toggleVisibleOverlayGlobal: string | null | undefined;
|
||||
@@ -38,7 +41,7 @@ export function registerGlobalShortcutsService(
|
||||
},
|
||||
);
|
||||
if (!toggleVisibleRegistered) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to register global shortcut toggleVisibleOverlayGlobal: ${visibleShortcut}`,
|
||||
);
|
||||
}
|
||||
@@ -56,7 +59,7 @@ export function registerGlobalShortcutsService(
|
||||
},
|
||||
);
|
||||
if (!toggleInvisibleRegistered) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to register global shortcut toggleInvisibleOverlayGlobal: ${invisibleShortcut}`,
|
||||
);
|
||||
}
|
||||
@@ -65,7 +68,7 @@ export function registerGlobalShortcutsService(
|
||||
normalizedInvisible &&
|
||||
normalizedInvisible === normalizedVisible
|
||||
) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Skipped registering toggleInvisibleOverlayGlobal because it collides with toggleVisibleOverlayGlobal",
|
||||
);
|
||||
}
|
||||
@@ -77,7 +80,7 @@ export function registerGlobalShortcutsService(
|
||||
normalizedJimaku === normalizedInvisible ||
|
||||
normalizedJimaku === normalizedSettings)
|
||||
) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"Skipped registering openJimaku because it collides with another global shortcut",
|
||||
);
|
||||
} else {
|
||||
@@ -88,7 +91,7 @@ export function registerGlobalShortcutsService(
|
||||
},
|
||||
);
|
||||
if (!openJimakuRegistered) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`Failed to register global shortcut openJimaku: ${options.shortcuts.openJimaku}`,
|
||||
);
|
||||
}
|
||||
@@ -99,7 +102,7 @@ export function registerGlobalShortcutsService(
|
||||
options.onOpenYomitanSettings();
|
||||
});
|
||||
if (!settingsRegistered) {
|
||||
console.warn("Failed to register global shortcut: Alt+Shift+Y");
|
||||
logger.warn("Failed to register global shortcut: Alt+Shift+Y");
|
||||
}
|
||||
|
||||
if (options.isDev) {
|
||||
@@ -110,7 +113,7 @@ export function registerGlobalShortcutsService(
|
||||
}
|
||||
});
|
||||
if (!devtoolsRegistered) {
|
||||
console.warn("Failed to register global shortcut: F12");
|
||||
logger.warn("Failed to register global shortcut: F12");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,7 @@ test("runStartupBootstrapRuntimeService configures startup state and starts life
|
||||
const result = runStartupBootstrapRuntimeService({
|
||||
argv: ["node", "main.ts", "--verbose"],
|
||||
parseArgs: () => args,
|
||||
setLogLevelEnv: (level) => calls.push(`setLog:${level}`),
|
||||
enableVerboseLogging: () => calls.push("enableVerbose"),
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
forceX11Backend: () => calls.push("forceX11"),
|
||||
enforceUnsupportedWaylandMode: () => calls.push("enforceWayland"),
|
||||
getDefaultSocketPath: () => "/tmp/default.sock",
|
||||
@@ -71,13 +70,39 @@ test("runStartupBootstrapRuntimeService configures startup state and starts life
|
||||
assert.equal(result.autoStartOverlay, true);
|
||||
assert.equal(result.texthookerOnlyMode, true);
|
||||
assert.deepEqual(calls, [
|
||||
"enableVerbose",
|
||||
"setLog:debug:cli",
|
||||
"forceX11",
|
||||
"enforceWayland",
|
||||
"startLifecycle",
|
||||
]);
|
||||
});
|
||||
|
||||
test("runStartupBootstrapRuntimeService prefers --log-level over --verbose", () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({
|
||||
logLevel: "warn",
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
runStartupBootstrapRuntimeService({
|
||||
argv: ["node", "main.ts", "--log-level", "warn", "--verbose"],
|
||||
parseArgs: () => args,
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
forceX11Backend: () => calls.push("forceX11"),
|
||||
enforceUnsupportedWaylandMode: () => calls.push("enforceWayland"),
|
||||
getDefaultSocketPath: () => "/tmp/default.sock",
|
||||
defaultTexthookerPort: 5174,
|
||||
runGenerateConfigFlow: () => false,
|
||||
startAppLifecycle: () => calls.push("startLifecycle"),
|
||||
});
|
||||
|
||||
assert.deepEqual(calls.slice(0, 3), [
|
||||
"setLog:warn:cli",
|
||||
"forceX11",
|
||||
"enforceWayland",
|
||||
]);
|
||||
});
|
||||
|
||||
test("runStartupBootstrapRuntimeService skips lifecycle when generate-config flow handled", () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({ generateConfig: true, logLevel: "warn" });
|
||||
@@ -85,8 +110,7 @@ test("runStartupBootstrapRuntimeService skips lifecycle when generate-config flo
|
||||
const result = runStartupBootstrapRuntimeService({
|
||||
argv: ["node", "main.ts", "--generate-config"],
|
||||
parseArgs: () => args,
|
||||
setLogLevelEnv: (level) => calls.push(`setLog:${level}`),
|
||||
enableVerboseLogging: () => calls.push("enableVerbose"),
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
forceX11Backend: () => calls.push("forceX11"),
|
||||
enforceUnsupportedWaylandMode: () => calls.push("enforceWayland"),
|
||||
getDefaultSocketPath: () => "/tmp/default.sock",
|
||||
@@ -99,7 +123,7 @@ test("runStartupBootstrapRuntimeService skips lifecycle when generate-config flo
|
||||
assert.equal(result.texthookerPort, 5174);
|
||||
assert.equal(result.backendOverride, null);
|
||||
assert.deepEqual(calls, [
|
||||
"setLog:warn",
|
||||
"setLog:warn:cli",
|
||||
"forceX11",
|
||||
"enforceWayland",
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CliArgs } from "../../cli/args";
|
||||
import { ConfigValidationWarning, SecondarySubMode } from "../../types";
|
||||
import type { LogLevelSource } from "../../logger";
|
||||
import { ConfigValidationWarning, ResolvedConfig, SecondarySubMode } from "../../types";
|
||||
|
||||
export interface StartupBootstrapRuntimeState {
|
||||
initialArgs: CliArgs;
|
||||
@@ -10,11 +11,27 @@ export interface StartupBootstrapRuntimeState {
|
||||
texthookerOnlyMode: boolean;
|
||||
}
|
||||
|
||||
interface RuntimeAutoUpdateOptionManagerLike {
|
||||
getOptionValue: (id: "anki.autoUpdateNewCards") => unknown;
|
||||
}
|
||||
|
||||
export interface RuntimeConfigLike {
|
||||
auto_start_overlay?: boolean;
|
||||
bind_visible_overlay_to_mpv_sub_visibility: boolean;
|
||||
invisibleOverlay: {
|
||||
startupVisibility: "visible" | "hidden" | "platform-default";
|
||||
};
|
||||
ankiConnect?: {
|
||||
behavior?: {
|
||||
autoUpdateNewCards?: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface StartupBootstrapRuntimeDeps {
|
||||
argv: string[];
|
||||
parseArgs: (argv: string[]) => CliArgs;
|
||||
setLogLevelEnv: (level: string) => void;
|
||||
enableVerboseLogging: () => void;
|
||||
setLogLevel: (level: string, source: LogLevelSource) => void;
|
||||
forceX11Backend: (args: CliArgs) => void;
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
|
||||
getDefaultSocketPath: () => string;
|
||||
@@ -29,9 +46,9 @@ export function runStartupBootstrapRuntimeService(
|
||||
const initialArgs = deps.parseArgs(deps.argv);
|
||||
|
||||
if (initialArgs.logLevel) {
|
||||
deps.setLogLevelEnv(initialArgs.logLevel);
|
||||
deps.setLogLevel(initialArgs.logLevel, "cli");
|
||||
} else if (initialArgs.verbose) {
|
||||
deps.enableVerboseLogging();
|
||||
deps.setLogLevel("debug", "cli");
|
||||
}
|
||||
|
||||
deps.forceX11Backend(initialArgs);
|
||||
@@ -61,6 +78,9 @@ interface AppReadyConfigLike {
|
||||
enabled?: boolean | "auto";
|
||||
port?: number;
|
||||
};
|
||||
logging?: {
|
||||
level?: "debug" | "info" | "warn" | "error";
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppReadyRuntimeDeps {
|
||||
@@ -71,6 +91,7 @@ export interface AppReadyRuntimeDeps {
|
||||
getResolvedConfig: () => AppReadyConfigLike;
|
||||
getConfigWarnings: () => ConfigValidationWarning[];
|
||||
logConfigWarning: (warning: ConfigValidationWarning) => void;
|
||||
setLogLevel: (level: string, source: LogLevelSource) => void;
|
||||
initRuntimeOptionsManager: () => void;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
defaultSecondarySubMode: SecondarySubMode;
|
||||
@@ -87,6 +108,40 @@ export interface AppReadyRuntimeDeps {
|
||||
handleInitialArgs: () => void;
|
||||
}
|
||||
|
||||
export function getInitialInvisibleOverlayVisibilityService(
|
||||
config: RuntimeConfigLike,
|
||||
platform: NodeJS.Platform,
|
||||
): boolean {
|
||||
const visibility = config.invisibleOverlay.startupVisibility;
|
||||
if (visibility === "visible") return true;
|
||||
if (visibility === "hidden") return false;
|
||||
if (platform === "linux") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shouldAutoInitializeOverlayRuntimeFromConfigService(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
if (config.auto_start_overlay === true) return true;
|
||||
if (config.invisibleOverlay.startupVisibility === "visible") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldBindVisibleOverlayToMpvSubVisibilityService(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
return config.bind_visible_overlay_to_mpv_sub_visibility;
|
||||
}
|
||||
|
||||
export function isAutoUpdateEnabledRuntimeService(
|
||||
config: ResolvedConfig | RuntimeConfigLike,
|
||||
runtimeOptionsManager: RuntimeAutoUpdateOptionManagerLike | null,
|
||||
): boolean {
|
||||
const value = runtimeOptionsManager?.getOptionValue("anki.autoUpdateNewCards");
|
||||
if (typeof value === "boolean") return value;
|
||||
return (config as ResolvedConfig).ankiConnect?.behavior?.autoUpdateNewCards !== false;
|
||||
}
|
||||
|
||||
export async function runAppReadyRuntimeService(
|
||||
deps: AppReadyRuntimeDeps,
|
||||
): Promise<void> {
|
||||
@@ -97,6 +152,7 @@ export async function runAppReadyRuntimeService(
|
||||
|
||||
deps.reloadConfig();
|
||||
const config = deps.getResolvedConfig();
|
||||
deps.setLogLevel(config.logging?.level ?? "info", "config");
|
||||
for (const warning of deps.getConfigWarnings()) {
|
||||
deps.logConfigWarning(warning);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ import {
|
||||
SubsyncResolvedConfig,
|
||||
} from "../../subsync/utils";
|
||||
import { isRemoteMediaPath } from "../../jimaku/utils";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:subsync");
|
||||
|
||||
interface FileExtractionResult {
|
||||
path: string;
|
||||
@@ -361,7 +364,7 @@ async function runSubsyncAutoInternal(
|
||||
return alassResult;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Auto alass sync failed, trying ffsubsync fallback:", error);
|
||||
logger.warn("Auto alass sync failed, trying ffsubsync fallback:", error);
|
||||
} finally {
|
||||
if (secondaryExtraction) {
|
||||
cleanupTemporaryFile(secondaryExtraction);
|
||||
|
||||
@@ -1,7 +1,40 @@
|
||||
import * as crypto from "crypto";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { SubtitlePosition } from "../../types";
|
||||
import { SecondarySubMode, SubtitlePosition } from "../../types";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:subtitle-position");
|
||||
|
||||
export interface CycleSecondarySubModeDeps {
|
||||
getSecondarySubMode: () => SecondarySubMode;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
getLastSecondarySubToggleAtMs: () => number;
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => void;
|
||||
broadcastSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
const SECONDARY_SUB_CYCLE: SecondarySubMode[] = ["hidden", "visible", "hover"];
|
||||
const SECONDARY_SUB_TOGGLE_DEBOUNCE_MS = 120;
|
||||
|
||||
export function cycleSecondarySubModeService(
|
||||
deps: CycleSecondarySubModeDeps,
|
||||
): void {
|
||||
const now = deps.now ? deps.now() : Date.now();
|
||||
if (now - deps.getLastSecondarySubToggleAtMs() < SECONDARY_SUB_TOGGLE_DEBOUNCE_MS) {
|
||||
return;
|
||||
}
|
||||
deps.setLastSecondarySubToggleAtMs(now);
|
||||
|
||||
const currentMode = deps.getSecondarySubMode();
|
||||
const currentIndex = SECONDARY_SUB_CYCLE.indexOf(currentMode);
|
||||
const nextMode = SECONDARY_SUB_CYCLE[(currentIndex + 1) % SECONDARY_SUB_CYCLE.length];
|
||||
deps.setSecondarySubMode(nextMode);
|
||||
deps.broadcastSecondarySubMode(nextMode);
|
||||
deps.showMpvOsd(`Secondary subtitle: ${nextMode}`);
|
||||
}
|
||||
|
||||
function getSubtitlePositionFilePath(
|
||||
mediaPath: string,
|
||||
@@ -97,7 +130,7 @@ export function loadSubtitlePositionService(options: {
|
||||
}
|
||||
return options.fallbackPosition;
|
||||
} catch (err) {
|
||||
console.error("Failed to load subtitle position:", (err as Error).message);
|
||||
logger.error("Failed to load subtitle position:", (err as Error).message);
|
||||
return options.fallbackPosition;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +144,7 @@ export function saveSubtitlePositionService(options: {
|
||||
}): void {
|
||||
if (!options.currentMediaPath) {
|
||||
options.onQueuePending(options.position);
|
||||
console.warn("Queued subtitle position save - no media path yet");
|
||||
logger.warn("Queued subtitle position save - no media path yet");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +156,7 @@ export function saveSubtitlePositionService(options: {
|
||||
);
|
||||
options.onPersisted();
|
||||
} catch (err) {
|
||||
console.error("Failed to save subtitle position:", (err as Error).message);
|
||||
logger.error("Failed to save subtitle position:", (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,8 +187,8 @@ export function updateCurrentMediaPathService(options: {
|
||||
);
|
||||
options.setSubtitlePosition(options.pendingSubtitlePosition);
|
||||
options.clearPendingSubtitlePosition();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Failed to persist queued subtitle position:",
|
||||
(err as Error).message,
|
||||
);
|
||||
|
||||
@@ -2,6 +2,9 @@ import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import WebSocket from "ws";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:subtitle-ws");
|
||||
|
||||
export function hasMpvWebsocketPlugin(): boolean {
|
||||
const mpvWebsocketPath = path.join(
|
||||
@@ -24,7 +27,7 @@ export class SubtitleWebSocketService {
|
||||
this.server = new WebSocket.Server({ port, host: "127.0.0.1" });
|
||||
|
||||
this.server.on("connection", (ws: WebSocket) => {
|
||||
console.log("WebSocket client connected");
|
||||
logger.info("WebSocket client connected");
|
||||
const currentText = getCurrentSubtitleText();
|
||||
if (currentText) {
|
||||
ws.send(JSON.stringify({ sentence: currentText }));
|
||||
@@ -32,10 +35,10 @@ export class SubtitleWebSocketService {
|
||||
});
|
||||
|
||||
this.server.on("error", (err: Error) => {
|
||||
console.error("WebSocket server error:", err.message);
|
||||
logger.error("WebSocket server error:", err.message);
|
||||
});
|
||||
|
||||
console.log(`Subtitle WebSocket server running on ws://127.0.0.1:${port}`);
|
||||
logger.info(`Subtitle WebSocket server running on ws://127.0.0.1:${port}`);
|
||||
}
|
||||
|
||||
public broadcast(text: string): void {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as fs from "fs";
|
||||
import * as http from "http";
|
||||
import * as path from "path";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:texthooker");
|
||||
|
||||
export class TexthookerService {
|
||||
private server: http.Server | null = null;
|
||||
@@ -12,7 +15,7 @@ export class TexthookerService {
|
||||
public start(port: number): http.Server | null {
|
||||
const texthookerPath = this.getTexthookerPath();
|
||||
if (!texthookerPath) {
|
||||
console.error("texthooker-ui not found");
|
||||
logger.error("texthooker-ui not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,7 +51,7 @@ export class TexthookerService {
|
||||
});
|
||||
|
||||
this.server.listen(port, "127.0.0.1", () => {
|
||||
console.log(`Texthooker server running at http://127.0.0.1:${port}`);
|
||||
logger.info(`Texthooker server running at http://127.0.0.1:${port}`);
|
||||
});
|
||||
|
||||
return this.server;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { BrowserWindow, Extension, session } from "electron";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:yomitan-extension-loader");
|
||||
|
||||
export interface YomitanExtensionLoaderDeps {
|
||||
userDataPath: string;
|
||||
@@ -49,7 +52,7 @@ function ensureExtensionCopy(sourceDir: string, userDataPath: string): string {
|
||||
fs.mkdirSync(extensionsRoot, { recursive: true });
|
||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||
fs.cpSync(sourceDir, targetDir, { recursive: true });
|
||||
console.log(`Copied yomitan extension to ${targetDir}`);
|
||||
logger.info(`Copied yomitan extension to ${targetDir}`);
|
||||
}
|
||||
|
||||
return targetDir;
|
||||
@@ -75,8 +78,8 @@ export async function loadYomitanExtensionService(
|
||||
}
|
||||
|
||||
if (!extPath) {
|
||||
console.error("Yomitan extension not found in any search path");
|
||||
console.error("Install Yomitan to one of:", searchPaths);
|
||||
logger.error("Yomitan extension not found in any search path");
|
||||
logger.error("Install Yomitan to one of:", searchPaths);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -102,8 +105,8 @@ export async function loadYomitanExtensionService(
|
||||
deps.setYomitanExtension(extension);
|
||||
return extension;
|
||||
} catch (err) {
|
||||
console.error("Failed to load Yomitan extension:", (err as Error).message);
|
||||
console.error("Full error:", err);
|
||||
logger.error("Failed to load Yomitan extension:", (err as Error).message);
|
||||
logger.error("Full error:", err);
|
||||
deps.setYomitanExtension(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { BrowserWindow, Extension, session } from "electron";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:yomitan-settings");
|
||||
|
||||
export interface OpenYomitanSettingsWindowOptions {
|
||||
yomitanExt: Extension | null;
|
||||
@@ -9,14 +12,14 @@ export interface OpenYomitanSettingsWindowOptions {
|
||||
export function openYomitanSettingsWindow(
|
||||
options: OpenYomitanSettingsWindowOptions,
|
||||
): void {
|
||||
console.log("openYomitanSettings called");
|
||||
logger.info("openYomitanSettings called");
|
||||
|
||||
if (!options.yomitanExt) {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Yomitan extension not loaded - yomitanExt is:",
|
||||
options.yomitanExt,
|
||||
);
|
||||
console.error(
|
||||
logger.error(
|
||||
"This may be due to Manifest V3 service worker issues with Electron",
|
||||
);
|
||||
return;
|
||||
@@ -24,12 +27,12 @@ export function openYomitanSettingsWindow(
|
||||
|
||||
const existingWindow = options.getExistingWindow();
|
||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
||||
console.log("Settings window already exists, focusing");
|
||||
logger.info("Settings window already exists, focusing");
|
||||
existingWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Creating new settings window for extension:", options.yomitanExt.id);
|
||||
logger.info("Creating new settings window for extension:", options.yomitanExt.id);
|
||||
|
||||
const settingsWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
@@ -44,7 +47,7 @@ export function openYomitanSettingsWindow(
|
||||
options.setWindow(settingsWindow);
|
||||
|
||||
const settingsUrl = `chrome-extension://${options.yomitanExt.id}/settings.html`;
|
||||
console.log("Loading settings URL:", settingsUrl);
|
||||
logger.info("Loading settings URL:", settingsUrl);
|
||||
|
||||
let loadAttempts = 0;
|
||||
const maxAttempts = 3;
|
||||
@@ -53,13 +56,13 @@ export function openYomitanSettingsWindow(
|
||||
settingsWindow
|
||||
.loadURL(settingsUrl)
|
||||
.then(() => {
|
||||
console.log("Settings URL loaded successfully");
|
||||
logger.info("Settings URL loaded successfully");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
console.error("Failed to load settings URL:", err);
|
||||
logger.error("Failed to load settings URL:", err);
|
||||
loadAttempts++;
|
||||
if (loadAttempts < maxAttempts && !settingsWindow.isDestroyed()) {
|
||||
console.log(
|
||||
logger.info(
|
||||
`Retrying in 500ms (attempt ${loadAttempts + 1}/${maxAttempts})`,
|
||||
);
|
||||
setTimeout(attemptLoad, 500);
|
||||
@@ -72,7 +75,7 @@ export function openYomitanSettingsWindow(
|
||||
settingsWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, errorCode, errorDescription) => {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Settings page failed to load:",
|
||||
errorCode,
|
||||
errorDescription,
|
||||
@@ -81,7 +84,7 @@ export function openYomitanSettingsWindow(
|
||||
);
|
||||
|
||||
settingsWindow.webContents.on("did-finish-load", () => {
|
||||
console.log("Settings page loaded successfully");
|
||||
logger.info("Settings page loaded successfully");
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
export type LogLevelSource = "cli" | "config";
|
||||
|
||||
type LogMethod = (message: string, ...meta: unknown[]) => void;
|
||||
|
||||
@@ -18,10 +19,25 @@ const LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||
error: 40,
|
||||
};
|
||||
|
||||
const DEFAULT_LOG_LEVEL: LogLevel = "info";
|
||||
|
||||
let cliLogLevel: LogLevel | undefined;
|
||||
let configLogLevel: LogLevel | undefined;
|
||||
|
||||
function pad(value: number): string {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function normalizeLogLevel(level: string | undefined): LogLevel | undefined {
|
||||
const normalized = (level || "").toLowerCase() as LogLevel;
|
||||
return LOG_LEVELS.includes(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
function getEnvLogLevel(): LogLevel | undefined {
|
||||
if (!process || !process.env) return undefined;
|
||||
return normalizeLogLevel(process.env.SUBMINER_LOG_LEVEL);
|
||||
}
|
||||
|
||||
function formatTimestamp(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = pad(date.getMonth() + 1);
|
||||
@@ -33,15 +49,29 @@ function formatTimestamp(date: Date): string {
|
||||
}
|
||||
|
||||
function resolveMinLevel(): LogLevel {
|
||||
const raw =
|
||||
typeof process !== "undefined" && process?.env
|
||||
? process.env.SUBMINER_LOG_LEVEL
|
||||
: undefined;
|
||||
const normalized = (raw || "").toLowerCase() as LogLevel;
|
||||
if (LOG_LEVELS.includes(normalized)) {
|
||||
return normalized;
|
||||
const envLevel = getEnvLogLevel();
|
||||
if (cliLogLevel) {
|
||||
return cliLogLevel;
|
||||
}
|
||||
if (envLevel) {
|
||||
return envLevel;
|
||||
}
|
||||
if (configLogLevel) {
|
||||
return configLogLevel;
|
||||
}
|
||||
return DEFAULT_LOG_LEVEL;
|
||||
}
|
||||
|
||||
export function setLogLevel(
|
||||
level: string | undefined,
|
||||
source: LogLevelSource = "cli",
|
||||
): void {
|
||||
const normalized = normalizeLogLevel(level);
|
||||
if (source === "cli") {
|
||||
cliLogLevel = normalized;
|
||||
} else {
|
||||
configLogLevel = normalized;
|
||||
}
|
||||
return "info";
|
||||
}
|
||||
|
||||
function normalizeError(error: Error): { message: string; stack?: string } {
|
||||
|
||||
42
src/main.ts
42
src/main.ts
@@ -68,6 +68,7 @@ import {
|
||||
import {
|
||||
getSubsyncConfig,
|
||||
} from "./subsync/utils";
|
||||
import { createLogger, setLogLevel, type LogLevelSource } from "./logger";
|
||||
import {
|
||||
parseArgs,
|
||||
shouldStartApp,
|
||||
@@ -228,17 +229,21 @@ const isDev =
|
||||
process.argv.includes("--dev") || process.argv.includes("--debug");
|
||||
const texthookerService = new TexthookerService();
|
||||
const subtitleWsService = new SubtitleWebSocketService();
|
||||
const logger = createLogger("main");
|
||||
let jlptDictionaryLookupInitialized = false;
|
||||
let jlptDictionaryLookupInitialization: Promise<void> | null = null;
|
||||
const appLogger = {
|
||||
logInfo: (message: string) => {
|
||||
console.log(message);
|
||||
logger.info(message);
|
||||
},
|
||||
logWarning: (message: string) => {
|
||||
console.warn(message);
|
||||
logger.warn(message);
|
||||
},
|
||||
logError: (message: string, details: unknown) => {
|
||||
logger.error(message, details);
|
||||
},
|
||||
logNoRunningInstance: () => {
|
||||
console.error("No running instance. Use --start to launch the app.");
|
||||
logger.error("No running instance. Use --start to launch the app.");
|
||||
},
|
||||
logConfigWarning: (warning: {
|
||||
path: string;
|
||||
@@ -246,7 +251,7 @@ const appLogger = {
|
||||
value: unknown;
|
||||
fallback: unknown;
|
||||
}) => {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`[config] ${warning.path}: ${warning.message} value=${JSON.stringify(warning.value)} fallback=${JSON.stringify(warning.fallback)}`,
|
||||
);
|
||||
},
|
||||
@@ -274,9 +279,7 @@ process.on("SIGTERM", () => {
|
||||
const overlayManager = createOverlayManagerService();
|
||||
const overlayContentMeasurementStore = createOverlayContentMeasurementStoreService({
|
||||
now: () => Date.now(),
|
||||
warn: (message: string) => {
|
||||
console.warn(message);
|
||||
},
|
||||
warn: (message: string) => logger.warn(message),
|
||||
});
|
||||
const overlayModalRuntime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
@@ -509,7 +512,7 @@ async function initializeJlptDictionaryLookup(): Promise<void> {
|
||||
appState.jlptLevelLookup = await createJlptVocabularyLookupService({
|
||||
searchPaths: getJlptDictionarySearchPaths(),
|
||||
log: (message) => {
|
||||
console.log(`[JLPT] ${message}`);
|
||||
logger.info(`[JLPT] ${message}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -593,11 +596,8 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
createStartupBootstrapRuntimeDeps({
|
||||
argv: process.argv,
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
setLogLevelEnv: (level: string) => {
|
||||
process.env.SUBMINER_LOG_LEVEL = level;
|
||||
},
|
||||
enableVerboseLogging: () => {
|
||||
process.env.SUBMINER_LOG_LEVEL = "debug";
|
||||
setLogLevel: (level: string, source: LogLevelSource) => {
|
||||
setLogLevel(level, source);
|
||||
},
|
||||
forceX11Backend: (args: CliArgs) => {
|
||||
forceX11Backend(args);
|
||||
@@ -624,7 +624,7 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
app.quit();
|
||||
},
|
||||
onGenerateConfigError: (error: Error) => {
|
||||
console.error(`Failed to generate config: ${error.message}`);
|
||||
logger.error(`Failed to generate config: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
app.quit();
|
||||
},
|
||||
@@ -655,6 +655,8 @@ const startupState = runStartupBootstrapRuntimeService(
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
||||
setLogLevel: (level: string, source: LogLevelSource) =>
|
||||
setLogLevel(level, source),
|
||||
initRuntimeOptionsManager: () => {
|
||||
appState.runtimeOptionsManager = new RuntimeOptionsManager(
|
||||
() => configService.getConfig().ankiConnect,
|
||||
@@ -759,7 +761,7 @@ function handleCliCommand(
|
||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||
openInBrowser: (url: string) => {
|
||||
void shell.openExternal(url).catch((error) => {
|
||||
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||
logger.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||
});
|
||||
},
|
||||
isOverlayInitialized: () => appState.overlayRuntimeInitialized,
|
||||
@@ -787,13 +789,13 @@ function handleCliCommand(
|
||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||
log: (message: string) => {
|
||||
console.log(message);
|
||||
logger.info(message);
|
||||
},
|
||||
warn: (message: string) => {
|
||||
console.warn(message);
|
||||
logger.warn(message);
|
||||
},
|
||||
error: (message: string, err: unknown) => {
|
||||
console.error(message, err);
|
||||
logger.error(message, err);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1092,7 +1094,7 @@ function showMpvOsd(text: string): void {
|
||||
appState.mpvClient,
|
||||
text,
|
||||
(line) => {
|
||||
console.log(line);
|
||||
logger.info(line);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1249,7 +1251,7 @@ function handleMineSentenceDigit(count: number): void {
|
||||
appState.mpvClient?.currentSecondarySubText || undefined,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
logError: (message, err) => {
|
||||
console.error(message, err);
|
||||
logger.error(message, err);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface AppReadyRuntimeDepsFactoryInput {
|
||||
hasMpvWebsocketPlugin: AppReadyRuntimeDeps["hasMpvWebsocketPlugin"];
|
||||
startSubtitleWebsocket: AppReadyRuntimeDeps["startSubtitleWebsocket"];
|
||||
log: AppReadyRuntimeDeps["log"];
|
||||
setLogLevel: AppReadyRuntimeDeps["setLogLevel"];
|
||||
createMecabTokenizerAndCheck: AppReadyRuntimeDeps["createMecabTokenizerAndCheck"];
|
||||
createSubtitleTimingTracker: AppReadyRuntimeDeps["createSubtitleTimingTracker"];
|
||||
loadYomitanExtension: AppReadyRuntimeDeps["loadYomitanExtension"];
|
||||
@@ -77,6 +78,7 @@ export function createAppReadyRuntimeDeps(
|
||||
hasMpvWebsocketPlugin: params.hasMpvWebsocketPlugin,
|
||||
startSubtitleWebsocket: params.startSubtitleWebsocket,
|
||||
log: params.log,
|
||||
setLogLevel: params.setLogLevel,
|
||||
createMecabTokenizerAndCheck: params.createMecabTokenizerAndCheck,
|
||||
createSubtitleTimingTracker: params.createSubtitleTimingTracker,
|
||||
loadYomitanExtension: params.loadYomitanExtension,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CliArgs } from "../cli/args";
|
||||
import type { ResolvedConfig } from "../types";
|
||||
import type { StartupBootstrapRuntimeDeps } from "../core/services/startup-service";
|
||||
import type { LogLevelSource } from "../logger";
|
||||
|
||||
export interface StartupBootstrapRuntimeFactoryDeps {
|
||||
argv: string[];
|
||||
parseArgs: (argv: string[]) => CliArgs;
|
||||
setLogLevelEnv: (level: string) => void;
|
||||
enableVerboseLogging: () => void;
|
||||
setLogLevel: (level: string, source: LogLevelSource) => void;
|
||||
forceX11Backend: (args: CliArgs) => void;
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
|
||||
shouldStartApp: (args: CliArgs) => boolean;
|
||||
@@ -34,8 +34,7 @@ export function createStartupBootstrapRuntimeDeps(
|
||||
return {
|
||||
argv: params.argv,
|
||||
parseArgs: params.parseArgs,
|
||||
setLogLevelEnv: params.setLogLevelEnv,
|
||||
enableVerboseLogging: params.enableVerboseLogging,
|
||||
setLogLevel: params.setLogLevel,
|
||||
forceX11Backend: (args: CliArgs) => params.forceX11Backend(args),
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) =>
|
||||
params.enforceUnsupportedWaylandMode(args),
|
||||
|
||||
@@ -348,6 +348,9 @@ export interface Config {
|
||||
jimaku?: JimakuConfig;
|
||||
invisibleOverlay?: InvisibleOverlayConfig;
|
||||
youtubeSubgen?: YoutubeSubgenConfig;
|
||||
logging?: {
|
||||
level?: "debug" | "info" | "warn" | "error";
|
||||
};
|
||||
}
|
||||
|
||||
export type RawConfig = Config;
|
||||
@@ -445,6 +448,9 @@ export interface ResolvedConfig {
|
||||
whisperModel: string;
|
||||
primarySubLanguages: string[];
|
||||
};
|
||||
logging: {
|
||||
level: "debug" | "info" | "warn" | "error";
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConfigValidationWarning {
|
||||
|
||||
Reference in New Issue
Block a user