refactor: add main.ts decomposition guardrails and extract core helpers

This commit is contained in:
2026-02-09 19:33:36 -08:00
parent 272d92169d
commit 6922a6741f
15 changed files with 1331 additions and 823 deletions

150
src/subsync/utils.ts Normal file
View File

@@ -0,0 +1,150 @@
import * as fs from "fs";
import * as childProcess from "child_process";
import { DEFAULT_CONFIG } from "../config";
import { SubsyncConfig, SubsyncMode } from "../types";
export interface MpvTrack {
id?: number;
type?: string;
selected?: boolean;
external?: boolean;
lang?: string;
title?: string;
codec?: string;
"ff-index"?: number;
"external-filename"?: string;
}
export interface SubsyncResolvedConfig {
defaultMode: SubsyncMode;
alassPath: string;
ffsubsyncPath: string;
ffmpegPath: string;
}
const DEFAULT_SUBSYNC_EXECUTABLE_PATHS = {
alass: "/usr/bin/alass",
ffsubsync: "/usr/bin/ffsubsync",
ffmpeg: "/usr/bin/ffmpeg",
} as const;
export interface SubsyncContext {
videoPath: string;
primaryTrack: MpvTrack;
secondaryTrack: MpvTrack | null;
sourceTracks: MpvTrack[];
audioStreamIndex: number | null;
}
export interface CommandResult {
ok: boolean;
code: number | null;
stderr: string;
stdout: string;
error?: string;
}
export function getSubsyncConfig(
config: SubsyncConfig | undefined,
): SubsyncResolvedConfig {
const resolvePath = (value: string | undefined, fallback: string): string => {
const trimmed = value?.trim();
return trimmed && trimmed.length > 0 ? trimmed : fallback;
};
return {
defaultMode: config?.defaultMode ?? DEFAULT_CONFIG.subsync.defaultMode,
alassPath: resolvePath(
config?.alass_path,
DEFAULT_SUBSYNC_EXECUTABLE_PATHS.alass,
),
ffsubsyncPath: resolvePath(
config?.ffsubsync_path,
DEFAULT_SUBSYNC_EXECUTABLE_PATHS.ffsubsync,
),
ffmpegPath: resolvePath(
config?.ffmpeg_path,
DEFAULT_SUBSYNC_EXECUTABLE_PATHS.ffmpeg,
),
};
}
export function hasPathSeparators(value: string): boolean {
return value.includes("/") || value.includes("\\");
}
export function fileExists(pathOrEmpty: string): boolean {
if (!pathOrEmpty) return false;
try {
return fs.existsSync(pathOrEmpty);
} catch {
return false;
}
}
export function formatTrackLabel(track: MpvTrack): string {
const trackId = typeof track.id === "number" ? track.id : -1;
const source = track.external ? "External" : "Internal";
const lang = track.lang || track.title || "unknown";
const active = track.selected ? " (active)" : "";
return `${source} #${trackId} - ${lang}${active}`;
}
export function getTrackById(
tracks: MpvTrack[],
trackId: number | null,
): MpvTrack | null {
if (trackId === null) return null;
return tracks.find((track) => track.id === trackId) ?? null;
}
export function codecToExtension(codec: string | undefined): string | null {
if (!codec) return null;
const normalized = codec.toLowerCase();
if (normalized === "subrip" || normalized === "srt") return "srt";
if (normalized === "ass" || normalized === "ssa") return "ass";
return null;
}
export function runCommand(
executable: string,
args: string[],
timeoutMs = 120000,
): Promise<CommandResult> {
return new Promise((resolve) => {
const child = childProcess.spawn(executable, args, {
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
const timeout = setTimeout(() => {
child.kill("SIGKILL");
}, timeoutMs);
child.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
child.on("error", (error: Error) => {
clearTimeout(timeout);
resolve({
ok: false,
code: null,
stderr,
stdout,
error: error.message,
});
});
child.on("close", (code: number | null) => {
clearTimeout(timeout);
resolve({
ok: code === 0,
code,
stderr,
stdout,
});
});
});
}