From a4cc4204a8bbf78388514236bc422a875cf45219 Mon Sep 17 00:00:00 2001 From: Ethan Shen <42264778+nczitzk@users.noreply.github.com> Date: Sat, 7 Jun 2025 01:05:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(route):=20add=20=E5=8D=8A=E6=9C=88?= =?UTF-8?q?=E8=B0=88=20(#19305)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/routes/banyuetan/index.ts | 238 ++++++++++++++++++ lib/routes/banyuetan/namespace.ts | 9 + .../banyuetan/templates/description.art | 21 ++ 3 files changed, 268 insertions(+) create mode 100644 lib/routes/banyuetan/index.ts create mode 100644 lib/routes/banyuetan/namespace.ts create mode 100644 lib/routes/banyuetan/templates/description.art diff --git a/lib/routes/banyuetan/index.ts b/lib/routes/banyuetan/index.ts new file mode 100644 index 0000000000..44570b5141 --- /dev/null +++ b/lib/routes/banyuetan/index.ts @@ -0,0 +1,238 @@ +import { type Data, type DataItem, type Route, ViewType } from '@/types'; + +import { art } from '@/utils/render'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +import { type CheerioAPI, type Cheerio, load } from 'cheerio'; +import type { Element } from 'domhandler'; +import { type Context } from 'hono'; +import path from 'node:path'; + +export const handler = async (ctx: Context): Promise => { + const { id = 'jinritan' } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const baseUrl: string = 'http://www.banyuetan.org'; + const targetUrl: string = new URL(`byt/${id}/index.html`, baseUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language = $('html').attr('lang') ?? 'zh'; + + let items: DataItem[] = []; + + items = $('div.bty_tbtj_list ul.clearFix li') + .slice(0, limit) + .toArray() + .map((el): Element => { + const $el: Cheerio = $(el); + const $aEl: Cheerio = $el.find('h3 a'); + + const title: string = $aEl.text(); + const image: string | undefined = $el.find('img').attr('src'); + const description: string | undefined = art(path.join(__dirname, 'templates/description.art'), { + images: image + ? [ + { + src: image, + alt: title, + }, + ] + : undefined, + intro: $el.find('p').text(), + }); + const pubDateStr: string | undefined = $el.find('span.tag3').text(); + const linkUrl: string | undefined = $aEl.attr('href'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? parseDate(pubDateStr) : undefined, + link: linkUrl, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? parseDate(upDatedStr) : undefined, + language, + }; + + return processedItem; + }); + + items = await Promise.all( + items.map((item) => { + if (!item.link) { + return item; + } + + return cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.detail_tit h1').text(); + const description: string | undefined = + item.description + + art(path.join(__dirname, 'templates/description.art'), { + description: $$('div#detail_content').html(), + }); + const pubDateStr: string | undefined = $$('meta[property="og:release_date"]').attr('content'); + const categories: string[] = $$('META[name="keywords"]').attr('content')?.split(/,/) ?? []; + const authorEls: Element[] = [...$$('META[name="author"]').toArray(), ...$$('META[name="source"]').toArray()]; + const authors: DataItem['author'] = authorEls.map((authorEl) => { + const $$authorEl: Cheerio = $$(authorEl); + + return { + name: $$authorEl.attr('content'), + url: undefined, + avatar: undefined, + }; + }); + const image: string | undefined = $$('meta[property="og:image"]').attr('content'); + const upDatedStr: string | undefined = pubDateStr; + + const processedItem: DataItem = { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate, + category: categories, + author: authors, + content: { + html: description, + text: description, + }, + image, + banner: image, + updated: upDatedStr ? timezone(parseDate(upDatedStr), +8) : item.updated, + language, + }; + + return { + ...item, + ...processedItem, + }; + }); + }) + ); + + const title: string = $('title').text(); + + return { + title, + description: title, + link: targetUrl, + item: items, + allowEmpty: true, + image: new URL('static/v1/image/logo.png', baseUrl).href, + author: title.split(/—/).pop(), + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/:id?', + name: '栏目', + url: 'www.banyuetan.org', + maintainers: ['nczitzk'], + handler, + example: '/banyuetan/jinritan', + parameters: { + id: { + description: '栏目 ID,默认为 `jinritan`,即今日谈,可在对应分类页 URL 中找到', + options: [ + { + label: '今日谈', + value: 'jinritan', + }, + { + label: '时政讲解', + value: 'shizhengjiangjie', + }, + { + label: '评论', + value: 'banyuetanpinglun', + }, + { + label: '基层治理', + value: 'jicengzhili', + }, + { + label: '文化', + value: 'wenhua', + }, + { + label: '教育', + value: 'jiaoyu', + }, + ], + }, + }, + description: `:::tip +订阅 [今日谈](http://www.banyuetan.org/byt/jinritan/),其源网址为 \`http://www.banyuetan.org/byt/jinritan/\`,请参考该 URL 指定部分构成参数,此时路由为 [\`/banyuetan/jinritan\`](https://rsshub.app/banyuetan/jinritan)。 +::: + +| 栏目 | ID | +| -------------------------------------------------------------------- | ----------------------------------------------------------------- | +| [今日谈](http://www.banyuetan.org/byt/jinritan/index.html) | [jinritan](https://rsshub.app/banyuetan/jinritan) | +| [时政讲解](http://www.banyuetan.org/byt/shizhengjiangjie/index.html) | [shizhengjiangjie](https://rsshub.app/banyuetan/shizhengjiangjie) | +| [评论](http://www.banyuetan.org/byt/banyuetanpinglun/index.html) | [banyuetanpinglun](https://rsshub.app/banyuetan/banyuetanpinglun) | +| [基层治理](http://www.banyuetan.org/byt/jicengzhili/index.html) | [jicengzhili](https://rsshub.app/banyuetan/jicengzhili) | +| [文化](http://www.banyuetan.org/byt/wenhua/index.html) | [wenhua](https://rsshub.app/banyuetan/wenhua) | +| [教育](http://www.banyuetan.org/byt/jiaoyu/index.html) | [jiaoyu](https://rsshub.app/banyuetan/jiaoyu) | + +`, + categories: ['traditional-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.banyuetan.org/byt/:id'], + target: '/:id', + }, + { + title: '今日谈', + source: ['www.banyuetan.org/byt/jinritan/index.html'], + target: '/jinritan', + }, + { + title: '时政讲解', + source: ['www.banyuetan.org/byt/shizhengjiangjie/index.html'], + target: '/shizhengjiangjie', + }, + { + title: '评论', + source: ['www.banyuetan.org/byt/banyuetanpinglun/index.html'], + target: '/banyuetanpinglun', + }, + { + title: '基层治理', + source: ['www.banyuetan.org/byt/jicengzhili/index.html'], + target: '/jicengzhili', + }, + { + title: '文化', + source: ['www.banyuetan.org/byt/wenhua/index.html'], + target: '/wenhua', + }, + { + title: '教育', + source: ['www.banyuetan.org/byt/jiaoyu/index.html'], + target: '/jiaoyu', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/banyuetan/namespace.ts b/lib/routes/banyuetan/namespace.ts new file mode 100644 index 0000000000..e213dd6620 --- /dev/null +++ b/lib/routes/banyuetan/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '半月谈', + url: 'banyuetan.org', + categories: ['traditional-media'], + description: '', + lang: 'zh-CN', +}; diff --git a/lib/routes/banyuetan/templates/description.art b/lib/routes/banyuetan/templates/description.art new file mode 100644 index 0000000000..249654e7e6 --- /dev/null +++ b/lib/routes/banyuetan/templates/description.art @@ -0,0 +1,21 @@ +{{ if images }} + {{ each images image }} + {{ if image?.src }} +
+ {{ image.alt }} +
+ {{ /if }} + {{ /each }} +{{ /if }} + +{{ if intro }} +
{{ intro }}
+{{ /if }} + +{{ if description }} + {{@ description }} +{{ /if }} \ No newline at end of file