Skip to content

Commit aa3fdba

Browse files
Detect podcast RSS item enclosures (#50)
1 parent be530ab commit aa3fdba

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

plugins/feed-discovery/src/feed-discovery.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ describe("feed parsing", () => {
2929
expect(result.sampleItems[0]).toMatchObject({ title: "Launch tiny products", url: "https://example.com/post" });
3030
});
3131

32+
it("detects podcast RSS feeds from item enclosures", () => {
33+
const result = parseFeedDocument(
34+
"https://example.com/podcast.xml",
35+
`<?xml version="1.0"?><rss version="2.0"><channel><title>Audio Show</title><link>https://example.com</link><item><title>Episode 1</title><enclosure url="https://example.com/episode-1.mp3" type="audio/mpeg" length="12345" /></item></channel></rss>`
36+
);
37+
38+
expect(result.ok).toBe(true);
39+
expect(result.kind).toBe("podcast");
40+
});
41+
3242
it("parses Atom and JSON Feed documents", () => {
3343
const atom = parseFeedDocument("https://example.com/atom.xml", `<feed><title>Atom Feed</title><entry><title>Entry</title><updated>2026-06-09T00:00:00Z</updated></entry></feed>`);
3444
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");

plugins/feed-discovery/src/feed-parsing.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function parseRss(feedUrl: string, rss: Record<string, unknown>): ValidationResu
9696
title: title || "Untitled RSS Feed",
9797
description: stringValue(channel.description),
9898
homepageUrl: linkValue(channel.link),
99-
kind: detectRssKind(channel),
99+
kind: detectRssKind(channel, items),
100100
language: stringValue(channel.language),
101101
imageUrl: imageValue(channel.image),
102102
lastPublishedAt: newestDate([stringValue(channel.lastBuildDate), stringValue(channel.pubDate), ...sampleItems.map((item) => item.publishedAt)]),
@@ -132,13 +132,30 @@ function parseAtom(feedUrl: string, feed: Record<string, unknown>): ValidationRe
132132
};
133133
}
134134

135-
function detectRssKind(channel: Record<string, unknown>): FeedKind {
135+
function detectRssKind(channel: Record<string, unknown>, items: Record<string, unknown>[]): FeedKind {
136136
if (channel.itunes || channel["itunes:author"] || channel.enclosure) {
137137
return "podcast";
138138
}
139+
if (items.some(hasPodcastEnclosure)) {
140+
return "podcast";
141+
}
139142
return "blog";
140143
}
141144

145+
function hasPodcastEnclosure(item: Record<string, unknown>) {
146+
return asArray(item.enclosure)
147+
.filter(isRecord)
148+
.some((enclosure) => {
149+
const type = stringValue(enclosure.type)?.toLowerCase();
150+
const url = stringValue(enclosure.url);
151+
return (
152+
type?.startsWith("audio/") ||
153+
type?.startsWith("video/") ||
154+
/\.(mp3|m4a|mp4|m4v|ogg|oga|wav|aac|flac)(?:[?#].*)?$/i.test(url ?? "")
155+
);
156+
});
157+
}
158+
142159
function imageValue(value: unknown) {
143160
if (isRecord(value)) {
144161
return stringValue(value.url);

0 commit comments

Comments
 (0)