// Which external tools are on this machine, and which pipeline needs which. // // Plain ESM with no app imports so `umtool doctor` runs from a terminal, and so // the app's /api/doctor and the CLI cannot disagree about what was probed. The // PROBE itself (run the version flag, read the version, ENOENT = absent) is // common/lib/toolProbe.mjs, shared with `archilyzer doctor`, which reports this // table too; only the table lives here. // // THE ONE RULE: this is never run during a render, and never from a page // render. Probing seven binaries is ~100 ms of fork/exec, which is nothing once // and a tax on every request if it leaks into a page. So the app keeps a cached // report with a TTL and the dashboard shows the cache or "not checked" plus a // button; only the button and the CLI probe. import path from "node:path"; import { probeTool } from "yt-dlp-transcript-common/lib/toolProbe.mjs"; import { SONG_SCRATCH } from "./paths.mjs"; /** Mirrors lib/faces.ts, which re-exports these so the two cannot drift. */ export const facedetPython = () => process.env.FACEDET_PYTHON ?? path.join(SONG_SCRATCH, "facedet", "bin", "python"); export const songCode = () => process.env.SONG_CODE_DIR ? path.resolve(process.env.SONG_CODE_DIR) : path.join(/* turbopackIgnore: true */ process.cwd(), "song"); export const facecropPy = () => path.join(/* turbopackIgnore: true */ songCode(), "facecrop.py"); /** * The tools, with the env override each pipeline script honours. `required` * marks the ones a report build cannot run without at all; the rest are needed * by parts of a build (the cards, the rail) or by another kind. * * `magick`, not `convert`: every ImageMagick call in the pipeline is * `execFile("magick", …)` (ImageMagick 7). A machine with only `convert` has * ImageMagick and still cannot build a card, and the report says so. */ export const TOOLS = () => [ { id: "ffmpeg", bin: process.env.FFMPEG_BIN ?? "ffmpeg", args: ["-version"], neededBy: ["report-video build", "song cuts", "mix"], required: true }, { id: "ffprobe", bin: process.env.FFPROBE_BIN ?? "ffprobe", args: ["-version"], neededBy: ["report-video verify", "browse posters", "mix"], required: true }, { id: "yt-dlp", bin: process.env.YTDLP_BIN ?? "yt-dlp", args: ["--version"], neededBy: ["report-video fetch", "check-sources"], required: true }, { id: "qrencode", bin: process.env.QRENCODE_BIN ?? "qrencode", args: ["--version"], neededBy: ["report-video QR"], required: true }, { id: "imagemagick", bin: "magick", args: ["-version"], fallback: "convert", neededBy: ["report-video cards, rail, chrome"], required: false }, { id: "rsvg-convert", bin: process.env.RSVG_BIN ?? "rsvg-convert", args: ["--version"], neededBy: ["report-video chart", "brand preset"], required: false }, { id: "python", bin: facedetPython(), args: ["--version"], neededBy: ["faces (facecrop.py)"], required: false }, { id: "facecrop.py", file: facecropPy(), neededBy: ["faces"], required: false }, ]; /** * Run every tool's version flag. ~100 ms in total, in parallel. * @returns {Promise<{ checkedAt: number, tools: Array<{id:string,bin:string,present:boolean,version:string|null,error:string|null,neededBy:string[],required:boolean}>, ok: boolean }>} */ export async function probeTools() { const tools = await Promise.all(TOOLS().map((t) => probeTool(t))); return { checkedAt: Date.now(), tools, // The report pipeline's hard requirements, all present. ok: tools.every((t) => !t.required || t.present), }; }