feedkeeper 0.35.5 → 0.35.6
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/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { fetchFeed } from "./fetcher.js";
|
|
|
3
3
|
import { discoverIconUrls, iconCheckDue } from "./icon.js";
|
|
4
4
|
import { refreshFeedIcon } from "./feedIcon.js";
|
|
5
5
|
import { decodeEntities, plainTitle } from "./text.js";
|
|
6
|
+
import { unwrapRedirectLink } from "./redirectLinks.js";
|
|
6
7
|
import { enqueueNewItemNotifications, startPushOutbox, stopPushOutbox } from "../pushOutbox.js";
|
|
7
8
|
import { hasUserCapability } from "../auth/capabilities.js";
|
|
8
9
|
import { findFeedById, listSubscriberIds, updateFeedAfterPoll, updateFeedIcon, upsertItems } from "./repository.js";
|
|
@@ -123,8 +124,8 @@ export function itemGuid(item) {
|
|
|
123
124
|
/** The article address: its link, or its guid when the feed gives only that and the guid is a web address. */
|
|
124
125
|
export function itemLink(item) {
|
|
125
126
|
if (item.link)
|
|
126
|
-
return item.link;
|
|
127
|
-
return item.guid && /^https?:\/\//i.test(item.guid) ? item.guid : undefined;
|
|
127
|
+
return unwrapRedirectLink(item.link);
|
|
128
|
+
return item.guid && /^https?:\/\//i.test(item.guid) ? unwrapRedirectLink(item.guid) : undefined;
|
|
128
129
|
}
|
|
129
130
|
/** One attempt; the queue owns retries, admission, single-flight and the total deadline. */
|
|
130
131
|
async function pollAttempt(feed, signal) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Some feeds wrap every article address in a redirect through the publisher's tracker. Google Alerts links through
|
|
3
|
+
* `https://www.google.com/url?...&url=<article>`, which is not the page to read and fails the full-text fetch. The article address
|
|
4
|
+
* inside the wrapper is what an item should point to.
|
|
5
|
+
*/
|
|
6
|
+
const GOOGLE_HOST = /^(?:www\.)?google\.[a-z]{2,3}(?:\.[a-z]{2})?$/i;
|
|
7
|
+
export function unwrapRedirectLink(link) {
|
|
8
|
+
if (!link)
|
|
9
|
+
return link;
|
|
10
|
+
let url;
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(link);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return link;
|
|
16
|
+
}
|
|
17
|
+
if (!GOOGLE_HOST.test(url.hostname) || url.pathname !== "/url")
|
|
18
|
+
return link;
|
|
19
|
+
const target = url.searchParams.get("url") ?? url.searchParams.get("q");
|
|
20
|
+
return target && /^https?:\/\//i.test(target) ? target : link;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=redirectLinks.js.map
|
|
@@ -3,7 +3,8 @@ import { articleText } from "./articleContent.js";
|
|
|
3
3
|
import { LEAD_VERSION } from "./contentNoise.js";
|
|
4
4
|
import { largerHeroImage } from "./heroImage.js";
|
|
5
5
|
import { refreshGalleryClassification, refreshLead } from "./lead.js";
|
|
6
|
-
import { decodeEntities } from "./text.js";
|
|
6
|
+
import { decodeEntities, plainTitle } from "./text.js";
|
|
7
|
+
import { unwrapRedirectLink } from "./redirectLinks.js";
|
|
7
8
|
export function findFeedByUrl(url) {
|
|
8
9
|
return db.prepare("SELECT * FROM feeds WHERE url = ?").get(url);
|
|
9
10
|
}
|
|
@@ -570,19 +571,24 @@ export function moveSubscription(userId, fromFeedId, toFeedId) {
|
|
|
570
571
|
export function updateFeedIcon(feedId, iconUrl) {
|
|
571
572
|
db.prepare("UPDATE feeds SET icon_url = ?, icon_checked_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?").run(iconUrl, feedId);
|
|
572
573
|
}
|
|
573
|
-
/**
|
|
574
|
+
/**
|
|
575
|
+
* Fix item titles, snippets, links, feed titles and labels stored before entity decoding, title markup removal and redirect unwrapping
|
|
576
|
+
* existed; cheap once nothing matches.
|
|
577
|
+
*/
|
|
574
578
|
export function repairEncodedText() {
|
|
575
579
|
const rows = db
|
|
576
|
-
.prepare(
|
|
580
|
+
.prepare(`SELECT id, title, content_snippet, link FROM items
|
|
581
|
+
WHERE title LIKE '%&%;%' OR title LIKE '%<%>%' OR content_snippet LIKE '%&%;%' OR link LIKE 'https://www.google.%/url?%'`)
|
|
577
582
|
.all();
|
|
578
|
-
const update = db.prepare("UPDATE items SET title = ?, content_snippet = ? WHERE id = ?");
|
|
583
|
+
const update = db.prepare("UPDATE items SET title = ?, content_snippet = ?, link = ? WHERE id = ?");
|
|
579
584
|
let fixed = 0;
|
|
580
585
|
db.transaction(() => {
|
|
581
586
|
for (const row of rows) {
|
|
582
|
-
const title =
|
|
587
|
+
const title = row.title === null ? null : (plainTitle(row.title) ?? row.title);
|
|
583
588
|
const snippet = decodeEntities(row.content_snippet);
|
|
584
|
-
|
|
585
|
-
|
|
589
|
+
const link = unwrapRedirectLink(row.link);
|
|
590
|
+
if (title !== row.title || snippet !== row.content_snippet || link !== row.link) {
|
|
591
|
+
update.run(title, snippet ?? null, link ?? null, row.id);
|
|
586
592
|
refreshLead(row.id);
|
|
587
593
|
fixed++;
|
|
588
594
|
}
|
|
@@ -15,9 +15,14 @@ function decodeOnce(text) {
|
|
|
15
15
|
return NAMED_ENTITIES[name] ?? match;
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Inline formatting that some feeds put into a title, such as the `<b>` Google Alerts wraps around the search term. Only these tags are
|
|
20
|
+
* removed, so a title that really contains an angle bracket stays as it is.
|
|
21
|
+
*/
|
|
22
|
+
const TITLE_MARKUP = /<\/?(?:b|i|u|em|strong|mark|span|small|sub|sup|font|a|br)\b[^>]*>/gi;
|
|
23
|
+
/** A title as one line of text: entities decoded, inline markup removed, line breaks and runs of spaces collapsed to one space, trimmed. */
|
|
19
24
|
export function plainTitle(text) {
|
|
20
|
-
const cleaned = decodeEntities(text)?.replace(/\s+/g, " ").trim();
|
|
25
|
+
const cleaned = decodeEntities(text)?.replace(TITLE_MARKUP, tag => (/^<br/i.test(tag) ? " " : "")).replace(/\s+/g, " ").trim();
|
|
21
26
|
return cleaned || undefined;
|
|
22
27
|
}
|
|
23
28
|
export function decodeEntities(text) {
|