@rmdes/indiekit-endpoint-microsub 1.0.56 → 1.0.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/reader.js +408 -0
- package/index.js +37 -36
- package/lib/cache/redis.js +12 -3
- package/lib/controllers/reader/actor.js +142 -0
- package/lib/controllers/reader/channel.js +301 -0
- package/lib/controllers/reader/compose.js +242 -0
- package/lib/controllers/reader/deck.js +129 -0
- package/lib/controllers/reader/feed-repair.js +117 -0
- package/lib/controllers/reader/feed.js +246 -0
- package/lib/controllers/reader/index.js +126 -0
- package/lib/controllers/reader/search.js +157 -0
- package/lib/controllers/reader/timeline.js +250 -0
- package/lib/controllers/timeline.js +4 -2
- package/lib/feeds/atom.js +1 -1
- package/lib/feeds/fetcher.js +1 -30
- package/lib/feeds/hfeed.js +1 -1
- package/lib/feeds/jsonfeed.js +1 -1
- package/lib/feeds/normalizer-hfeed.js +209 -0
- package/lib/feeds/normalizer-jsonfeed.js +171 -0
- package/lib/feeds/normalizer-rss.js +178 -0
- package/lib/feeds/normalizer.js +20 -560
- package/lib/feeds/rss.js +1 -1
- package/lib/polling/processor.js +3 -17
- package/lib/storage/items-read-state.js +287 -0
- package/lib/storage/items-retention.js +174 -0
- package/lib/storage/items-search.js +34 -0
- package/lib/storage/items.js +99 -590
- package/lib/storage/read-state.js +1 -1
- package/lib/utils/async-handler.js +7 -0
- package/lib/utils/html.js +25 -0
- package/lib/utils/source-type.js +28 -0
- package/lib/webmention/processor.js +1 -1
- package/locales/de.json +3 -0
- package/locales/en.json +2 -0
- package/locales/es-419.json +3 -0
- package/locales/es.json +3 -0
- package/locales/fr.json +3 -0
- package/locales/hi.json +3 -0
- package/locales/id.json +3 -0
- package/locales/it.json +3 -0
- package/locales/nl.json +3 -0
- package/locales/pl.json +3 -0
- package/locales/pt-BR.json +3 -0
- package/locales/pt.json +3 -0
- package/locales/sr.json +3 -0
- package/locales/sv.json +3 -0
- package/locales/zh-Hans-CN.json +3 -0
- package/package.json +1 -1
- package/views/channel.njk +1 -348
- package/views/timeline.njk +3 -274
- package/lib/controllers/reader.js +0 -1562
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* h-feed (Microformats2) normalization
|
|
3
|
+
* @module feeds/normalizer-hfeed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
generateItemUid,
|
|
8
|
+
extractImagesFromHtml,
|
|
9
|
+
extractPhotoUrl,
|
|
10
|
+
normalizeUrlArray,
|
|
11
|
+
getFirst,
|
|
12
|
+
getContentText,
|
|
13
|
+
sanitizeHtml,
|
|
14
|
+
SANITIZE_OPTIONS,
|
|
15
|
+
} from "./normalizer.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize h-card author
|
|
19
|
+
* @param {object|string} hcard - h-card or author name string
|
|
20
|
+
* @returns {object} Normalized author object
|
|
21
|
+
*/
|
|
22
|
+
function normalizeHcard(hcard) {
|
|
23
|
+
if (typeof hcard === "string") {
|
|
24
|
+
return { type: "card", name: hcard };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!hcard || !hcard.properties) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const properties = hcard.properties;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
type: "card",
|
|
35
|
+
name: getFirst(properties.name),
|
|
36
|
+
url: getFirst(properties.url),
|
|
37
|
+
photo: extractPhotoUrl(getFirst(properties.photo)),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Normalize h-feed entry
|
|
43
|
+
* @param {object} entry - Microformats h-entry
|
|
44
|
+
* @param {string} feedUrl - Feed URL
|
|
45
|
+
* @returns {object} Normalized jf2 item
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeHfeedItem(entry, feedUrl) {
|
|
48
|
+
const properties = entry.properties || {};
|
|
49
|
+
const url = getFirst(properties.url) || getFirst(properties.uid);
|
|
50
|
+
const uid = generateItemUid(feedUrl, getFirst(properties.uid) || url);
|
|
51
|
+
|
|
52
|
+
const normalized = {
|
|
53
|
+
type: "entry",
|
|
54
|
+
uid,
|
|
55
|
+
url,
|
|
56
|
+
_source: {
|
|
57
|
+
url: feedUrl,
|
|
58
|
+
feedUrl,
|
|
59
|
+
feedType: "hfeed",
|
|
60
|
+
originalId: getFirst(properties.uid),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Name/title
|
|
65
|
+
if (properties.name) {
|
|
66
|
+
const name = getFirst(properties.name);
|
|
67
|
+
// Only include name if it's not just the content
|
|
68
|
+
if (
|
|
69
|
+
name &&
|
|
70
|
+
(!properties.content || name !== getContentText(properties.content))
|
|
71
|
+
) {
|
|
72
|
+
normalized.name = name;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Published
|
|
77
|
+
if (properties.published) {
|
|
78
|
+
const published = getFirst(properties.published);
|
|
79
|
+
normalized.published = new Date(published).toISOString();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Updated
|
|
83
|
+
if (properties.updated) {
|
|
84
|
+
const updated = getFirst(properties.updated);
|
|
85
|
+
normalized.updated = new Date(updated).toISOString();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Content
|
|
89
|
+
if (properties.content) {
|
|
90
|
+
const content = getFirst(properties.content);
|
|
91
|
+
if (typeof content === "object") {
|
|
92
|
+
normalized.content = {
|
|
93
|
+
html: content.html
|
|
94
|
+
? sanitizeHtml(content.html, SANITIZE_OPTIONS)
|
|
95
|
+
: undefined,
|
|
96
|
+
text: content.value || undefined,
|
|
97
|
+
};
|
|
98
|
+
} else if (typeof content === "string") {
|
|
99
|
+
normalized.content = { text: content };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Summary
|
|
104
|
+
if (properties.summary) {
|
|
105
|
+
normalized.summary = getFirst(properties.summary);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Author
|
|
109
|
+
if (properties.author) {
|
|
110
|
+
const author = getFirst(properties.author);
|
|
111
|
+
normalized.author = normalizeHcard(author);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Categories
|
|
115
|
+
if (properties.category) {
|
|
116
|
+
normalized.category = properties.category;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Photos
|
|
120
|
+
if (properties.photo) {
|
|
121
|
+
normalized.photo = properties.photo.map((p) =>
|
|
122
|
+
typeof p === "object" ? p.value || p.url : p,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Videos
|
|
127
|
+
if (properties.video) {
|
|
128
|
+
normalized.video = properties.video.map((v) =>
|
|
129
|
+
typeof v === "object" ? v.value || v.url : v,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Audio
|
|
134
|
+
if (properties.audio) {
|
|
135
|
+
normalized.audio = properties.audio.map((a) =>
|
|
136
|
+
typeof a === "object" ? a.value || a.url : a,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Interaction types - normalize to string URLs
|
|
141
|
+
if (properties["like-of"]) {
|
|
142
|
+
normalized["like-of"] = normalizeUrlArray(properties["like-of"]);
|
|
143
|
+
}
|
|
144
|
+
if (properties["repost-of"]) {
|
|
145
|
+
normalized["repost-of"] = normalizeUrlArray(properties["repost-of"]);
|
|
146
|
+
}
|
|
147
|
+
if (properties["bookmark-of"]) {
|
|
148
|
+
normalized["bookmark-of"] = normalizeUrlArray(properties["bookmark-of"]);
|
|
149
|
+
}
|
|
150
|
+
if (properties["in-reply-to"]) {
|
|
151
|
+
normalized["in-reply-to"] = normalizeUrlArray(properties["in-reply-to"]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// RSVP
|
|
155
|
+
if (properties.rsvp) {
|
|
156
|
+
normalized.rsvp = getFirst(properties.rsvp);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Syndication
|
|
160
|
+
if (properties.syndication) {
|
|
161
|
+
normalized.syndication = properties.syndication;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Extract images from HTML content as fallback
|
|
165
|
+
if (!normalized.photo && normalized.content?.html) {
|
|
166
|
+
const extracted = extractImagesFromHtml(normalized.content.html);
|
|
167
|
+
if (extracted.length > 0) {
|
|
168
|
+
normalized.photo = extracted;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Normalize h-feed metadata
|
|
177
|
+
* @param {object} hfeed - h-feed microformat object
|
|
178
|
+
* @param {string} feedUrl - Feed URL
|
|
179
|
+
* @returns {object} Normalized feed metadata
|
|
180
|
+
*/
|
|
181
|
+
export function normalizeHfeedMeta(hfeed, feedUrl) {
|
|
182
|
+
const properties = hfeed.properties || {};
|
|
183
|
+
|
|
184
|
+
const normalized = {
|
|
185
|
+
name: getFirst(properties.name) || feedUrl,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (properties.summary) {
|
|
189
|
+
normalized.summary = getFirst(properties.summary);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (properties.url) {
|
|
193
|
+
normalized.url = getFirst(properties.url);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (properties.photo) {
|
|
197
|
+
normalized.photo = getFirst(properties.photo);
|
|
198
|
+
if (typeof normalized.photo === "object") {
|
|
199
|
+
normalized.photo = normalized.photo.value || normalized.photo.url;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (properties.author) {
|
|
204
|
+
const author = getFirst(properties.author);
|
|
205
|
+
normalized.author = normalizeHcard(author);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return normalized;
|
|
209
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Feed normalization
|
|
3
|
+
* @module feeds/normalizer-jsonfeed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
generateItemUid,
|
|
8
|
+
extractImagesFromHtml,
|
|
9
|
+
sanitizeHtml,
|
|
10
|
+
SANITIZE_OPTIONS,
|
|
11
|
+
} from "./normalizer.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalize JSON Feed item
|
|
15
|
+
* @param {object} item - JSON Feed item
|
|
16
|
+
* @param {string} feedUrl - Feed URL
|
|
17
|
+
* @returns {object} Normalized jf2 item
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeJsonFeedItem(item, feedUrl) {
|
|
20
|
+
const url = item.url || item.external_url;
|
|
21
|
+
const uid = generateItemUid(feedUrl, item.id || url);
|
|
22
|
+
|
|
23
|
+
const normalized = {
|
|
24
|
+
type: "entry",
|
|
25
|
+
uid,
|
|
26
|
+
url,
|
|
27
|
+
name: item.title
|
|
28
|
+
? sanitizeHtml(item.title, { allowedTags: [] }).trim()
|
|
29
|
+
: undefined,
|
|
30
|
+
published: item.date_published
|
|
31
|
+
? new Date(item.date_published).toISOString()
|
|
32
|
+
: undefined,
|
|
33
|
+
updated: item.date_modified
|
|
34
|
+
? new Date(item.date_modified).toISOString()
|
|
35
|
+
: undefined,
|
|
36
|
+
_source: {
|
|
37
|
+
url: feedUrl,
|
|
38
|
+
feedUrl,
|
|
39
|
+
feedType: "jsonfeed",
|
|
40
|
+
originalId: item.id,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Content
|
|
45
|
+
if (item.content_html || item.content_text) {
|
|
46
|
+
normalized.content = {};
|
|
47
|
+
if (item.content_html) {
|
|
48
|
+
normalized.content.html = sanitizeHtml(
|
|
49
|
+
item.content_html,
|
|
50
|
+
SANITIZE_OPTIONS,
|
|
51
|
+
);
|
|
52
|
+
normalized.content.text = sanitizeHtml(item.content_html, {
|
|
53
|
+
allowedTags: [],
|
|
54
|
+
}).trim();
|
|
55
|
+
} else if (item.content_text) {
|
|
56
|
+
normalized.content.text = item.content_text;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Summary
|
|
61
|
+
if (item.summary) {
|
|
62
|
+
normalized.summary = item.summary;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Author
|
|
66
|
+
if (item.author || item.authors) {
|
|
67
|
+
const author = item.author || (item.authors && item.authors[0]);
|
|
68
|
+
if (author) {
|
|
69
|
+
normalized.author = {
|
|
70
|
+
type: "card",
|
|
71
|
+
name: author.name,
|
|
72
|
+
url: author.url,
|
|
73
|
+
photo: author.avatar,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Tags
|
|
79
|
+
if (item.tags && item.tags.length > 0) {
|
|
80
|
+
normalized.category = item.tags;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Featured image
|
|
84
|
+
if (item.image) {
|
|
85
|
+
normalized.photo = [item.image];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (item.banner_image && !normalized.photo) {
|
|
89
|
+
normalized.photo = [item.banner_image];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Attachments
|
|
93
|
+
if (item.attachments && item.attachments.length > 0) {
|
|
94
|
+
for (const attachment of item.attachments) {
|
|
95
|
+
const mediaType = attachment.mime_type || "";
|
|
96
|
+
|
|
97
|
+
if (mediaType.startsWith("image/")) {
|
|
98
|
+
normalized.photo = normalized.photo || [];
|
|
99
|
+
normalized.photo.push(attachment.url);
|
|
100
|
+
} else if (mediaType.startsWith("video/")) {
|
|
101
|
+
normalized.video = normalized.video || [];
|
|
102
|
+
normalized.video.push(attachment.url);
|
|
103
|
+
} else if (mediaType.startsWith("audio/")) {
|
|
104
|
+
normalized.audio = normalized.audio || [];
|
|
105
|
+
normalized.audio.push(attachment.url);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// External URL
|
|
111
|
+
if (item.external_url && item.url !== item.external_url) {
|
|
112
|
+
normalized["bookmark-of"] = [item.external_url];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Extract images from HTML content as fallback
|
|
116
|
+
if (!normalized.photo && normalized.content?.html) {
|
|
117
|
+
const extracted = extractImagesFromHtml(normalized.content.html);
|
|
118
|
+
if (extracted.length > 0) {
|
|
119
|
+
normalized.photo = extracted;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return normalized;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Normalize JSON Feed metadata
|
|
128
|
+
* @param {object} feed - JSON Feed object
|
|
129
|
+
* @param {string} feedUrl - Feed URL
|
|
130
|
+
* @returns {object} Normalized feed metadata
|
|
131
|
+
*/
|
|
132
|
+
export function normalizeJsonFeedMeta(feed, feedUrl) {
|
|
133
|
+
const normalized = {
|
|
134
|
+
name: feed.title
|
|
135
|
+
? sanitizeHtml(feed.title, { allowedTags: [] }).trim()
|
|
136
|
+
: feedUrl,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (feed.description) {
|
|
140
|
+
normalized.summary = feed.description;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (feed.home_page_url) {
|
|
144
|
+
normalized.url = feed.home_page_url;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (feed.icon) {
|
|
148
|
+
normalized.photo = feed.icon;
|
|
149
|
+
} else if (feed.favicon) {
|
|
150
|
+
normalized.photo = feed.favicon;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (feed.author || feed.authors) {
|
|
154
|
+
const author = feed.author || (feed.authors && feed.authors[0]);
|
|
155
|
+
if (author) {
|
|
156
|
+
normalized.author = {
|
|
157
|
+
type: "card",
|
|
158
|
+
name: author.name,
|
|
159
|
+
url: author.url,
|
|
160
|
+
photo: author.avatar,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Hub for WebSub
|
|
166
|
+
if (feed.hubs && feed.hubs.length > 0) {
|
|
167
|
+
normalized._hub = feed.hubs[0].url;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return normalized;
|
|
171
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSS/Atom feed normalization
|
|
3
|
+
* @module feeds/normalizer-rss
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
generateItemUid,
|
|
8
|
+
toISOStringSafe,
|
|
9
|
+
extractImagesFromHtml,
|
|
10
|
+
sanitizeHtml,
|
|
11
|
+
SANITIZE_OPTIONS,
|
|
12
|
+
} from "./normalizer.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalize RSS/Atom item from feedparser
|
|
16
|
+
* @param {object} item - Feedparser item
|
|
17
|
+
* @param {string} feedUrl - Feed URL
|
|
18
|
+
* @param {string} feedType - 'rss' or 'atom'
|
|
19
|
+
* @returns {object} Normalized jf2 item
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeItem(item, feedUrl, feedType) {
|
|
22
|
+
const url = item.link || item.origlink || item.guid;
|
|
23
|
+
const uid = generateItemUid(feedUrl, item.guid || url || item.title);
|
|
24
|
+
|
|
25
|
+
const normalized = {
|
|
26
|
+
type: "entry",
|
|
27
|
+
uid,
|
|
28
|
+
url,
|
|
29
|
+
name: item.title
|
|
30
|
+
? sanitizeHtml(item.title, { allowedTags: [] }).trim()
|
|
31
|
+
: undefined,
|
|
32
|
+
published: toISOStringSafe(item.pubdate),
|
|
33
|
+
updated: toISOStringSafe(item.date),
|
|
34
|
+
_source: {
|
|
35
|
+
url: feedUrl,
|
|
36
|
+
feedUrl,
|
|
37
|
+
feedType,
|
|
38
|
+
originalId: item.guid,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Content
|
|
43
|
+
if (item.description || item.summary) {
|
|
44
|
+
const html = item.description || item.summary;
|
|
45
|
+
normalized.content = {
|
|
46
|
+
html: sanitizeHtml(html, SANITIZE_OPTIONS),
|
|
47
|
+
text: sanitizeHtml(html, { allowedTags: [] }).trim(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Summary (prefer explicit summary over truncated content)
|
|
52
|
+
if (item.summary && item.description && item.summary !== item.description) {
|
|
53
|
+
normalized.summary = sanitizeHtml(item.summary, { allowedTags: [] }).trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Author
|
|
57
|
+
if (item.author || item["dc:creator"]) {
|
|
58
|
+
const authorName = item.author || item["dc:creator"];
|
|
59
|
+
normalized.author = {
|
|
60
|
+
type: "card",
|
|
61
|
+
name: authorName,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Categories/tags
|
|
66
|
+
if (item.categories && item.categories.length > 0) {
|
|
67
|
+
normalized.category = item.categories;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Enclosures (media)
|
|
71
|
+
if (item.enclosures && item.enclosures.length > 0) {
|
|
72
|
+
for (const enclosure of item.enclosures) {
|
|
73
|
+
const mediaUrl = enclosure.url;
|
|
74
|
+
const mediaType = enclosure.type || "";
|
|
75
|
+
|
|
76
|
+
if (mediaType.startsWith("image/")) {
|
|
77
|
+
normalized.photo = normalized.photo || [];
|
|
78
|
+
normalized.photo.push(mediaUrl);
|
|
79
|
+
} else if (mediaType.startsWith("video/")) {
|
|
80
|
+
normalized.video = normalized.video || [];
|
|
81
|
+
normalized.video.push(mediaUrl);
|
|
82
|
+
} else if (mediaType.startsWith("audio/")) {
|
|
83
|
+
normalized.audio = normalized.audio || [];
|
|
84
|
+
normalized.audio.push(mediaUrl);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Featured image from media content
|
|
90
|
+
if (item["media:content"] && item["media:content"].url) {
|
|
91
|
+
const mediaType = item["media:content"].type || "";
|
|
92
|
+
if (
|
|
93
|
+
mediaType.startsWith("image/") ||
|
|
94
|
+
item["media:content"].medium === "image"
|
|
95
|
+
) {
|
|
96
|
+
normalized.photo = normalized.photo || [];
|
|
97
|
+
if (!normalized.photo.includes(item["media:content"].url)) {
|
|
98
|
+
normalized.photo.push(item["media:content"].url);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Image from item.image
|
|
104
|
+
if (item.image && item.image.url) {
|
|
105
|
+
normalized.photo = normalized.photo || [];
|
|
106
|
+
if (!normalized.photo.includes(item.image.url)) {
|
|
107
|
+
normalized.photo.push(item.image.url);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Extract images from HTML content as fallback
|
|
112
|
+
if (!normalized.photo && normalized.content?.html) {
|
|
113
|
+
const extracted = extractImagesFromHtml(normalized.content.html);
|
|
114
|
+
if (extracted.length > 0) {
|
|
115
|
+
normalized.photo = extracted;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Normalize feed metadata from feedparser
|
|
124
|
+
* @param {object} meta - Feedparser meta object
|
|
125
|
+
* @param {string} feedUrl - Feed URL
|
|
126
|
+
* @returns {object} Normalized feed metadata
|
|
127
|
+
*/
|
|
128
|
+
export function normalizeFeedMeta(meta, feedUrl) {
|
|
129
|
+
const normalized = {
|
|
130
|
+
name: meta.title
|
|
131
|
+
? sanitizeHtml(meta.title, { allowedTags: [] }).trim()
|
|
132
|
+
: feedUrl,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (meta.description) {
|
|
136
|
+
normalized.summary = meta.description;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (meta.link) {
|
|
140
|
+
normalized.url = meta.link;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (meta.image && meta.image.url) {
|
|
144
|
+
normalized.photo = meta.image.url;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (meta.favicon) {
|
|
148
|
+
normalized.photo = normalized.photo || meta.favicon;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Author/publisher
|
|
152
|
+
if (meta.author) {
|
|
153
|
+
normalized.author = {
|
|
154
|
+
type: "card",
|
|
155
|
+
name: meta.author,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Hub for WebSub
|
|
160
|
+
if (meta.cloud && meta.cloud.href) {
|
|
161
|
+
normalized._hub = meta.cloud.href;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Look for hub in links
|
|
165
|
+
if (meta.link && meta["atom:link"]) {
|
|
166
|
+
const links = Array.isArray(meta["atom:link"])
|
|
167
|
+
? meta["atom:link"]
|
|
168
|
+
: [meta["atom:link"]];
|
|
169
|
+
for (const link of links) {
|
|
170
|
+
if (link["@"] && link["@"].rel === "hub") {
|
|
171
|
+
normalized._hub = link["@"].href;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return normalized;
|
|
178
|
+
}
|