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 -
- ()` holding either yt-dlp's files
// (`-.` + `.info.json`) or a bare `.` 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);
});