import { test } from "node:test"; import assert from "node:assert/strict"; import { archiveFolderInfoJson, countAttachRows, descriptionFromText, groupArchiveFolders, idFromFolderName, idFromMediaName, planAttach, publishedDateFromText, titleFromFolderName, type SourceFile, } from "./attachMediaPlan"; // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test lib/attachMediaPlan.test.ts // // The folder shapes are the pilot archive's (release 21), with synthetic ids // and titles: `NNN - - (<id>)` holding either yt-dlp's files // (`<title>-<id>.<ext>` + `.info.json`) or a bare `<id>.<ext>` beside // description.txt / source.txt, and `[LOST]` folders with no media. const f = (p: string, size = 10): SourceFile => ({ path: p, size }); const ARCHIVE: SourceFile[] = [ f("YouTube/README.txt"), f("YouTube/001 - Intro - (aaaaaaaaaaa)/Intro-aaaaaaaaaaa.mkv", 1000), f("YouTube/001 - Intro - (aaaaaaaaaaa)/Intro-aaaaaaaaaaa.info.json"), f("YouTube/001 - Intro - (aaaaaaaaaaa)/Intro-aaaaaaaaaaa.jpg"), f("YouTube/002 [LOST] - Gone - (bbbbbbbbbbb)/description.txt"), f("YouTube/002 [LOST] - Gone - (bbbbbbbbbbb)/source.txt"), f("YouTube/003 - Episode 2 - (ccccccccccc)/ccccccccccc.mp4", 2000), f("YouTube/003 - Episode 2 - (ccccccccccc)/description.txt"), f("YouTube/003 - Episode 2 - (ccccccccccc)/Sources.txt"), f("YouTube/004 - Text only - (ddddddddddd)/description.txt"), f("YouTube/005 - Two files - (eeeeeeeeeee)/a.mp4"), f("YouTube/005 - Two files - (eeeeeeeeeee)/b.webm"), f("YouTube/006 - Odd name - (fffffffffff)/Odd name [watch-fffffffffff-1].mp4", 3000), f("loose/Some title [ggggggggggg].webm", 4000), f("loose/no id here.mp4"), ]; test("ids: the folder's trailing (id), else the file's yt-dlp suffix", () => { assert.equal(idFromFolderName("001 - Intro - (aaaaaaaaaaa)"), "aaaaaaaaaaa"); assert.equal(idFromFolderName("Intro (too-short)"), null); assert.equal(idFromMediaName("x/Intro-aaaaaaaaaaa.mkv"), "aaaaaaaaaaa"); assert.equal(idFromMediaName("ccccccccccc.mp4"), "ccccccccccc"); assert.equal(idFromMediaName("Some title [ggggggggggg].webm"), "ggggggggggg"); assert.equal(idFromMediaName("no id here.mp4"), null); }); test("folders: grouped by parent, sidecars found, [LOST] marked", () => { const folders = groupArchiveFolders(ARCHIVE); const by = (id: string) => folders.find((x) => x.id === id)!; assert.equal(by("aaaaaaaaaaa").idFrom, "folder"); assert.equal(by("aaaaaaaaaaa").media.length, 1); assert.ok(by("aaaaaaaaaaa").infoJson); assert.equal(by("bbbbbbbbbbb").lost, true); assert.ok(by("ccccccccccc").descriptionTxt); assert.ok(by("ccccccccccc").sourceTxt, "Sources.txt, any case"); assert.equal(by("ggggggggggg").idFrom, "file"); // The archive's root holds no media and yields a folder with no id. assert.ok(folders.some((x) => x.folder === "YouTube" && x.id === null && x.media.length === 0)); // A folder with no id splits per media file. assert.ok(folders.some((x) => x.folder === "loose" && x.id === null && x.media.length === 1)); }); test("plan: every class, and held videos the archive has no media for", () => { const folders = groupArchiveFolders(ARCHIVE); const plan = planAttach({ folders, held: new Set(["aaaaaaaaaaa", "bbbbbbbbbbb", "fffffffffff", "hhhhhhhhhhh", "ddddddddddd"]), attached: new Set(["fffffffffff"]), }); const cls = Object.fromEntries(plan.rows.map((r) => [r.id ?? r.folder, r.class])); assert.equal(cls.aaaaaaaaaaa, "attach"); assert.equal(cls.bbbbbbbbbbb, "lost"); assert.equal(cls.ccccccccccc, "not-held"); assert.equal(cls.ddddddddddd, "no-media"); assert.equal(cls.eeeeeeeeeee, "ambiguous"); assert.equal(cls.fffffffffff, "already-attached"); assert.equal(cls.ggggggggggg, "not-held"); assert.equal(cls.loose, "unmatched"); assert.deepEqual(plan.heldWithoutMedia, ["bbbbbbbbbbb", "ddddddddddd", "hhhhhhhhhhh"]); const counts = countAttachRows(plan.rows); assert.equal(counts.attach, 1); assert.equal(counts["not-held"], 2); }); test("plan: createRecords turns not-held into create; replace re-attaches", () => { const folders = groupArchiveFolders(ARCHIVE); const plan = planAttach({ folders, held: new Set(["fffffffffff"]), attached: new Set(["fffffffffff"]), createRecords: true, replace: true, }); const row = (id: string) => plan.rows.find((r) => r.id === id)!; assert.equal(row("ccccccccccc").class, "create"); assert.ok(row("ccccccccccc").source?.descriptionTxt, "the folder rides along for the record"); assert.equal(row("fffffffffff").class, "attach"); assert.equal(row("fffffffffff").replacing, true); }); test("plan: two folders claiming one id are both ambiguous", () => { const folders = groupArchiveFolders([ f("a/001 - X - (aaaaaaaaaaa)/x.mp4"), f("b/001 - X again - (aaaaaaaaaaa)/y.mp4"), ]); const plan = planAttach({ folders, held: new Set(["aaaaaaaaaaa"]), attached: new Set() }); assert.deepEqual(plan.rows.map((r) => r.class), ["ambiguous", "ambiguous"]); assert.deepEqual(plan.heldWithoutMedia, [], "an ambiguous id is not 'no media'"); }); test("plan: match narrows the folders and drops the held-without-media list", () => { const plan = planAttach({ folders: groupArchiveFolders(ARCHIVE), held: new Set(["aaaaaaaaaaa", "ccccccccccc"]), attached: new Set(), match: /episode 2/i, }); assert.deepEqual(plan.rows.map((r) => [r.id, r.class]), [["ccccccccccc", "attach"]]); assert.equal(plan.heldWithoutMedia, null); }); test("plan: explicit items are the whole plan, by path", () => { const plan = planAttach({ folders: groupArchiveFolders(ARCHIVE), held: new Set(["zzzzzzzzzzz"]), attached: new Set(), files: ARCHIVE, items: [ { id: "zzzzzzzzzzz", path: "YouTube/005 - Two files - (eeeeeeeeeee)/b.webm" }, { id: "yyyyyyyyyyy", path: "nope.mp4" }, ], }); assert.deepEqual(plan.rows.map((r) => [r.id, r.class, r.media?.path ?? null]), [ ["zzzzzzzzzzz", "attach", "YouTube/005 - Two files - (eeeeeeeeeee)/b.webm"], ["yyyyyyyyyyy", "no-media", null], ]); }); test("records: title from the folder name, date and description from description.txt", () => { assert.equal(titleFromFolderName("006 - The Show Episode 002 - (zz1Yy2Xx3Ww)"), "The Show Episode 002"); assert.equal(titleFromFolderName("002 [LOST] - Gone - (bbbbbbbbbbb)"), "Gone"); assert.equal(titleFromFolderName("News, Weather & More! - (q_9Rr8Ss7Tt)"), "News, Weather & More!"); assert.equal(publishedDateFromText("Published on July 13th 2018\n\nTips"), "20180713"); assert.equal(publishedDateFromText("Published on March 1, 2019"), "20190301"); assert.equal(publishedDateFromText("no date"), null); assert.equal(descriptionFromText("Published on July 13th 2018\n\nTips & comments\nCategory: X"), "Tips & comments\nCategory: X"); const info = archiveFolderInfoJson({ id: "ccccccccccc", folderName: "003 - Episode 2 - (ccccccccccc)", descriptionText: "Published on July 13th 2018\n\nTips", platform: "youtube", durationSec: 61.234, }); assert.deepEqual(info, { id: "ccccccccccc", title: "Episode 2", description: "Tips", duration: 61.23, webpage_url: "https://www.youtube.com/watch?v=ccccccccccc", original_url: "https://www.youtube.com/watch?v=ccccccccccc", extractor: "youtube", extractor_key: "Youtube", upload_date: "20180713", }); const keys = Object.keys(info); assert.equal(keys[1], "title", "title near the head"); assert.equal(keys.at(-1), "upload_date", "upload_date last, in the tail"); // Another platform: no invented YouTube URL. const other = archiveFolderInfoJson({ id: "x", folderName: "x", platform: "rumble" }); assert.equal(other.webpage_url, undefined); assert.equal(other.extractor, undefined); });