feedkeeper 0.35.1 → 0.35.3
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 +1 -1
- package/server/dist/backup-cli.js +10 -2
- package/server/dist/feeds/content/patterns/boilerplate/index.js +3 -2
- package/server/dist/feeds/content/patterns/boilerplate/ja.js +19 -0
- package/server/dist/feeds/content/patterns/consent/index.js +2 -1
- package/server/dist/feeds/content/patterns/consent/ja.js +14 -0
- package/server/dist/feeds/content/patterns/loading/index.js +2 -1
- package/server/dist/feeds/content/patterns/loading/ja.js +8 -0
- package/server/dist/feeds/content/patterns/play-labels/index.js +2 -1
- package/server/dist/feeds/content/patterns/play-labels/ja.js +3 -0
- package/server/dist/feeds/content/patterns/sidebars/index.js +2 -1
- package/server/dist/feeds/content/patterns/sidebars/ja.js +7 -0
- package/server/dist/feeds/contentNoise.js +47 -4
- package/server/package.json +1 -1
package/package.json
CHANGED
|
@@ -31,7 +31,11 @@ async function main() {
|
|
|
31
31
|
process.umask(0o077);
|
|
32
32
|
const { db } = await import("./db/index.js");
|
|
33
33
|
try {
|
|
34
|
-
await db.backup(filePath
|
|
34
|
+
await db.backup(filePath, {
|
|
35
|
+
progress({ remainingPages }) {
|
|
36
|
+
return remainingPages;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
35
39
|
const saved = new Database(filePath, { readonly: true, fileMustExist: true });
|
|
36
40
|
try {
|
|
37
41
|
assertHealthy(saved);
|
|
@@ -72,7 +76,11 @@ async function main() {
|
|
|
72
76
|
assertHealthy(source);
|
|
73
77
|
mkdirSync(dirname(databasePath), { recursive: true, mode: 0o700 });
|
|
74
78
|
process.umask(0o077);
|
|
75
|
-
await source.backup(databasePath
|
|
79
|
+
await source.backup(databasePath, {
|
|
80
|
+
progress({ remainingPages }) {
|
|
81
|
+
return remainingPages;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
76
84
|
}
|
|
77
85
|
finally {
|
|
78
86
|
source.close();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as de from "./de.js";
|
|
2
2
|
import * as en from "./en.js";
|
|
3
|
+
import * as ja from "./ja.js";
|
|
3
4
|
/** One file per language, searched all at once. To add a language, add a file next to these and list it here. */
|
|
4
|
-
export const BOILERPLATE_ANYWHERE = [de.ANYWHERE, en.ANYWHERE].flat();
|
|
5
|
-
export const BOILERPLATE_TRAILING = [de.TRAILING, en.TRAILING].flat();
|
|
5
|
+
export const BOILERPLATE_ANYWHERE = [de.ANYWHERE, en.ANYWHERE, ja.ANYWHERE].flat();
|
|
6
|
+
export const BOILERPLATE_TRAILING = [de.TRAILING, en.TRAILING, ja.TRAILING].flat();
|
|
6
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocks, in Japanese, that publishers put into their text without being part of the story. A block counts only when its whole text is one
|
|
3
|
+
* of these lines (see `noise/boilerplate.ts`), so a sentence of the story that contains the words stays.
|
|
4
|
+
*/
|
|
5
|
+
export const ANYWHERE = [
|
|
6
|
+
// Calls to follow the site on Google or elsewhere.
|
|
7
|
+
/^(?:Google(?: ニュース)?でフォロー(?:する)?|Google ニュースで[^.!?]{2,40}をフォロー)$/i,
|
|
8
|
+
// The text of download buttons whose picture is gone.
|
|
9
|
+
/^(?:App Storeからダウンロード|Google Playで手に入れよう|Google Playで入手|App Storeでダウンロード)$/i,
|
|
10
|
+
// A picture's filename shown as text.
|
|
11
|
+
/^[\w\-. ]{3,120}\.(?:jpe?g|png|webp|gif|avif)$/i,
|
|
12
|
+
];
|
|
13
|
+
/** Footers at the end of the text; they count only in the second half. */
|
|
14
|
+
export const TRAILING = [
|
|
15
|
+
/^(?:この記事|本記事|本稿).{3,120}(?:で初出|に初出|で公開|に掲載|転載).*$/i,
|
|
16
|
+
/^(?:この記事について(?:話し合う|語る|コメント)|コメントする)\b.{0,40}$/i,
|
|
17
|
+
/^(?:関連記事|トピック|タグ|関連タグ):.{0,200}$/i,
|
|
18
|
+
];
|
|
19
|
+
//# sourceMappingURL=ja.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import de from "./de.js";
|
|
2
2
|
import en from "./en.js";
|
|
3
|
+
import ja from "./ja.js";
|
|
3
4
|
/**
|
|
4
5
|
* One file per language. A notice is looked for in every language at once, because an article in one language can
|
|
5
6
|
* sit on a site in another. To add a language, add a file next to these and list it here.
|
|
6
7
|
*/
|
|
7
|
-
const LANGUAGES = [de, en];
|
|
8
|
+
const LANGUAGES = [de, en, ja];
|
|
8
9
|
export const CONSENT_ANCHORS = LANGUAGES.flatMap(language => language.anchors);
|
|
9
10
|
export const CONSENT_COMPANIONS = LANGUAGES.flatMap(language => language.companions);
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Consent notices that sit where an embed would be, in Japanese. See `de.ts` for what the two lists mean. */
|
|
2
|
+
export default {
|
|
3
|
+
anchors: [
|
|
4
|
+
/^(?:外部コンテンツ|おすすめの外部コンテンツ|埋め込みコンテンツ)$/i,
|
|
5
|
+
/^(?:YouTube|Vimeo|Instagram|X|Twitter|TikTok|Spotify|Facebook)のコンテンツ$/i,
|
|
6
|
+
/^(?:外部コンテンツを表示|コンテンツを表示|外部コンテンツを読み込む)$/i,
|
|
7
|
+
],
|
|
8
|
+
companions: [
|
|
9
|
+
/^(?:外部コンテンツの読み込みに同意する|同意して表示|コンテンツを表示する)$/i,
|
|
10
|
+
/個人データが(?:外部|サードパーティ)/i,
|
|
11
|
+
/プライバシーポリシー/i,
|
|
12
|
+
],
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=ja.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import de from "./de.js";
|
|
2
2
|
import en from "./en.js";
|
|
3
|
+
import ja from "./ja.js";
|
|
3
4
|
/** One file per language, searched all at once. To add a language, add a file next to these and list it here. */
|
|
4
|
-
export const LOADING_LABELS = [de, en].flat();
|
|
5
|
+
export const LOADING_LABELS = [de, en, ja].flat();
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-line placeholders, in Japanese, that a script replaces with content. A reader that does not run the script
|
|
3
|
+
* keeps them as text. Only a block that holds nothing else counts.
|
|
4
|
+
*/
|
|
5
|
+
export default [
|
|
6
|
+
/^(?:読み込み中|ロード中|少々お待ちください)(?:\s*(?:\.{3}|…))?$/i,
|
|
7
|
+
];
|
|
8
|
+
//# sourceMappingURL=ja.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import de from "./de.js";
|
|
2
2
|
import en from "./en.js";
|
|
3
|
+
import ja from "./ja.js";
|
|
3
4
|
/** One file per language, searched all at once. To add a language, add a file next to these and list it here. */
|
|
4
|
-
export const PLAY_LABEL = new RegExp(`^(?:${[de, en].flat().map(label => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`, "i");
|
|
5
|
+
export const PLAY_LABEL = new RegExp(`^(?:${[de, en, ja].flat().map(label => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`, "i");
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import de from "./de.js";
|
|
2
2
|
import en from "./en.js";
|
|
3
|
+
import ja from "./ja.js";
|
|
3
4
|
/** One file per language, searched all at once. To add a language, add a file next to these and list it here. */
|
|
4
|
-
export const SIDEBAR_HEADINGS = [de, en].flat();
|
|
5
|
+
export const SIDEBAR_HEADINGS = [de, en, ja].flat();
|
|
5
6
|
export { SIDEBAR_SELECTORS } from "./selectors.js";
|
|
6
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Headings, in Japanese, that start a list of other articles. See `en.ts` for how they are used. */
|
|
2
|
+
export default [
|
|
3
|
+
/^(?:関連記事|おすすめ記事|オススメ記事|あわせて読みたい|人気の記事|人気記事|アクセスランキング|注目の記事|こちらもおすすめ|最新記事|新着記事|関連リンク)$/i,
|
|
4
|
+
// A site's welcome with logo and links at the end of the text.
|
|
5
|
+
/^.+へようこそ!?$/i,
|
|
6
|
+
];
|
|
7
|
+
//# sourceMappingURL=ja.js.map
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { JSDOM } from "jsdom";
|
|
2
2
|
import { CONSENT_ANCHORS, CONSENT_COMPANIONS } from "./content/patterns/consent/index.js";
|
|
3
|
+
import { SIDEBAR_HEADINGS } from "./content/patterns/sidebars/index.js";
|
|
3
4
|
import { resolveEmbeds } from "./content/embeds/index.js";
|
|
5
|
+
import { loadingPlaceholders } from "./content/noise/loading.js";
|
|
6
|
+
import { trackingPixels } from "./content/noise/pixels.js";
|
|
4
7
|
import { teaserLinks } from "./content/noise/teaserLinks.js";
|
|
5
8
|
import { markVideoTeasers } from "./content/noise/videoTeasers.js";
|
|
6
9
|
import { flat, pruneEmpty } from "./content/shared.js";
|
|
@@ -76,6 +79,7 @@ function isStructuredData(text) {
|
|
|
76
79
|
}
|
|
77
80
|
function removeReaderNoise(root, title, stats, author) {
|
|
78
81
|
let changed = false;
|
|
82
|
+
const total = (root.textContent ?? "").length;
|
|
79
83
|
const drop = (element, rule) => {
|
|
80
84
|
if (!element.parentNode)
|
|
81
85
|
return;
|
|
@@ -88,16 +92,25 @@ function removeReaderNoise(root, title, stats, author) {
|
|
|
88
92
|
// A feed's own text ends with "[mehr]": the link to the article is no part of what is read.
|
|
89
93
|
if (teaserLinks.apply(root, stats))
|
|
90
94
|
changed = true;
|
|
95
|
+
if (loadingPlaceholders.apply(root, stats))
|
|
96
|
+
changed = true;
|
|
97
|
+
if (trackingPixels.apply(root, stats))
|
|
98
|
+
changed = true;
|
|
91
99
|
for (const selector of BREADCRUMB_MARKERS) {
|
|
92
100
|
try {
|
|
93
101
|
root.querySelectorAll(selector).forEach(element => drop(element, "breadcrumb"));
|
|
94
102
|
}
|
|
95
103
|
catch { /* selector unsupported by the DOM */ }
|
|
96
104
|
}
|
|
97
|
-
//
|
|
105
|
+
// Short list of single links near the start (breadcrumbs) or in the tail (topic/tag chips).
|
|
98
106
|
for (const list of root.querySelectorAll("ol, ul")) {
|
|
99
107
|
const items = [...list.children].filter(child => child.tagName === "LI");
|
|
100
|
-
if (items.length < 2 || items.length > 8
|
|
108
|
+
if (items.length < 2 || items.length > 8)
|
|
109
|
+
continue;
|
|
110
|
+
const before = charsBefore(root, list);
|
|
111
|
+
const isHead = before <= 300;
|
|
112
|
+
const isTail = (total - before) <= TAIL_CHARACTERS;
|
|
113
|
+
if (!isHead && !isTail)
|
|
101
114
|
continue;
|
|
102
115
|
const onlyLinks = items.every(item => {
|
|
103
116
|
const links = item.querySelectorAll("a");
|
|
@@ -105,7 +118,7 @@ function removeReaderNoise(root, title, stats, author) {
|
|
|
105
118
|
return links.length === 1 && label.length > 0 && label.length <= 30 && flat(links[0].textContent) === label;
|
|
106
119
|
});
|
|
107
120
|
if (onlyLinks)
|
|
108
|
-
drop(list, "breadcrumb");
|
|
121
|
+
drop(list, isHead ? "breadcrumb" : "topic-list");
|
|
109
122
|
}
|
|
110
123
|
// A product card of a shop link: its picture is a CSS background, which no reader draws, and its price is a snapshot
|
|
111
124
|
// that is out of date when the article is read later. What is left would be a label, a link and a number.
|
|
@@ -260,7 +273,6 @@ function removeReaderNoise(root, title, stats, author) {
|
|
|
260
273
|
}
|
|
261
274
|
}
|
|
262
275
|
// A comment section's house rules at the end of the text: no reader has comments, so heading and what follows go.
|
|
263
|
-
const total = (root.textContent ?? "").length;
|
|
264
276
|
for (const block of leafBlocks(root)) {
|
|
265
277
|
if (!block.parentNode || !COMMENT_RULES_HEADING.test(flat(block.textContent)))
|
|
266
278
|
continue;
|
|
@@ -275,6 +287,36 @@ function removeReaderNoise(root, title, stats, author) {
|
|
|
275
287
|
drop(block, "comment-rules");
|
|
276
288
|
break;
|
|
277
289
|
}
|
|
290
|
+
// A consent wall, tracking choice or publisher welcome at the end: heading and everything below go.
|
|
291
|
+
for (const block of leafBlocks(root)) {
|
|
292
|
+
if (!block.parentNode || !CONSENT_WALL_HEADING.test(flat(block.textContent)))
|
|
293
|
+
continue;
|
|
294
|
+
if (total - charsBefore(root, block) > TAIL_CHARACTERS)
|
|
295
|
+
continue;
|
|
296
|
+
let node = block;
|
|
297
|
+
while (node.parentNode && node !== root) {
|
|
298
|
+
while (node.nextSibling)
|
|
299
|
+
node.nextSibling.remove();
|
|
300
|
+
node = node.parentNode;
|
|
301
|
+
}
|
|
302
|
+
drop(block, "consent-wall");
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
// An orphan recommendation heading left without subsequent links or content.
|
|
306
|
+
for (const block of leafBlocks(root)) {
|
|
307
|
+
if (!block.parentNode)
|
|
308
|
+
continue;
|
|
309
|
+
const text = flat(block.textContent);
|
|
310
|
+
if (!SIDEBAR_HEADINGS.some(pattern => pattern.test(text)))
|
|
311
|
+
continue;
|
|
312
|
+
const parent = block.parentElement;
|
|
313
|
+
if (parent && !parent.querySelector("a[href], img, picture, video, iframe") && flat(parent.textContent) === text) {
|
|
314
|
+
drop(parent, "empty-section-label");
|
|
315
|
+
}
|
|
316
|
+
else if (!block.querySelector("a[href], img, picture, video, iframe") && block.nextElementSibling === null) {
|
|
317
|
+
drop(block, "empty-section-label");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
278
320
|
// Empty nested slots are structural evidence: the next story paragraph is outside this ad container.
|
|
279
321
|
// Do not infer an empty ad from a standalone heading followed by text (which may itself be sponsored).
|
|
280
322
|
for (const container of [...root.querySelectorAll("div, aside, section, p")]) {
|
|
@@ -591,6 +633,7 @@ const AD_LABEL = /^(Anzeige|Werbung|Advertisement|Advertising|Advertorial|Ads?|S
|
|
|
591
633
|
const AD_SLOT_LABEL = /^(Anzeige|Werbung|Advertisement|Advertising|Ads?|広告|広告掲載|宣伝)\s*[::]?$/i;
|
|
592
634
|
const PRODUCT_LABEL = /^(Produkthinweis|Produktempfehlung|Product (notice|recommendation)|商品紹介|商品案内|製品紹介)\s*[::]?$/i;
|
|
593
635
|
const COMMENT_RULES_HEADING = /^(Regeln für Kommentare|Kommentarregeln|Kommentar-Richtlinien|Community[- ]Richtlinien|(Comment(ing)?|Community|Discussion|House) (rules|guidelines|policy|policies)|Rules for comments|Commenting rules)\s*:?$/i;
|
|
636
|
+
const CONSENT_WALL_HEADING = /^(?:Weiter mit Werbung|Mit Werbung weitersurfen|Weiterlesen mit Werbung|Zustimmen und weiter(?:lesen)?|Zustimmung erforderlich|Willkommen bei [^.!?]{2,40}!?|Einwilligung zu (?:Cookies|Tracking|Datenverarbeitung)|Cookie[- ]Einstellungen|Datenschutzoptionen|Continue with (?:ads|advertising|personalized advertising)|(?:Accept|Agree) and continue|Welcome to [^.!?]{2,40}!?|Cookie (?:and|&) (?:privacy|tracking) settings|Privacy (?:and|&) cookie policy|広告(?:付き|に同意して)で?続ける|同意して(?:続ける|進む)|.+へようこそ!?|Cookie(?:と|および)プライバシーの?設定)\s*:?$/i;
|
|
594
637
|
const TAG_LABEL = /^(Schlagw(ö|oe)rter|Schlagworte|Tags?|Keywords|Themen|Topics)\s*:?$/i;
|
|
595
638
|
const VIDEO_CARD = /^\d{1,2}:\d{2}(:\d{2})?(?=\D)\s*\S/;
|
|
596
639
|
const SECTION_LABEL = /^(Auch interessant|Das könnte (dich|Sie|euch) (auch )?interessieren|Mehr zum Thema|Lesen Sie auch|Lies auch|Lest auch|Aktuelle News|Weitere (Artikel|Meldungen|News)|Popular Stories|Related (Stories|Articles|Posts|Reading)|More (Stories|from)|Top Stories|Meistgelesen|Topaktuell|Regeln für Kommentare|Kommentare|Comments?)\b\s*:?/i;
|