import { test } from "node:test"; import assert from "node:assert/strict"; import { chmod, lstat, mkdir, mkdtemp, readFile, readlink, rename, rm, symlink, writeFile, } from "node:fs/promises"; import { existsSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import type { Paths } from "../lib/paths"; import { tierVideoDir } from "../lib/mediaTier-server"; import { transcodeAudio } from "./transcode"; import { normalizeLiveChat, isLiveChatCuesFresh } from "./normalizeLiveChat"; import { removeWrongFormatAudio } from "./removeWrongFormatAudio"; import { normalizeAllLiveChat } from "./normalizeAllLiveChat"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test controller/mediaTierHooks.test.ts // // RELEASE 17, THE MEDIA TIER, AT ITS CALL SITES: the finalisers call the hook // and the deleters go through the link. A channel tiered IN PLACE — a real // `channels//media/` on the same disk — is the fixture: the hook's // cross-device path is lib/mediaTier-server.test.ts's. async function fixture() { const root = await mkdtemp(path.join(tmpdir(), "media-hooks-")); const paths = { transcriptsDir: root, channelsDir: path.join(root, "channels"), ffmpegBin: path.join(root, "ffmpeg"), } as Paths; const channelDir = path.join(paths.channelsDir, "chan"); const videoDir = path.join(channelDir, "data", "v1"); const mediaDir = path.join(channelDir, "media"); await mkdir(videoDir, { recursive: true }); await mkdir(mediaDir); await writeFile( path.join(channelDir, "config.json"), JSON.stringify({ handling: "transcribe", url: "https://example.com/c", audioFormat: "mp3" }), ); await writeFile( path.join(videoDir, "metadata.info.json"), JSON.stringify({ id: "v1", title: "A video", duration: 60 }), ); // A fake ffmpeg: copies its input (-i) to its last argument. await writeFile( paths.ffmpegBin, '#!/bin/sh\nin=""\nprev=""\nfor a in "$@"; do if [ "$prev" = "-i" ]; then in="$a"; fi; prev="$a"; last="$a"; done\ncp "$in" "$last"\n', ); await chmod(paths.ffmpegBin, 0o755); return { root, paths, channelDir, videoDir, mediaDir }; } test("transcodeAudio tiers its output, and re-tiers over a link the rename replaced", async () => { const f = await fixture(); try { await writeFile(path.join(f.videoDir, "audio.m4a"), "SOURCE-1"); const run = () => transcodeAudio({ paths: f.paths, videoDir: f.videoDir, sourceFilename: "audio.m4a", targetFormat: "mp3", onLog: () => {}, signal: new AbortController().signal, }); await run(); const out = path.join(f.videoDir, "audio.mp3"); assert.ok((await lstat(out)).isSymbolicLink()); assert.equal(await readlink(out), "../../media/v1/audio.mp3"); assert.equal(await readFile(out, "utf8"), "SOURCE-1"); // Again, over the link: the stale bytes in media/ are replaced, the link // made again — nothing orphaned, nothing left real. await writeFile(path.join(f.videoDir, "audio.m4a"), "SOURCE-2"); await run(); assert.ok((await lstat(out)).isSymbolicLink()); assert.equal(await readFile(path.join(f.mediaDir, "v1", "audio.mp3"), "utf8"), "SOURCE-2"); } finally { await rm(f.root, { recursive: true, force: true }); } }); test("normalizeLiveChat tiers the raw replay after writing the cues; freshness never reaches the media drive", async () => { const f = await fixture(); try { await writeFile(path.join(f.videoDir, "transcript.live_chat.json"), ""); const first = await normalizeLiveChat({ videoDir: f.videoDir, channelSlug: "chan" }); assert.equal(first.status, "wrote"); const raw = path.join(f.videoDir, "transcript.live_chat.json"); assert.ok((await lstat(raw)).isSymbolicLink()); assert.ok((await lstat(path.join(f.videoDir, "live_chat.cues.json"))).isFile(), "the cues are text"); // The media drive goes away: the cues are still fresh, from the link's mtime. await rename(f.mediaDir, `${f.mediaDir}-away`); assert.equal((await isLiveChatCuesFresh(f.videoDir)).fresh, true); assert.equal((await normalizeLiveChat({ videoDir: f.videoDir, channelSlug: "chan" })).status, "fresh"); await rename(`${f.mediaDir}-away`, f.mediaDir); } finally { await rm(f.root, { recursive: true, force: true }); } }); test("a deleter derefs: removeWrongFormatAudio removes the bytes in media/, not just the link", async () => { const f = await fixture(); try { await writeFile(path.join(f.videoDir, "audio.mp3"), "KEEP"); await writeFile(path.join(f.videoDir, "audio.m4a"), "WRONG"); await tierVideoDir(f.videoDir); assert.ok((await lstat(path.join(f.videoDir, "audio.m4a"))).isSymbolicLink()); const r = await removeWrongFormatAudio({ channelSlug: "chan", paths: f.paths, onLog: () => {} }); assert.equal(r.removedFiles, 1); assert.equal(existsSync(path.join(f.mediaDir, "v1", "audio.m4a")), false, "no orphan on the media tier"); await assert.rejects(lstat(path.join(f.videoDir, "audio.m4a"))); assert.equal(await readFile(path.join(f.videoDir, "audio.mp3"), "utf8"), "KEEP"); } finally { await rm(f.root, { recursive: true, force: true }); } }); test("normalize-live-chat skips a channel whose media is not reachable", async () => { const f = await fixture(); try { // Relocated media on an unmounted drive. await rm(f.mediaDir, { recursive: true }); const target = path.join(f.root, "platter", "chan", "media"); await writeFile( path.join(f.channelDir, "config.json"), JSON.stringify({ handling: "transcribe", url: "https://example.com/c", mediaDir: target }), ); await symlink(target, f.mediaDir); const lines: string[] = []; const r = await normalizeAllLiveChat({ paths: f.paths, onLog: (l) => lines.push(l) }); assert.equal(r.failed, 1); assert.ok(lines.some((l) => /Normalize live chat chan: SKIPPED — .*not reachable/.test(l)), lines.join("\n")); } finally { await rm(f.root, { recursive: true, force: true }); } });