Apply remaining working-tree updates

This commit is contained in:
2026-02-14 00:36:01 -08:00
parent cb9a599b23
commit a1209ca69f
40 changed files with 1001 additions and 607 deletions

View File

@@ -18,12 +18,27 @@ import {
SubsyncContext,
SubsyncResolvedConfig,
} from "../../subsync/utils";
import { isRemoteMediaPath } from "../../jimaku/utils";
interface FileExtractionResult {
path: string;
temporary: boolean;
}
function summarizeCommandFailure(command: string, result: CommandResult): string {
const parts = [
`code=${result.code ?? "n/a"}`,
result.stderr ? `stderr: ${result.stderr}` : "",
result.stdout ? `stdout: ${result.stdout}` : "",
result.error ? `error: ${result.error}` : "",
]
.map((value) => value.trim())
.filter(Boolean);
if (parts.length === 0) return `command failed (${command})`;
return `command failed (${command}) ${parts.join(" | ")}`;
}
interface MpvClientLike {
connected: boolean;
currentAudioStreamIndex: number | null;
@@ -36,6 +51,32 @@ interface SubsyncCoreDeps {
getResolvedConfig: () => SubsyncResolvedConfig;
}
function parseTrackId(value: unknown): number | null {
if (typeof value === "number") {
return Number.isInteger(value) ? value : null;
}
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed.length) return null;
const parsed = Number(trimmed);
return Number.isInteger(parsed) && String(parsed) === trimmed ? parsed : null;
}
return null;
}
function normalizeTrackIds(tracks: unknown[]): MpvTrack[] {
return tracks.map((track) => {
if (!track || typeof track !== "object") return track as MpvTrack;
const typed = track as MpvTrack & { id?: unknown };
const parsedId = parseTrackId(typed.id);
if (parsedId === null) {
const { id: _ignored, ...rest } = typed;
return rest as MpvTrack;
}
return { ...typed, id: parsedId };
});
}
export interface TriggerSubsyncFromConfigDeps extends SubsyncCoreDeps {
isSubsyncInProgress: () => boolean;
setSubsyncInProgress: (inProgress: boolean) => void;
@@ -69,12 +110,11 @@ async function gatherSubsyncContext(
}
const tracks = Array.isArray(trackListRaw)
? (trackListRaw as MpvTrack[])
? normalizeTrackIds(trackListRaw as MpvTrack[])
: [];
const subtitleTracks = tracks.filter((track) => track.type === "sub");
const sid = typeof sidRaw === "number" ? sidRaw : null;
const secondarySid =
typeof secondarySidRaw === "number" ? secondarySidRaw : null;
const sid = parseTrackId(sidRaw);
const secondarySid = parseTrackId(secondarySidRaw);
const primaryTrack = subtitleTracks.find((track) => track.id === sid);
if (!primaryTrack) {
@@ -147,7 +187,7 @@ async function extractSubtitleTrackToFile(
"-nostdin",
"-y",
"-loglevel",
"quiet",
"error",
"-an",
"-vn",
"-i",
@@ -160,7 +200,12 @@ async function extractSubtitleTrackToFile(
]);
if (!extraction.ok || !fileExists(outputPath)) {
throw new Error("Failed to extract internal subtitle track with ffmpeg");
throw new Error(
`Failed to extract internal subtitle track with ffmpeg: ${summarizeCommandFailure(
"ffmpeg",
extraction,
)}`,
);
}
return { path: outputPath, temporary: true };
@@ -264,9 +309,10 @@ async function subsyncToReference(
}
if (!result.ok || !fileExists(outputPath)) {
const details = summarizeCommandFailure(engine, result);
return {
ok: false,
message: `${engine} synchronization failed`,
message: `${engine} synchronization failed: ${details}`,
};
}
@@ -280,6 +326,14 @@ async function subsyncToReference(
}
}
function validateFfsubsyncReference(videoPath: string): void {
if (isRemoteMediaPath(videoPath)) {
throw new Error(
"FFsubsync cannot reliably sync stream URLs because it needs direct reference media access. Use Alass with a secondary subtitle source or play a local file.",
);
}
}
async function runSubsyncAutoInternal(
deps: SubsyncCoreDeps,
): Promise<SubsyncResult> {
@@ -325,6 +379,14 @@ async function runSubsyncAutoInternal(
message: "No secondary subtitle for alass and ffsubsync not configured",
};
}
try {
validateFfsubsyncReference(context.videoPath);
} catch (error) {
return {
ok: false,
message: `ffsubsync synchronization failed: ${(error as Error).message}`,
};
}
return subsyncToReference(
"ffsubsync",
context.videoPath,
@@ -343,6 +405,11 @@ export async function runSubsyncManualService(
const resolved = deps.getResolvedConfig();
if (request.engine === "ffsubsync") {
try {
validateFfsubsyncReference(context.videoPath);
} catch (error) {
return { ok: false, message: `ffsubsync synchronization failed: ${(error as Error).message}` };
}
return subsyncToReference(
"ffsubsync",
context.videoPath,