From 225a3eaaeb50e8316bac988be9ad528f5b5f31c3 Mon Sep 17 00:00:00 2001 From: magazian <16831220+magazian@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:00:02 +0800 Subject: [PATCH 1/2] feat(route): add Anhui Museum news and exhibition route --- lib/routes/ahm/abxw.ts | 80 ++++++++++++++++++++ lib/routes/ahm/namespace.ts | 10 +++ lib/routes/ahm/xztj.tsx | 147 ++++++++++++++++++++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 lib/routes/ahm/abxw.ts create mode 100644 lib/routes/ahm/namespace.ts create mode 100644 lib/routes/ahm/xztj.tsx diff --git a/lib/routes/ahm/abxw.ts b/lib/routes/ahm/abxw.ts new file mode 100644 index 000000000000..7345c639becd --- /dev/null +++ b/lib/routes/ahm/abxw.ts @@ -0,0 +1,80 @@ +import { load } from 'cheerio'; + +import type { DataItem, Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import { getPlaywrightPage } from '@/utils/playwright'; +import timezone from '@/utils/timezone'; + +import { namespace } from './namespace'; + +const baseUrl = 'https://www.ahm.cn'; + +export const route: Route = { + path: '/news/abxw', + categories: ['travel'], + example: '/ahm/news/abxw', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.ahm.cn/News/List/abxw'], + target: '/news/abxw', + }, + ], + name: '安博新闻', + maintainers: ['magazian'], + handler: async () => { + const museumName = namespace.zh?.name || namespace.name; + const listUrl = `${baseUrl}/News/List/abxw`; + + // Anhui Museum website use CT2-WAAP to prevent web scraping, so need to use Playwright to get the page content. + const { page, destroy } = await getPlaywrightPage(listUrl, { + gotoConfig: { waitUntil: 'domcontentloaded' }, + }); + + let html: string; + try { + await page.waitForSelector('ul.img-cont-list li', { + timeout: 15000, + }); + html = await page.content(); + } finally { + await destroy(); + } + + const $ = load(html); + + const items: DataItem[] = $('ul.img-cont-list li') + .toArray() + .map((el) => { + const $el = $(el); + const a = $el.find('a'); + const href = a.attr('href') ?? ''; + const link = new URL(href, baseUrl).href; + + const dateP = $el.find('.date p'); + const monthDay = dateP.eq(0).text(); + const year = dateP.eq(1).text(); + const dateStr = `${year}/${monthDay}`; + + return { + title: $el.find('.cont h3').text(), + link, + pubDate: timezone(parseDate(dateStr, 'YYYY/MM/DD'), 8), + }; + }); + + return { + title: `${museumName} - 安博新闻`, + link: listUrl, + language: 'zh-CN', + item: items, + }; + }, +}; diff --git a/lib/routes/ahm/namespace.ts b/lib/routes/ahm/namespace.ts new file mode 100644 index 000000000000..c2975fdf15a9 --- /dev/null +++ b/lib/routes/ahm/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Anhui Museum', + url: 'www.ahm.cn', + + zh: { + name: '安徽博物院', + }, +}; diff --git a/lib/routes/ahm/xztj.tsx b/lib/routes/ahm/xztj.tsx new file mode 100644 index 000000000000..779edc0e7dfa --- /dev/null +++ b/lib/routes/ahm/xztj.tsx @@ -0,0 +1,147 @@ +import { load } from 'cheerio'; +import dayjs from 'dayjs'; +import { renderToString } from 'hono/jsx/dom/server'; + +import type { DataItem, Route } from '@/types'; +import { parseDate } from '@/utils/parse-date'; +import { getPlaywrightPage } from '@/utils/playwright'; + +import { namespace } from './namespace'; + +const baseUrl = 'https://www.ahm.cn'; + +const parseExhibitionDate = (dateStr?: string): string | undefined => (dateStr ? dayjs(dateStr).format('YYYY-MM-DD') : undefined); + +const parseExhibitionDuration = (durationText: string) => { + const [start, end] = durationText.split('-').map((s) => s.trim()); + return { + startDate: parseExhibitionDate(start), + endDate: parseExhibitionDate(end), + }; +}; + +export const route: Route = { + path: '/exhibition/xztj', + categories: ['travel'], + example: '/ahm/exhibition/xztj', + features: { + requireConfig: false, + requirePuppeteer: true, + antiCrawler: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.ahm.cn/Exhibition/TListNow/xztj'], + target: '/exhibition/xztj', + }, + ], + name: 'Special Exhibition', + maintainers: ['magazian'], + handler: async () => { + const museumName = namespace.zh?.name || namespace.name; + const listUrl = `${baseUrl}/Exhibition/TListNow/xztj`; + + // Anhui Museum website use CT2-WAAP to prevent web scraping, so need to use Playwright to get the page content. + const { page, destroy } = await getPlaywrightPage(listUrl, { + gotoConfig: { waitUntil: 'domcontentloaded' }, + }); + + let html: string; + try { + await page.waitForSelector('ul.exhibition-new li', { + timeout: 15000, + }); + html = await page.content(); + } finally { + await destroy(); + } + + const $ = load(html); + + const items: DataItem[] = $('ul.exhibition-new li') + .toArray() + .flatMap((el) => { + const $el = $(el); + + const onclickAttr = $el.find('a[onclick]').first().attr('onclick') ?? ''; + const onclickMatch = onclickAttr.match(/f_visitCount\('(\d+)','([^']+)','([^']+)'\)/); + + const exhibitionId = onclickMatch?.[1] ?? ''; + const link = onclickMatch?.[2] ?? ''; + + const title = $el.find('.cont h3').text(); + const imgUrl = $el.find('img').attr('src') ?? ''; + + const durationText = + $el + .find('p') + .toArray() + .map((p) => $(p).text()) + .find((t) => t.startsWith('展出时间:')) ?? ''; + + const locationText = + $el + .find('p') + .toArray() + .map((p) => $(p).text()) + .find((t) => t.startsWith('展出地点:')) ?? ''; + + const location = locationText.replace('展出地点:', ''); + const fullDuration = durationText.replace('展出时间:', ''); + + const { startDate, endDate } = parseExhibitionDuration(fullDuration); + + const pubDate = startDate ? parseDate(startDate) : undefined; + + const description = renderToString( +
+ 地点: + {location || '参考详情'} +
++ 开展: + {startDate || '未定/常设'} +
++ 闭展: + {endDate || '未定/常设'} +
+ {fullDuration && ( ++ 原始展期:{fullDuration} +
+ )} +