import { test } from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { copyFile, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { execa } from "execa"; import parseTorrent from "parse-torrent"; import type { Paths } from "../lib/paths"; import type { ChannelConfig } from "../lib/channelConfig"; import { loadSavedVideo } from "../lib/savedVideo-server"; import { loadPlayableManifest, makePlayableTorrent, preparePlayable, type PreparePlayableOptions, } from "./preparePlayable"; // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test controller/preparePlayable.test.ts // // Fixture media made here by ffmpeg (a few seconds of test pattern and tone), // in a temp dir. Skipped where ffmpeg/ffprobe are not installed. const SLUG = "demo"; const have = async (bin: string) => (await execa(bin, ["-version"], { reject: false }).catch(() => null))?.exitCode === 0; const tools = (await have("ffmpeg")) && (await have("ffprobe")); async function make(file: string, vArgs: string[], aArgs: string[]): Promise { await execa("ffmpeg", [ "-nostdin", "-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", "testsrc=d=2:s=96x64:r=10", "-f", "lavfi", "-i", "sine=d=2:f=440", ...vArgs, ...aArgs, "-shortest", file, ]); } const sha = async (f: string) => createHash("sha256").update(await readFile(f)).digest("hex"); // Top-level MP4 atoms in order — "moov" before "mdat" is a faststart file. async function topAtoms(file: string): Promise { const b = await readFile(file); const out: string[] = []; let at = 0; while (at + 8 <= b.length) { let size = b.readUInt32BE(at); const type = b.toString("latin1", at + 4, at + 8); if (size === 1) size = Number(b.readBigUInt64BE(at + 8)); if (size < 8) break; out.push(type); at += size; } return out; } async function videoFrameMd5(file: string): Promise { const r = await execa("ffmpeg", ["-nostdin", "-loglevel", "error", "-i", file, "-map", "0:V:0", "-c", "copy", "-f", "framemd5", "-"]); // Drop the header comments (they name the container's timebase). return String(r.stdout).split("\n").filter((l) => l && !l.startsWith("#")).map((l) => l.split(",").pop()).join("\n"); } type Fixture = { dir: string; paths: Paths; root: string; opts: (o?: Partial) => PreparePlayableOptions; lines: string[] }; async function withFixture(fn: (f: Fixture) => Promise): Promise { const dir = await mkdtemp(path.join(tmpdir(), "ttb-playable-")); const paths = { transcriptsDir: path.join(dir, "corpus"), channelsDir: path.join(dir, "corpus", "channels"), savedVideosDir: path.join(dir, "media", "saved-videos"), ffmpegBin: "ffmpeg", ffprobeBin: "ffprobe", } as Paths; const root = path.join(dir, "media", "playable"); const lines: string[] = []; const opts = (o: Partial = {}): PreparePlayableOptions => ({ paths, slug: SLUG, channelConfig: { name: "Demo", handling: "youtube", platform: "youtube" } as unknown as ChannelConfig, onLog: (l) => lines.push(l), deps: { now: () => new Date("2026-10-09T12:00:00Z") }, ...o, }); try { await fn({ dir, paths, root, opts, lines }); } finally { await rm(dir, { recursive: true, force: true }); } } // One saved container per id, with a pointer (sha256 recorded or not). async function saved(paths: Paths, id: string, src: string, ext: string, withSha = true): Promise { const storeDir = path.join(paths.savedVideosDir, SLUG, id); await mkdir(storeDir, { recursive: true }); const file = path.join(storeDir, `source-media.${ext}`); await copyFile(src, file); const vd = path.join(paths.channelsDir, SLUG, "data", id); await mkdir(vd, { recursive: true }); await writeFile( path.join(vd, "saved-video.json"), JSON.stringify({ storedAt: "x", dir: storeDir, file: `source-media.${ext}`, bytes: (await stat(file)).size, keepReason: "pin", ...(withSha ? { sha256: await sha(file) } : {}), }), ); return file; } test("remuxes each container losslessly, makes its torrent, and a re-run is a no-op", { skip: !tools && "ffmpeg/ffprobe not installed" }, async () => { await withFixture(async ({ dir, paths, root, opts }) => { const src = path.join(dir, "src"); await mkdir(src); await make(path.join(src, "a.mp4"), ["-c:v", "libx264", "-preset", "ultrafast"], ["-c:a", "aac"]); await make(path.join(src, "b.mkv"), ["-c:v", "libvpx-vp9", "-deadline", "realtime", "-cpu-used", "8"], ["-c:a", "libopus"]); await make(path.join(src, "c.webm"), ["-c:v", "libvpx-vp9", "-deadline", "realtime", "-cpu-used", "8"], ["-c:a", "libopus"]); await make(path.join(src, "d.mkv"), ["-c:v", "mpeg4"], ["-c:a", "aac"]); // The source mp4 has its moov at the back (ffmpeg's default). assert.ok((await topAtoms(path.join(src, "a.mp4"))).indexOf("moov") > (await topAtoms(path.join(src, "a.mp4"))).indexOf("mdat")); const aSrc = await saved(paths, "aaaaaaaaaaa", path.join(src, "a.mp4"), "mp4"); await saved(paths, "bbbbbbbbbbb", path.join(src, "b.mkv"), "mkv", false); await saved(paths, "ccccccccccc", path.join(src, "c.webm"), "webm"); await saved(paths, "ddddddddddd", path.join(src, "d.mkv"), "mkv"); await mkdir(path.join(paths.channelsDir, SLUG, "data", "eeeeeeeeeee"), { recursive: true }); // held, no container let remuxes = 0; const counting = (o: Partial = {}) => opts({ root, ...o, deps: { now: () => new Date("2026-10-09T12:00:00Z"), remux: async (s, d, args) => { remuxes += 1; await execa("ffmpeg", ["-nostdin", "-hide_banner", "-loglevel", "error", "-y", "-i", s, ...args, d]); }, }, }); const r = await preparePlayable(counting()); assert.deepEqual(r.prepared, ["aaaaaaaaaaa", "bbbbbbbbbbb", "ccccccccccc"]); assert.deepEqual(r.unplayable.map((u) => u.id), ["ddddddddddd"]); assert.match(r.unplayable[0].reason, /mpeg4/); assert.deepEqual(r.failed, []); assert.equal(remuxes, 3); const m = await loadPlayableManifest(root, SLUG); assert.deepEqual(Object.keys(m.entries).sort(), ["aaaaaaaaaaa", "bbbbbbbbbbb", "ccccccccccc"]); const a = m.entries.aaaaaaaaaaa; assert.equal(a.file, "aaaaaaaaaaa/aaaaaaaaaaa.mp4"); assert.equal(a.mime, "video/mp4"); assert.equal(a.sourceSha256, await sha(aSrc)); assert.equal(m.entries.bbbbbbbbbbb.ext, "mp4", "vp9/opus in mkv → mp4"); assert.equal(m.entries.ccccccccccc.ext, "webm", "vp9/opus in webm stays webm"); assert.equal(m.entries.bbbbbbbbbbb.acodec, "opus"); // Faststart: the index is in front now. const atoms = await topAtoms(path.join(root, SLUG, a.file)); assert.ok(atoms.indexOf("moov") < atoms.indexOf("mdat"), atoms.join(",")); // Lossless: every video packet is bit-identical. assert.equal(await videoFrameMd5(path.join(root, SLUG, a.file)), await videoFrameMd5(aSrc)); // Bitexact: the same source remuxes to the same bytes (so to the same infohash). const again1 = path.join(dir, "again.mp4"); const decision = (await import("../lib/playableMedia")).decidePlayable({ formatName: "", ext: "mp4", vcodec: "h264", acodec: "aac" }); assert.ok(decision.playable); if (decision.playable) { await execa("ffmpeg", ["-nostdin", "-loglevel", "error", "-y", "-i", aSrc, ...decision.args, again1]); assert.equal(await sha(again1), await sha(path.join(root, SLUG, a.file))); } // The torrent: one file, named ., the manifest's infohash, and // nothing that names a person, a tool or a URL. const tBuf = await readFile(path.join(root, SLUG, a.torrent)); const t = await parseTorrent(tBuf); assert.equal(t.infoHash, a.infoHash); assert.equal(t.name, "aaaaaaaaaaa.mp4"); assert.equal(t.length, a.bytes); assert.deepEqual(t.urlList, []); assert.deepEqual(t.announce, []); assert.equal(t.createdBy, undefined); assert.equal(t.comment, undefined); assert.notEqual(t.private, true); for (const k of ["created by", "comment", "url-list", "announce", "private"]) { assert.equal(tBuf.includes(Buffer.from(`${k.length}:${k}`)), false, `no ${k}`); } // A pointer that had no hash got one. assert.match((await loadSavedVideo(path.join(paths.channelsDir, SLUG, "data", "bbbbbbbbbbb")))?.sha256 ?? "", /^[0-9a-f]{64}$/); // Again: nothing to do, nothing remuxed, the manifest unchanged. const before = await readFile(path.join(root, SLUG, "playable.json"), "utf8"); const again = await preparePlayable(counting()); assert.deepEqual(again.unchanged.sort(), ["aaaaaaaaaaa", "bbbbbbbbbbb", "ccccccccccc"]); assert.deepEqual(again.prepared, []); assert.equal(remuxes, 3); assert.equal(await readFile(path.join(root, SLUG, "playable.json"), "utf8"), before); assert.deepEqual(again.noContainer, []); }); }); test("each remux goes through the heavy slot's gate when the checkout has one", { skip: !tools && "ffmpeg/ffprobe not installed" }, async () => { await withFixture(async ({ dir, paths, root, opts }) => { await make(path.join(dir, "a.mp4"), ["-c:v", "libx264", "-preset", "ultrafast"], ["-c:a", "aac"]); await saved(paths, "aaaaaaaaaaa", path.join(dir, "a.mp4"), "mp4"); // A stand-in for scripts/queue-lock.mjs: records its argv, runs the rest. const repo = path.join(dir, "repo"); await mkdir(path.join(repo, "scripts"), { recursive: true }); const marker = path.join(dir, "gate.json"); await writeFile( path.join(repo, "scripts", "queue-lock.mjs"), `import { writeFileSync } from "node:fs";\nimport { spawnSync } from "node:child_process";\n` + `const argv = process.argv.slice(2);\nwriteFileSync(${JSON.stringify(marker)}, JSON.stringify(argv));\n` + `const at = argv.indexOf("--");\nconst r = spawnSync(argv[at + 1], argv.slice(at + 2), { stdio: "inherit" });\nprocess.exit(r.status ?? 1);\n`, ); const r = await preparePlayable(opts({ root, paths: { ...paths, monorepoRoot: repo } as Paths })); assert.deepEqual(r.prepared, ["aaaaaaaaaaa"]); const argv = JSON.parse(await readFile(marker, "utf8")) as string[]; assert.deepEqual(argv.slice(0, 3), ["--heavy", "--", "ffmpeg"]); }); }); test("dryRun decides and writes nothing", { skip: !tools && "ffmpeg/ffprobe not installed" }, async () => { await withFixture(async ({ dir, paths, root, opts }) => { await make(path.join(dir, "a.mp4"), ["-c:v", "libx264", "-preset", "ultrafast"], ["-c:a", "aac"]); await saved(paths, "aaaaaaaaaaa", path.join(dir, "a.mp4"), "mp4", false); const r = await preparePlayable(opts({ root, dryRun: true })); assert.deepEqual(r.prepared, ["aaaaaaaaaaa"]); await assert.rejects(stat(root)); assert.equal((await loadSavedVideo(path.join(paths.channelsDir, SLUG, "data", "aaaaaaaaaaa")))?.sha256, undefined); }); }); test("the default root is playable/ beside the store's real directory; an absent drive refuses", async () => { await withFixture(async ({ dir, paths, opts }) => { await mkdir(paths.savedVideosDir, { recursive: true }); const r = await preparePlayable(opts({ ids: [] })); assert.equal(r.root, path.join(dir, "media", "playable")); await assert.rejects( preparePlayable(opts({ root: path.join(dir, "not-mounted", "playable"), ids: [] })), /is not there/, ); }); }); test("torrent determinism: same bytes + piece length → same infohash, whatever the trackers or the path", async () => { const dir = await mkdtemp(path.join(tmpdir(), "ttb-torrent-")); try { const bytes = Buffer.from(Array.from({ length: 700_000 }, (_, i) => (i * 31) & 0xff)); const f1 = path.join(dir, "one.bin"); const f2 = path.join(dir, "elsewhere.bin"); await writeFile(f1, bytes); await writeFile(f2, bytes); const base = { name: "x.mp4", bytes: bytes.length }; const a = await makePlayableTorrent({ ...base, file: f1 }); const b = await makePlayableTorrent({ ...base, file: f2 }); const c = await makePlayableTorrent({ ...base, file: f1, trackers: ["wss://tracker.example/announce", "udp://t.example:6969"] }); const d = await makePlayableTorrent({ ...base, file: f1, pieceLength: 512 * 1024 }); assert.match(a.infoHash, /^[0-9a-f]{40}$/); assert.equal(a.infoHash, b.infoHash, "the path does not matter"); assert.equal(a.infoHash, c.infoHash, "the trackers are outside the info dict"); assert.notEqual(a.infoHash, d.infoHash, "the piece length is inside it"); assert.equal(a.pieceLength, 256 * 1024); assert.deepEqual(a.torrent, b.torrent, "fixed creation date: the same .torrent byte for byte"); const parsed = await parseTorrent(c.torrent); assert.deepEqual(parsed.announce, ["wss://tracker.example/announce", "udp://t.example:6969"]); // Different bytes, different hash. bytes[0] ^= 1; await writeFile(f2, bytes); assert.notEqual((await makePlayableTorrent({ ...base, file: f2 })).infoHash, a.infoHash); } finally { await rm(dir, { recursive: true, force: true }); } });