|
| 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 | +}; |
0 commit comments