rsshub
Version:
Make RSS Great Again!
245 lines (214 loc) • 9.74 kB
text/typescript
import path from 'node:path';
import { type CheerioAPI, type Cheerio, load } from 'cheerio';
import type { Element } from 'domhandler';
import { type Context } from 'hono';
import { type DataItem, type Route, type Data, 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';
export const handler = async (ctx: Context): Promise<Data> => {
const { id } = ctx.req.param();
const limit: number = Number.parseInt(ctx.req.query('limit') ?? '10', 10);
const baseUrl: string = 'https://www.oschina.net';
const userHostRegex: string = String.raw`https://my\.oschina\.net`;
const targetUrl: string = new URL(`news/column?columnId=${id}`, baseUrl).href;
const response = await ofetch(targetUrl);
const $: CheerioAPI = load(response);
const language: string = $('html').attr('lang') ?? 'zh-CN';
let items: DataItem[] = [];
items = $('div.news-item')
.slice(0, limit)
.toArray()
.map((el): Element => {
const $el: Cheerio<Element> = $(el);
const title: string = $el.find('div.title').text();
const description: string = art(path.join(__dirname, 'templates/description.art'), {
intro: $el.find('div.description p.line-clamp').text(),
});
const pubDateStr: string | undefined = $el.find('inddiv.item').contents().last().text().trim();
const linkUrl: string | undefined = $el.attr('data-url');
const authorEls: Element[] = $el.find('inddiv.item a').toArray();
const authors: DataItem['author'] = authorEls.map((authorEl) => {
const $authorEl: Cheerio<Element> = $(authorEl);
return {
name: $authorEl.text(),
url: $authorEl.attr('href'),
};
});
const image: string | undefined = $el.find('img').attr('src');
const upDatedStr: string | undefined = pubDateStr;
const processedItem: DataItem = {
title,
description,
pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : undefined,
link: linkUrl,
author: authors,
content: {
html: description,
text: description,
},
image,
banner: image,
updated: upDatedStr ? timezone(parseDate(upDatedStr), +8) : undefined,
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);
$$('.ad-wrap').remove();
const title: string = $$('h1.article-box__title').text();
const description: string = art(path.join(__dirname, 'templates/description.art'), {
description: $$('div.content').html(),
});
const pubDateEl: Element = $$('div.article-box__meta div.item-list div.item')
.toArray()
.find((i) => /\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}/.test($$(i).text()));
const pubDateStr: string | undefined = pubDateEl ? $$(pubDateEl).text() : undefined;
const linkUrl: string | undefined = $$('val[data-name="shareUrl"]').attr('data-value');
const categoryEls: Element[] = [...$$('div.breadcrumb-box a.item').toArray().slice(0, -1), ...$$('div.article-box__meta div.item-list div.item span.label').toArray(), ...$$('div.tags-box a.tag-item').toArray()];
const categories: string[] = [...new Set(categoryEls.map((el) => $$(el).text()).filter(Boolean))];
const authorEls: Element[] = $$('div.article-box__meta div.item-list div.item a')
.toArray()
.filter((i) => ($$(i).attr('href') ? new RegExp(`^${userHostRegex}/u/\\d+$`).test($$(i).attr('href') as string) : false));
const authors: DataItem['author'] = authorEls.map((authorEl) => {
const $authorEl: Cheerio<Element> = $$(authorEl);
return {
name: $authorEl.text(),
url: $authorEl.attr('href'),
};
});
const guid: string = `oschina-${$$('val[data-name="objId"]').attr('data-value')}`;
const image: string | undefined = $$('val[data-name="sharePic"]').attr('data-value');
const upDatedStr: string | undefined = $$('meta[property="bytedance:updated_time"]').attr('content') || pubDateStr;
let processedItem: DataItem = {
title,
description,
pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate,
link: linkUrl ? new URL(linkUrl, baseUrl).href : item.link,
category: categories,
author: authors,
guid,
id: guid,
content: {
html: description,
text: description,
},
image,
banner: image,
updated: upDatedStr ? parseDate(upDatedStr) : item.updated,
language,
};
const extraLinkEls: Element[] = $$('div.related-links-box ul.link-list li a').toArray();
const extraLinks = extraLinkEls
.map((extraLinkEl) => {
const $$extraLinkEl: Cheerio<Element> = $$(extraLinkEl);
return {
url: $$extraLinkEl.attr('href'),
type: 'related',
content_html: $$extraLinkEl.parent().html(),
};
})
.filter((_): _ is { url: string; type: string; content_html: string } => true);
if (extraLinks) {
processedItem = {
...processedItem,
_extra: {
links: extraLinks,
},
};
}
return {
...item,
...processedItem,
};
});
})
)
).filter((_): _ is DataItem => true);
const author: string | undefined = $('a.logo').attr('title');
return {
title: `${author} - ${$('div#tabDropdownListOpen a.selected').text()}`,
description: $('meta[name="description"]').attr('content'),
link: targetUrl,
item: items,
allowEmpty: true,
image: $('a.logo img').attr('src'),
author,
language,
id: $('val[data-name="weixinShareUrl"]').attr('data-value'),
};
};
export const route: Route = {
path: '/column/:id',
name: '专栏',
url: 'www.oschina.net',
maintainers: ['nczitzk'],
handler,
example: '/oschina/column/14',
parameters: {
id: '专栏 id,可在对应专栏页 URL 中找到',
},
description: `:::tip
若订阅 [开源安全专栏](https://www.oschina.net/news/column?columnId=14),网址为 \`https://www.oschina.net/news/column?columnId=14\`,请截取 \`https://www.oschina.net/news/column?columnId=\` 到末尾的部分 \`14\` 作为 \`id\` 参数填入,此时目标路由为 [\`/oschina/column/14\`](https://rsshub.app/oschina/column/14)。
:::
<details>
<summary>更多专栏</summary>
| 名称 | ID |
| --------------- | --- |
| 古典主义 Debian | 4 |
| 自由&开源 | 5 |
| 溯源 | 6 |
| 开源先懂协议 | 7 |
| 开源变局 | 8 |
| 创造者说 | 9 |
| 精英主义 BSD | 10 |
| 苹果有开源 | 11 |
| 开源访谈 | 12 |
| 抱团找组织 | 13 |
| 开源安全 | 14 |
| OSPO | 15 |
| 创业小辑 | 16 |
| 星推荐 | 17 |
| 单口开源 | 18 |
| 编辑部观察直播 | 19 |
| 开源商业化 | 20 |
| ChatGPT 专题 | 21 |
| 开源新思 | 24 |
| 开源日报 | 25 |
| 大模型思辨 | 26 |
| 家里有个程序员 | 27 |
| 开源漫谈 | 23 |
</details>
`,
categories: ['programming'],
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportRadar: true,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['www.oschina.net'],
target: (_, url) => {
const urlObj: URL = new URL(url);
const id: string | undefined = urlObj.searchParams.get('id') ?? undefined;
return `/oschina/column/${id}`;
},
},
],
view: ViewType.Articles,
};