rsshub
Version:
Make RSS Great Again!
325 lines (295 loc) • 13.7 kB
text/typescript
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<Data> => {
const { category = 'thelatest' } = ctx.req.param();
const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10);
const baseUrl: string = 'https://language.chinadaily.com.cn';
const targetUrl: string = new URL(category, baseUrl).href;
const response = await ofetch(targetUrl);
const $: CheerioAPI = load(response);
const language = $('html').attr('lang') ?? 'zh-CN';
let items: DataItem[] = [];
items = $('div.gy_box, ul.content_list li')
.slice(0, limit)
.toArray()
.map((el): Element => {
const $el: Cheerio<Element> = $(el);
const $aEl: Cheerio<Element> = $el.find('a:not(.gy_box_img):not(.a_img)').first();
const title: string = $aEl.text();
const image: string | undefined = $el.find('a.gy_box_img img, a.a_img 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.gy_box_txt3 a').text(),
});
const linkUrl: string | undefined = $aEl.attr('href');
const processedItem: DataItem = {
title,
description,
link: linkUrl?.startsWith('//') ? `https:${linkUrl}` : linkUrl,
content: {
html: description,
text: description,
},
image,
banner: image,
language,
};
return processedItem;
});
items = (
await Promise.all(
items.map((item) => {
if (!item.link) {
return item;
}
return cache.tryGet(item.link, async (): Promise<DataItem> => {
const detailResponse = await ofetch(item.link);
const $$: CheerioAPI = load(detailResponse);
const title: string = $$('meta[ property="og:title"]').attr('content');
const pubDateStr: string | undefined = $$('p.main_title3').text().split(/\s/).pop();
const categories: string[] | undefined = $$('meta[name="keywords"]').attr('content')?.split(/,/);
const authorEls: Element[] = $$('meta[name="source"], meta[name="author"], meta[name="editor"]').toArray();
const authorNames: string[] = [
...new Set(
authorEls
.map((authorEl) => {
const $$authorEl: Cheerio<Element> = $$(authorEl);
return $$authorEl.attr('content');
})
.filter((content): content is string => content !== undefined)
),
];
const authors: DataItem['author'] = authorNames.map((name) => ({
name,
}));
const image: string | undefined = $$('meta[property="og:image"]').attr('content');
const upDatedStr: string | undefined = pubDateStr;
let processedItem: DataItem = {
title,
pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate,
category: categories,
author: authors,
image,
banner: image,
updated: upDatedStr ? timezone(parseDate(upDatedStr), +8) : item.updated,
language,
};
const $enclosureEl: Cheerio<Element> = $$('iframe#playerFrame, audio').first();
const enclosureUrl: string | undefined = $enclosureEl.attr('src');
if (enclosureUrl) {
processedItem = {
...processedItem,
enclosure_url: enclosureUrl?.startsWith('//') ? `https:${enclosureUrl}` : enclosureUrl,
enclosure_title: title,
enclosure_length: undefined,
itunes_duration: undefined,
itunes_item_image: image,
};
}
$$('div.urlShareArea').remove();
const description: string | undefined =
item.description +
art(path.join(__dirname, 'templates/description.art'), {
description: $$('div#Content').html(),
});
processedItem = {
...processedItem,
description,
content: {
html: description,
text: description,
},
};
return {
...item,
...processedItem,
};
});
})
)
).filter((_): _ is DataItem => true);
const title: string = $('title').text();
const author: string = title.split(/-\s/).pop();
return {
title,
description: $('meta[name="description"]').attr('content'),
link: targetUrl,
item: items,
allowEmpty: true,
image: $('a.header2_logo1 img').attr('src'),
author,
language,
itunes_author: author,
itunes_category: 'Language',
id: targetUrl,
};
};
export const route: Route = {
path: '/language/:category{.+}?',
name: '英语点津',
url: 'language.chinadaily.com.cn',
maintainers: ['nczitzk'],
handler,
example: '/chinadaily/language/thelatest',
parameters: {
category: {
description: '分类,默认为 `thelatest`,即精彩推荐,可在对应分类页 URL 中找到, Category, `thelatest`,即精彩推荐 by default',
options: [
{
label: '精彩推荐',
value: 'thelatest',
},
{
label: '每日一词',
value: 'news_hotwords/word_of_the_day',
},
{
label: '双语新闻',
value: 'news_bilingual',
},
{
label: '新闻热词',
value: 'news_hotwords',
},
{
label: '实用口语',
value: 'practice_tongue',
},
{
label: '译词课堂',
value: 'trans_collect',
},
{
label: '图片新闻',
value: 'news_photo',
},
{
label: '视频精选',
value: 'video_links',
},
{
label: '新闻播报',
value: 'audio_cd',
},
{
label: '专栏作家',
value: 'columnist',
},
{
label: '权威发布',
value: '5af95d44a3103f6866ee845c',
},
],
},
},
description: `:::tip
若订阅 [精彩推荐](https://language.chinadaily.com.cn/thelatest),网址为 \`https://language.chinadaily.com.cn/thelatest\`,请截取 \`https://language.chinadaily.com.cn/\` 到末尾的部分 \`thelatest\` 作为 \`category\` 参数填入,此时目标路由为 [\`/chinadaily/language/thelatest\`](https://rsshub.app/chinadaily/language/thelatest)。
:::
<details>
<summary>更多分类</summary>
| 分类 | ID |
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| [精彩推荐](https://language.chinadaily.com.cn/thelatest) | [thelatest](https://rsshub.app/chinadaily/language/thelatest) |
| [每日一词](https://language.chinadaily.com.cn/news_hotwords/word_of_the_day) | [news_hotwords/word_of_the_day](https://rsshub.app/chinadaily/language/news_hotwords/word_of_the_day) |
| [双语新闻](https://language.chinadaily.com.cn/news_bilingual) | [news_bilingual](https://rsshub.app/chinadaily/language/news_bilingual) |
| [新闻热词](https://language.chinadaily.com.cn/news_hotwords) | [news_hotwords](https://rsshub.app/chinadaily/language/news_hotwords) |
| [实用口语](https://language.chinadaily.com.cn/practice_tongue) | [practice_tongue](https://rsshub.app/chinadaily/language/practice_tongue) |
| [译词课堂](https://language.chinadaily.com.cn/trans_collect) | [trans_collect](https://rsshub.app/chinadaily/language/trans_collect) |
| [图片新闻](https://language.chinadaily.com.cn/news_photo) | [news_photo](https://rsshub.app/chinadaily/language/news_photo) |
| [视频精选](https://language.chinadaily.com.cn/video_links) | [video_links](https://rsshub.app/chinadaily/language/video_links) |
| [新闻播报](https://language.chinadaily.com.cn/audio_cd) | [audio_cd](https://rsshub.app/chinadaily/language/audio_cd) |
| [专栏作家](https://language.chinadaily.com.cn/columnist) | [audio_cd](https://rsshub.app/chinadaily/language/columnist) |
| [权威发布](https://language.chinadaily.com.cn/5af95d44a3103f6866ee845c) | [5af95d44a3103f6866ee845c](https://rsshub.app/chinadaily/language/5af95d44a3103f6866ee845c) |
</details>
`,
categories: ['traditional-media'],
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportRadar: true,
supportBT: false,
supportPodcast: true,
supportScihub: false,
},
radar: [
{
source: ['language.chinadaily.com.cn/:category'],
target: (params) => {
const category: string = params.category;
return `/chinadaily/language${category ? `/${category}` : ''}`;
},
},
{
title: '精彩推荐',
source: ['language.chinadaily.com.cn/thelatest'],
target: '/language/thelatest',
},
{
title: '每日一词',
source: ['language.chinadaily.com.cn/news_hotwords/word_of_the_day'],
target: '/language/news_hotwords/word_of_the_day',
},
{
title: '双语新闻',
source: ['language.chinadaily.com.cn/news_bilingual'],
target: '/language/news_bilingual',
},
{
title: '新闻热词',
source: ['language.chinadaily.com.cn/news_hotwords'],
target: '/language/news_hotwords',
},
{
title: '实用口语',
source: ['language.chinadaily.com.cn/practice_tongue'],
target: '/language/practice_tongue',
},
{
title: '译词课堂',
source: ['language.chinadaily.com.cn/trans_collect'],
target: '/language/trans_collect',
},
{
title: '图片新闻',
source: ['language.chinadaily.com.cn/news_photo'],
target: '/language/news_photo',
},
{
title: '视频精选',
source: ['language.chinadaily.com.cn/video_links'],
target: '/language/video_links',
},
{
title: '新闻播报',
source: ['language.chinadaily.com.cn/audio_cd'],
target: '/language/audio_cd',
},
{
title: '专栏作家',
source: ['language.chinadaily.com.cn/columnist'],
target: '/language/columnist',
},
{
title: '权威发布',
source: ['language.chinadaily.com.cn/5af95d44a3103f6866ee845c'],
target: '/language/5af95d44a3103f6866ee845c',
},
],
view: ViewType.Articles,
};