From da729e3848a404052daa996c1240fb01e7dd98a4 Mon Sep 17 00:00:00 2001 From: jsdavid278-cyber Date: Sat, 13 Jun 2026 15:52:00 -0600 Subject: [PATCH] Detect podcast RSS item enclosures --- .../feed-discovery/src/feed-discovery.test.ts | 10 +++++++++ plugins/feed-discovery/src/feed-parsing.ts | 21 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/plugins/feed-discovery/src/feed-discovery.test.ts b/plugins/feed-discovery/src/feed-discovery.test.ts index f2c4383..59db1eb 100644 --- a/plugins/feed-discovery/src/feed-discovery.test.ts +++ b/plugins/feed-discovery/src/feed-discovery.test.ts @@ -29,6 +29,16 @@ describe("feed parsing", () => { expect(result.sampleItems[0]).toMatchObject({ title: "Launch tiny products", url: "https://example.com/post" }); }); + it("detects podcast RSS feeds from item enclosures", () => { + const result = parseFeedDocument( + "https://example.com/podcast.xml", + `Audio Showhttps://example.comEpisode 1` + ); + + expect(result.ok).toBe(true); + expect(result.kind).toBe("podcast"); + }); + it("parses Atom and JSON Feed documents", () => { const atom = parseFeedDocument("https://example.com/atom.xml", `Atom FeedEntry2026-06-09T00:00:00Z`); const json = parseFeedDocument("https://example.com/feed.json", JSON.stringify({ version: "https://jsonfeed.org/version/1.1", title: "JSON Feed", items: [{ title: "Item", url: "https://example.com/item" }] }), "application/feed+json"); diff --git a/plugins/feed-discovery/src/feed-parsing.ts b/plugins/feed-discovery/src/feed-parsing.ts index 92fd4d7..88287e9 100644 --- a/plugins/feed-discovery/src/feed-parsing.ts +++ b/plugins/feed-discovery/src/feed-parsing.ts @@ -96,7 +96,7 @@ function parseRss(feedUrl: string, rss: Record): ValidationResu title: title || "Untitled RSS Feed", description: stringValue(channel.description), homepageUrl: linkValue(channel.link), - kind: detectRssKind(channel), + kind: detectRssKind(channel, items), language: stringValue(channel.language), imageUrl: imageValue(channel.image), lastPublishedAt: newestDate([stringValue(channel.lastBuildDate), stringValue(channel.pubDate), ...sampleItems.map((item) => item.publishedAt)]), @@ -132,13 +132,30 @@ function parseAtom(feedUrl: string, feed: Record): ValidationRe }; } -function detectRssKind(channel: Record): FeedKind { +function detectRssKind(channel: Record, items: Record[]): FeedKind { if (channel.itunes || channel["itunes:author"] || channel.enclosure) { return "podcast"; } + if (items.some(hasPodcastEnclosure)) { + return "podcast"; + } return "blog"; } +function hasPodcastEnclosure(item: Record) { + return asArray(item.enclosure) + .filter(isRecord) + .some((enclosure) => { + const type = stringValue(enclosure.type)?.toLowerCase(); + const url = stringValue(enclosure.url); + return ( + type?.startsWith("audio/") || + type?.startsWith("video/") || + /\.(mp3|m4a|mp4|m4v|ogg|oga|wav|aac|flac)(?:[?#].*)?$/i.test(url ?? "") + ); + }); +} + function imageValue(value: unknown) { if (isRecord(value)) { return stringValue(value.url);