From 737cf6063bf1f57d4086213b7b941bb5c69cfd53 Mon Sep 17 00:00:00 2001 From: Jiamin <16831220+magazian@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:21:36 +0800 Subject: [PATCH] feat(route): add Guandong Museum Exhibitions and news route (#22729) * feat:add Guandong Museum Exhibitions and news * fix: update selector --- lib/routes/gdmuseum/exhibition.tsx | 126 +++++++++++++++++++++++++++++ lib/routes/gdmuseum/namespace.ts | 10 +++ lib/routes/gdmuseum/news.ts | 58 +++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 lib/routes/gdmuseum/exhibition.tsx create mode 100644 lib/routes/gdmuseum/namespace.ts create mode 100644 lib/routes/gdmuseum/news.ts diff --git a/lib/routes/gdmuseum/exhibition.tsx b/lib/routes/gdmuseum/exhibition.tsx new file mode 100644 index 0000000000..b42baebd3f --- /dev/null +++ b/lib/routes/gdmuseum/exhibition.tsx @@ -0,0 +1,126 @@ +import { load } from 'cheerio'; +import dayjs from 'dayjs'; +import type { Context } from 'hono'; +import { renderToString } from 'hono/jsx/dom/server'; + +import type { DataItem, Route } from '@/types'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; + +import { namespace } from './namespace'; + +// convert exhibition date string to YYYY-MM-DD format, e.g. "2023年5月1日" or "2023/5/1" => "2023-05-01" +const formatExhibitionDate = (d?: string) => { + if (!d) { + return; + } + return dayjs(d.replaceAll(/[年月/.]/g, '-').replaceAll('日', '')).format('YYYY-MM-DD'); +}; + +const parseExhibitionDuration = (duration?: string) => { + const allDates = duration?.match(/\d{4}[./年-]\d{1,2}[./月-]\d{1,2}日?/g) || []; + return { + startDate: formatExhibitionDate(allDates[0]), + endDate: formatExhibitionDate(allDates[1]), + }; +}; + +export const route: Route = { + path: '/exhibition/:type?', + categories: ['travel'], + example: '/gdmuseum/exhibition/temp', + parameters: { + type: 'Exhibition type, supported values: temp(临时展览), default: All exhibitions.', + }, + name: 'Current Exhibitions', + maintainers: ['magazian'], + radar: [ + { + source: ['www.gdmuseum.org.cn/col9/list'], + target: '/exhibition', + }, + ], + handler: async (ctx: Context) => { + const typeParam = ctx.req.param('type') || 'all'; + + const museumName = namespace.zh?.name || namespace.name; + const baseUrl = 'https://www.gdmuseum.org.cn'; + const url = `${baseUrl}/col9/list`; + + const response = await got(url); + const $ = load(response.data); + + const list = $('.ULLIST li a[href^="/cn/col"]') + .toArray() + .map((el) => { + const $item = $(el); + const title = $item.find('.divtt.qui-dot').text(); + + if (typeParam === 'temp') { + if (title.includes('常设展览')) { + return null; + } + } else if (typeParam !== 'all') { + return null; + } + + const rawLink = $item.attr('href'); + const itemLink = rawLink ? new URL(rawLink, baseUrl).href : ''; + + const rawSrc = $item.find('img').attr('src'); + const imgUrl = rawSrc ? new URL(rawSrc, baseUrl).href : undefined; + + const textDurationAndLocation = $item.find('.quitxt.qui-dot').text().trim(); + const [fullDuration = '', location = ''] = textDurationAndLocation.split('|').map((p) => p.trim()); + + const { startDate, endDate } = parseExhibitionDuration(fullDuration); + const pubDate = startDate ? parseDate(startDate) : undefined; + + const description = renderToString( +
+ {imgUrl && } +
+

+ 地点: + {location || '参考详情'} +

+

+ 开展: + {startDate || '未定/常设'} +

+

+ 闭展: + {endDate || '未定/常设'} +

+ {fullDuration && ( +

+ 原始展期:{fullDuration} +

+ )} +
+ ); + + return { + title, + link: itemLink, + pubDate, + description, + // For further .ics file processing + _extra: { + museumName, + location, + startDate, + endDate, + }, + } as DataItem; + }) + .filter((i): i is DataItem => i !== null); + + return { + title: `${museumName} - 正在热展${typeParam === 'temp' ? ' - 临时展览' : ''}`, + link: url, + language: 'zh-CN', + item: list, + }; + }, +}; diff --git a/lib/routes/gdmuseum/namespace.ts b/lib/routes/gdmuseum/namespace.ts new file mode 100644 index 0000000000..c37b3988de --- /dev/null +++ b/lib/routes/gdmuseum/namespace.ts @@ -0,0 +1,10 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Guangdong Museum', + url: 'gdmuseum.org.cn', + + zh: { + name: '广东省博物馆', + }, +}; diff --git a/lib/routes/gdmuseum/news.ts b/lib/routes/gdmuseum/news.ts new file mode 100644 index 0000000000..2271f9b771 --- /dev/null +++ b/lib/routes/gdmuseum/news.ts @@ -0,0 +1,58 @@ +import { load } from 'cheerio'; + +import type { DataItem, Route } from '@/types'; +import got from '@/utils/got'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +import { namespace } from './namespace'; + +export const route: Route = { + path: '/information', + categories: ['travel'], + example: '/gdmuseum/information', + name: 'Information', + maintainers: ['magazian'], + radar: [ + { + source: ['www.gdmuseum.org.cn/cn/col51/list'], + target: '/information', + }, + ], + handler: async () => { + const baseUrl = 'https://www.gdmuseum.org.cn'; + const apiUrl = `${baseUrl}/cn/col51/list`; + const museumName = namespace.zh?.name || namespace.name; + + const response = await got(apiUrl); + const $ = load(response.data); + + const list = $('.ULLIST li a[href^="/cn/col"]') + .toArray() + .map((el) => { + const $item = $(el); + + const rawLink = $item.attr('href') || ''; + const itemLink = new URL(rawLink, baseUrl).href; + + const title = $item.find('h3.h3.qui-dot').text(); + const day = $item.find('time b').text().trim(); + const yearMonth = $item.find('time i').text().trim(); + + const pubDate = timezone(parseDate(`${yearMonth}-${day}`), 8); + + return { + title, + link: itemLink, + pubDate, + } as DataItem; + }); + + return { + title: `${museumName} - 公告`, + link: apiUrl, + language: 'zh-CN', + item: list, + }; + }, +};