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.com- Episode 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);