Skip to content

Commit e334550

Browse files
authored
feat(route): add Peter Wunder achievements badges feed (#21580)
* feat(route): add Peter Wunder achievements badges feed * fix(route): address review feedback for Peter Wunder achievements
1 parent 6288f06 commit e334550

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import type { CheerioAPI } from 'cheerio';
2+
import { load } from 'cheerio';
3+
4+
import type { DataItem, Route } from '@/types';
5+
import { ViewType } from '@/types';
6+
import cache from '@/utils/cache';
7+
import got from '@/utils/got';
8+
import { parseDate } from '@/utils/parse-date';
9+
10+
const author = 'Peter Wunder';
11+
const rootUrl = 'https://projects.peterwunder.de';
12+
const currentUrl = new URL('/achievements/', rootUrl).href;
13+
const icon = new URL('/achievements/images/touchicon.png', rootUrl).href;
14+
const defaultLimit = 20;
15+
16+
type BadgeItem = DataItem & {
17+
link: string;
18+
title: string;
19+
};
20+
21+
function absolutizeImageSource($: CheerioAPI, itemUrl: string) {
22+
$('article')
23+
.first()
24+
.find('[src]')
25+
.each((_, element) => {
26+
const value = $(element).attr('src');
27+
28+
if (value) {
29+
$(element).attr('src', new URL(value, itemUrl).href);
30+
}
31+
});
32+
}
33+
34+
function extractBadgeDescription($: CheerioAPI) {
35+
const article = $('article').first();
36+
37+
if (!article.length) {
38+
return;
39+
}
40+
41+
article.find('h1, script, style, noscript').remove();
42+
43+
return article.html() ?? undefined;
44+
}
45+
46+
function extractListItems($: CheerioAPI, limit: number): BadgeItem[] {
47+
return $('section.badges a.badge')
48+
.slice(0, limit)
49+
.toArray()
50+
.map((element) => {
51+
const badge = $(element);
52+
const href = badge.attr('href');
53+
const title = badge.find('.title').text().trim();
54+
55+
if (!href || !title) {
56+
return null;
57+
}
58+
59+
const image = badge.find('img').attr('src');
60+
const visibleStart = badge.attr('data-vis-start');
61+
62+
return {
63+
title,
64+
link: new URL(href, rootUrl).href,
65+
pubDate: visibleStart ? parseDate(visibleStart) : undefined,
66+
image: image ? new URL(image, rootUrl).href : undefined,
67+
};
68+
})
69+
.filter(Boolean) as BadgeItem[];
70+
}
71+
72+
function fetchBadge(item: BadgeItem) {
73+
return cache.tryGet(item.link, async () => {
74+
const { data: response } = await got(item.link);
75+
const $: CheerioAPI = load(response);
76+
77+
const title = $('article h1').first().text().trim();
78+
const visibleStart = $('ul.metadata li').first().find('time.date').first().attr('datetime');
79+
const image = $('meta[property="og:image"]').attr('content');
80+
absolutizeImageSource($, item.link);
81+
82+
return {
83+
...item,
84+
title: title || item.title,
85+
description: extractBadgeDescription($),
86+
pubDate: visibleStart ? parseDate(visibleStart) : item.pubDate,
87+
author,
88+
image: image ? new URL(image, rootUrl).href : item.image,
89+
};
90+
});
91+
}
92+
93+
const handler: Route['handler'] = async (ctx) => {
94+
const limit = Math.max(Number.parseInt(ctx.req.query('limit') ?? '', 10) || defaultLimit, 1);
95+
96+
const { data: response } = await got(currentUrl);
97+
const $: CheerioAPI = load(response);
98+
99+
const items = await Promise.all(extractListItems($, limit).map((item) => fetchBadge(item)));
100+
101+
return {
102+
title: 'All Activity Challenges - New Badges',
103+
description: "Latest badge pages from Peter Wunder's All Activity Challenges catalog. The website's own Atom feed was discontinued on August 20, 2024, so this route follows the latest entries directly from the site.",
104+
link: currentUrl,
105+
item: items,
106+
language: 'en',
107+
author,
108+
icon,
109+
logo: icon,
110+
image: icon,
111+
};
112+
};
113+
114+
export const route: Route = {
115+
path: '/achievements',
116+
categories: ['other'],
117+
view: ViewType.Pictures,
118+
example: '/peterwunder/achievements',
119+
parameters: {},
120+
features: {
121+
requireConfig: false,
122+
requirePuppeteer: false,
123+
antiCrawler: false,
124+
supportBT: false,
125+
supportPodcast: false,
126+
supportScihub: false,
127+
},
128+
radar: [
129+
{
130+
source: ['projects.peterwunder.de/achievements'],
131+
},
132+
],
133+
name: 'New Badges',
134+
maintainers: ['LinxHex'],
135+
description: "Latest badge pages from Peter Wunder's All Activity Challenges catalog. `pubDate` uses the first 'Visible in the app' date because the site does not expose a publication timestamp.",
136+
handler,
137+
url: 'projects.peterwunder.de/achievements',
138+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Namespace } from '@/types';
2+
3+
export const namespace: Namespace = {
4+
name: 'Peter Wunder',
5+
url: 'projects.peterwunder.de',
6+
categories: ['other'],
7+
description: 'Projects and catalogs maintained by Peter Wunder.',
8+
lang: 'en',
9+
};

0 commit comments

Comments
 (0)