Skip to content

Commit 6b1a628

Browse files
committed
feat: add support for atom feeds
1 parent 2dec337 commit 6b1a628

File tree

2 files changed

+125
-38
lines changed

2 files changed

+125
-38
lines changed

src/lib/api/fetchRSS.ts

Lines changed: 95 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { parseString } from "xml2js"
22

3-
import type { RSSChannel, RSSItem, RSSResult } from "../types"
3+
import type { AtomResult, RSSChannel, RSSItem, RSSResult } from "../types"
44
import { isValidDate } from "../utils/date"
55

66
/**
@@ -12,45 +12,102 @@ export const fetchRSS = async (xmlUrl: string | string[]) => {
1212
const urls = Array.isArray(xmlUrl) ? xmlUrl : [xmlUrl]
1313
const allItems: RSSItem[][] = []
1414
for (const url of urls) {
15-
const rssItems = (await fetchXml(url)) as RSSResult
16-
if (!rssItems.rss) continue
17-
const [mainChannel] = rssItems.rss.channel as RSSChannel[]
18-
const [source] = mainChannel.title
19-
const [sourceUrl] = mainChannel.link
20-
const channelImage = mainChannel.image ? mainChannel.image[0].url[0] : ""
15+
const response = (await fetchXml(url)) as RSSResult | AtomResult
16+
if ("rss" in response) {
17+
const [mainChannel] = response.rss.channel as RSSChannel[]
18+
const [source] = mainChannel.title
19+
const [sourceUrl] = mainChannel.link
20+
const channelImage = mainChannel.image ? mainChannel.image[0].url[0] : ""
2121

22-
const parsedRssItems = mainChannel.item
23-
// Filter out items with invalid dates
24-
.filter((item) => {
25-
if (!item.pubDate) return false
26-
const [pubDate] = item.pubDate
27-
return isValidDate(pubDate)
28-
})
29-
// Sort by pubDate (most recent is first in array
30-
.sort((a, b) => {
31-
const dateA = new Date(a.pubDate[0])
32-
const dateB = new Date(b.pubDate[0])
33-
return dateB.getTime() - dateA.getTime()
34-
})
35-
// Map to RSSItem object
36-
.map((item) => {
37-
const getImgSrc = () => {
38-
if (item.enclosure) return item.enclosure[0].$.url
39-
if (item["media:content"]) return item["media:content"][0].$.url
40-
return channelImage
41-
}
42-
return {
43-
pubDate: item.pubDate[0],
44-
title: item.title[0],
45-
link: item.link[0],
46-
imgSrc: getImgSrc(),
47-
source,
48-
sourceUrl,
49-
sourceFeedUrl: url,
50-
} as RSSItem
51-
})
22+
const parsedRssItems = mainChannel.item
23+
// Filter out items with invalid dates
24+
.filter((item) => {
25+
if (!item.pubDate) return false
26+
const [pubDate] = item.pubDate
27+
return isValidDate(pubDate)
28+
})
29+
// Sort by pubDate (most recent is first in array
30+
.sort((a, b) => {
31+
const dateA = new Date(a.pubDate[0])
32+
const dateB = new Date(b.pubDate[0])
33+
return dateB.getTime() - dateA.getTime()
34+
})
35+
// Map to RSSItem object
36+
.map((item) => {
37+
const getImgSrc = () => {
38+
if (item.enclosure) return item.enclosure[0].$.url
39+
if (item["media:content"]) return item["media:content"][0].$.url
40+
return channelImage
41+
}
42+
return {
43+
pubDate: item.pubDate[0],
44+
title: item.title[0],
45+
link: item.link[0],
46+
imgSrc: getImgSrc(),
47+
source,
48+
sourceUrl,
49+
sourceFeedUrl: url,
50+
} as RSSItem
51+
})
5252

53-
allItems.push(parsedRssItems)
53+
allItems.push(parsedRssItems)
54+
} else if ("feed" in response) {
55+
const [source] = response.feed.title
56+
const [sourceUrl] = response.feed.id
57+
const feedImage = response.feed.icon?.[0]
58+
59+
const parsedAtomItems = response.feed.entry
60+
// Filter out items with invalid dates
61+
.filter((entry) => {
62+
if (!entry.updated) return false
63+
const [published] = entry.updated
64+
return isValidDate(published)
65+
})
66+
// Sort by published (most recent is first in array
67+
.sort((a, b) => {
68+
const dateA = new Date(a.updated[0])
69+
const dateB = new Date(b.updated[0])
70+
return dateB.getTime() - dateA.getTime()
71+
})
72+
// Map to RSSItem object
73+
.map((entry) => {
74+
const getImgSrc = (): string => {
75+
const imgRegEx = /https?:\/\/[^"]*?\.(jpe?g|png|webp)/g
76+
const content = entry.content?.[0]?._ || ""
77+
const summary = entry.summary?.[0]?._ || ""
78+
const contentMatch = content.match(imgRegEx)
79+
const summaryMatch = summary.match(imgRegEx)
80+
if (contentMatch) return contentMatch[0]
81+
if (summaryMatch) return summaryMatch[0]
82+
return feedImage || ""
83+
}
84+
const getLink = (): string => {
85+
if (!entry.link) {
86+
console.warn(`No link found for RSS url: ${url}`)
87+
return ""
88+
}
89+
const link = entry.link[0]
90+
if (typeof link === "string") return link
91+
return link.$.href || ""
92+
}
93+
const getTitle = (): string => {
94+
const title = entry.title[0]
95+
if (typeof title === "string") return title
96+
return title._ || ""
97+
}
98+
return {
99+
pubDate: entry.updated[0],
100+
title: getTitle(),
101+
link: getLink(),
102+
imgSrc: getImgSrc(),
103+
source,
104+
sourceUrl,
105+
sourceFeedUrl: url,
106+
} as RSSItem
107+
})
108+
109+
allItems.push(parsedAtomItems)
110+
}
54111
}
55112
return allItems as RSSItem[][]
56113
}

src/lib/types.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,36 @@ export type RSSResult = {
803803
}
804804
}
805805

806+
export type AtomItem = {
807+
_?: string
808+
$: {
809+
type?: string
810+
href?: string
811+
rel?: string
812+
}
813+
}
814+
export type AtomEntry = {
815+
id: string[]
816+
title: (AtomItem | string)[]
817+
updated: string[]
818+
content?: AtomItem[]
819+
link?: (AtomItem | string)[]
820+
summary?: AtomItem[]
821+
}
822+
823+
export type AtomResult = {
824+
feed: {
825+
id: string[]
826+
title: string[]
827+
updated: string[]
828+
generator: string[]
829+
link: string[]
830+
subtitle: string[]
831+
icon?: string[]
832+
entry: AtomEntry[]
833+
}
834+
}
835+
806836
export type CommunityBlog = {
807837
href: string
808838
} & ({ name: string; feed?: string } | { name?: string; feed: string })

0 commit comments

Comments
 (0)