import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import type { Paths } from "../lib/paths"; import { persistSourceVideo, loadSavedVideo } from "../lib/savedVideo-server"; import { setDoNotClean } from "../lib/doNotClean-server"; import { pruneSavedVideos } from "./pruneSavedVideos"; import type { SavedVideoKeepReason } from "../lib/savedVideo"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test controller/pruneSavedVideos.test.ts async function withPaths(fn: (paths: Paths) => Promise): Promise { const dir = await mkdtemp(path.join(tmpdir(), "ttb-prune-")); const paths = { channelsDir: path.join(dir, "channels"), savedVideosDir: path.join(dir, "saved"), } as Paths; try { await fn(paths); } finally { await rm(dir, { recursive: true, force: true }); } } // Seed a data/ dir with an upload date + a persisted source container. async function seedSaved( paths: Paths, slug: string, id: string, uploadDate: string, keepReason: SavedVideoKeepReason, ): Promise { const videoDir = path.join(paths.channelsDir, slug, "data", id); await mkdir(videoDir, { recursive: true }); await writeFile( path.join(videoDir, "metadata.info.json"), JSON.stringify({ id, upload_date: uploadDate }), ); await writeFile(path.join(videoDir, "source-media.mp4"), `${id}-bytes`); await persistSourceVideo({ videoDir, sourceFilename: "source-media.mp4", storeDir: path.join(paths.savedVideosDir, slug, id), keepReason, }); } async function writeConfig( paths: Paths, slug: string, keepLatest: number, ): Promise { const dir = path.join(paths.channelsDir, slug); await mkdir(dir, { recursive: true }); await writeFile( path.join(dir, "config.json"), JSON.stringify({ handling: "transcribe", keepLatest }), ); } test("prunes only keep-latest containers that rolled out of the window", async () => { await withPaths(async (paths) => { await writeConfig(paths, "ch", 2); // Newest two (by date): c, b -> kept. a is the rolled-out keep-latest one. await seedSaved(paths, "ch", "a", "20240101", "keep-latest"); // evict await seedSaved(paths, "ch", "b", "20240201", "keep-latest"); // in window await seedSaved(paths, "ch", "c", "20240301", "keep-latest"); // in window const res = await pruneSavedVideos({ channelSlug: "ch", paths }); assert.equal(res.scanned, 3); assert.equal(res.pruned, 1); assert.equal(await loadSavedVideo(path.join(paths.channelsDir, "ch", "data", "a")), null); assert.notEqual(await loadSavedVideo(path.join(paths.channelsDir, "ch", "data", "b")), null); assert.notEqual(await loadSavedVideo(path.join(paths.channelsDir, "ch", "data", "c")), null); }); }); test("never evicts override or pinned containers, even out of window", async () => { await withPaths(async (paths) => { await writeConfig(paths, "ch", 1); await seedSaved(paths, "ch", "newest", "20240301", "keep-latest"); // Out-of-window manual archive -> kept. await seedSaved(paths, "ch", "archived", "20240101", "override"); // Out-of-window pin-category container -> kept. await seedSaved(paths, "ch", "pinptr", "20240102", "pin"); // Out-of-window keep-latest, but separately do-not-clean pinned -> kept. await seedSaved(paths, "ch", "deleted", "20240103", "keep-latest"); await setDoNotClean( path.join(paths.channelsDir, "ch", "data", "deleted"), true, "deleted from source", ); const res = await pruneSavedVideos({ channelSlug: "ch", paths }); assert.equal(res.pruned, 0); for (const id of ["archived", "pinptr", "deleted"]) { assert.notEqual( await loadSavedVideo(path.join(paths.channelsDir, "ch", "data", id)), null, `${id} should be kept`, ); } }); });