import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { feedDateToTimestamp, feedDateToUploadDate, htmlToPlainText, parseItunesDuration, parseRssFeed, } from "./rssFeed"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test lib/rssFeed.test.ts const HERE = path.dirname(fileURLToPath(import.meta.url)); const FIXTURE = readFileSync( path.join(HERE, "__fixtures__", "demo-podcast.rss"), "utf8", ); test("the fixture feed: channel title, and every item in order", () => { const feed = parseRssFeed(FIXTURE); assert.equal(feed.title, "Demo & Friends Podcast"); assert.deepEqual( feed.items.map((i) => i.guid), ["guid-0001", "guid-0002", "https://example.com/?p=3", "guid-0004", "guid-trailer"], "a commented-out item is not an item, and a CDATA ends nothing", ); }); test("CDATA is verbatim; entities are decoded once outside it", () => { const [one, two] = parseRssFeed(FIXTURE).items; assert.equal(one.title, "Episode 1: Cats & "); assert.equal(two.title, "Two & a half ’quotes’ \"here\""); // An attribute's & is a URL's &. assert.equal( one.enclosureUrl, "https://cdn.example.com/audio/abc123.mp3?key=a&updated=1", ); }); test("the item's own wins over itunes:title, and a comment is no title", () => { const items = parseRssFeed(FIXTURE).items; assert.equal(items[1].title?.startsWith("Two"), true); assert.equal(items[2].title, "Episode three"); }); test("descriptions become plain text: HTML in CDATA, escaped HTML, itunes:summary", () => { const [one, two, three, four, trailer] = parseRssFeed(FIXTURE).items; assert.equal(one.description, "First line & more.\nSecond line\nthird"); assert.equal(two.description, "Hello & goodbye"); assert.equal(three.description, "Summary only."); assert.equal( four.description, "A CDATA body that mentions and ]] without ending.", "the CDATA's literal </item> is markup to the plain-text pass, not the end of the item", ); assert.equal(trailer.description, null); }); test("itunes tags, links, enclosures in either quote style", () => { const [one, two, three, four, trailer] = parseRssFeed(FIXTURE).items; assert.equal(one.durationSeconds, 3723); assert.equal(two.durationSeconds, 2730); assert.equal(three.durationSeconds, 3725); assert.equal(four.durationSeconds, null); assert.equal(one.link, "https://example.com/episodes/one"); assert.equal(two.link, null); assert.equal(three.enclosureUrl, "https://cdn.example.com/audio/ghi789.mp3"); assert.equal( two.enclosureUrl, "https://track.example.net/redirect.mp3/cdn.example.com/audio/def456.mp3?updated=2", ); assert.equal(trailer.enclosureUrl, null); assert.equal(one.pubDate, "Tue, 05 Mar 2024 10:00:00 GMT"); }); test("parseItunesDuration: seconds, MM:SS, HH:MM:SS; junk is null", () => { assert.equal(parseItunesDuration("3725"), 3725); assert.equal(parseItunesDuration("45:30"), 2730); assert.equal(parseItunesDuration("1:02:03"), 3723); assert.equal(parseItunesDuration(" 90.6 "), 91); assert.equal(parseItunesDuration("1 hour"), null); assert.equal(parseItunesDuration(""), null); assert.equal(parseItunesDuration(null), null); }); test("upload_date is the UTC day of the publication instant", () => { assert.equal(feedDateToUploadDate("Tue, 05 Mar 2024 10:00:00 GMT"), "20240305"); // 22:30 at -0500 is 03:30 the next day in UTC. assert.equal(feedDateToUploadDate("Mon, 04 Mar 2024 22:30:00 -0500"), "20240305"); assert.equal(feedDateToUploadDate("Mon, 04 Mar 2024 22:30:00 EST"), "20240305"); assert.equal(feedDateToUploadDate("2024-03-05T23:30:00+02:00"), "20240305"); // No zone at all reads as UTC, never as this machine's zone. assert.equal(feedDateToUploadDate("Tue, 05 Mar 2024 23:30:00"), "20240305"); assert.equal(feedDateToUploadDate("2024-03-05T23:30:00"), "20240305"); assert.equal(feedDateToUploadDate("2024-03-05"), "20240305"); assert.equal(feedDateToUploadDate("not a date"), null); assert.equal(feedDateToUploadDate(null), null); assert.equal(feedDateToTimestamp("Tue, 05 Mar 2024 10:00:00 GMT"), 1709632800); }); test("htmlToPlainText: list items, blank-line runs, numeric entities", () => { assert.equal( htmlToPlainText("<ul><li>one</li><li>two</li></ul><p></p><p></p><p>© x</p>"), "- one\n- two\n\n© x", ); assert.equal(htmlToPlainText("plain text"), "plain text"); }); test("a self-closing Atom-style link is read from its href; a BOM is ignored", () => { const feed = parseRssFeed( "<rss><channel><title>TA" + '', ); assert.equal(feed.title, "T"); assert.equal(feed.items[0].link, "https://example.com/a"); });