diff --git a/lib/routes/jisilu/category.ts b/lib/routes/jisilu/category.ts new file mode 100644 index 00000000000000..1607014e45d4fe --- /dev/null +++ b/lib/routes/jisilu/category.ts @@ -0,0 +1,115 @@ +import { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import ofetch from '@/utils/ofetch'; + +import { rootUrl, processItems } from './util'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const handler = async (ctx: Context): Promise => { + const { id } = ctx.req.param(); + + if (!id) { + throw new InvalidParameterError('请填入合法的分类 id,参见广场 https://www.jisilu.cn/explore/'); + } + + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`/category/${id}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'zh'; + + const items: DataItem[] = await processItems($, $('div.aw-question-list'), limit); + + $('div.pagination').remove(); + + const author = $('meta[name="keywords"]').prop('content').split(/,/)[0]; + const feedImage = $('div.aw-logo img').prop('src'); + + return { + title: `${$('title').text()} - ${$('li.active') + .slice(1) + .toArray() + .map((l) => $(l).text()) + .join('|')}`, + description: $('meta[name="description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/category/:id', + name: '分类', + url: 'www.jisilu.cn', + maintainers: ['nczitzk'], + handler, + example: '/jisilu/category/4', + parameters: { + id: '分类 id,可在对应分类页 URL 中找到', + }, + description: `:::tip +若订阅 [债券/可转债](https://www.jisilu.cn/category/4),网址为 \`https://www.jisilu.cn/category/4\`,请截取 \`https://www.jisilu.cn/category/\` 到末尾的部分 \`4\` 作为 \`id\` 参数填入,此时目标路由为 [\`/jisilu/category/4\`](https://rsshub.app/jisilu/category/4)。 +::: + +| 新股 | 债券/可转债 | 套利 | 其他 | 基金 | 股票 | +| ---- | ----------- | ---- | ---- | ---- | ---- | +| 3 | 4 | 5 | 6 | 7 | 8 | +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.jisilu.cn/category/:id'], + target: '/category/:id', + }, + { + title: '新股', + source: ['www.jisilu.cn/category/3'], + target: '/category/3', + }, + { + title: '债券/可转债', + source: ['www.jisilu.cn/category/4'], + target: '/category/4', + }, + { + title: '套利', + source: ['www.jisilu.cn/category/5'], + target: '/category/5', + }, + { + title: '其他', + source: ['www.jisilu.cn/category/6'], + target: '/category/6', + }, + { + title: '基金', + source: ['www.jisilu.cn/category/7'], + target: '/category/7', + }, + { + title: '股票', + source: ['www.jisilu.cn/category/8'], + target: '/category/8', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/jisilu/explore.ts b/lib/routes/jisilu/explore.ts new file mode 100644 index 00000000000000..71e6683ce20974 --- /dev/null +++ b/lib/routes/jisilu/explore.ts @@ -0,0 +1,79 @@ +import { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import ofetch from '@/utils/ofetch'; + +import { rootUrl, processItems } from './util'; + +export const handler = async (ctx: Context): Promise => { + const { filter } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`/${filter ? 'home/' : ''}explore/${filter ?? ''}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'zh'; + + const items: DataItem[] = await processItems($, $('div.aw-question-list'), limit); + + $('div.pagination').remove(); + + const author = $('meta[name="keywords"]').prop('content').split(/,/)[0]; + const feedImage = $('div.aw-logo img').prop('src'); + + return { + title: `${$('title').text()} - ${$('li.active') + .slice(1) + .toArray() + .map((l) => $(l).text()) + .join('|')}`, + description: $('meta[name="description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/explore/:filter?', + name: '广场', + url: 'www.jisilu.cn', + maintainers: ['nczitzk'], + handler, + example: '/jisilu/explore', + parameters: { + category: '过滤器,默认为空,可在对应页 URL 中找到', + }, + description: `:::tip +若订阅 [债券/可转债 - 热门 - 30天](https://www.jisilu.cn/home/explore/category-4__sort_type-hot__day-30),网址为 \`https://www.jisilu.cn/home/explore/category-4__sort_type-hot__day-30\`,请截取 \`https://www.jisilu.cn/home/explore/\` 到末尾的部分 \`category-4__sort_type-hot__day-30\` 作为 \`filter\` 参数填入,此时目标路由为 [\`/jisilu/explore/category-4__sort_type-hot__day-30\`](https://rsshub.app/jisilu/explore/category-4__sort_type-hot__day-30)。 +::: + `, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.jisilu.cn/home/explore/:filter', 'www.jisilu.cn/home/explore', 'www.jisilu.cn/explore'], + target: (params) => { + const filter = params.filter; + + return `/jisilu/explore${filter ? `/${filter}` : ''}`; + }, + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/jisilu/index.ts b/lib/routes/jisilu/index.ts deleted file mode 100644 index 5c0efdf420f2b8..00000000000000 --- a/lib/routes/jisilu/index.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Route } from '@/types'; -import cache from '@/utils/cache'; -import got from '@/utils/got'; -import { load } from 'cheerio'; -import timezone from '@/utils/timezone'; -import { parseDate } from '@/utils/parse-date'; - -export const route: Route = { - path: '/:category?/:sort?/:day?', - categories: ['bbs'], - example: '/jisilu', - parameters: { category: '分类,见下表,默认为全部,可在 URL 中找到', sort: '排序,见下表,默认为最新,可在 URL 中找到', day: '几天内,见下表,默认为30天,本参数仅在排序参数设定为 `热门` 后才可生效' }, - features: { - requireConfig: false, - requirePuppeteer: false, - antiCrawler: false, - supportBT: false, - supportPodcast: false, - supportScihub: false, - }, - radar: [ - { - source: ['jisilu.cn/home/explore', 'jisilu.cn/explore', 'jisilu.cn/'], - }, - ], - name: '广场', - maintainers: ['nczitzk'], - handler, - url: 'jisilu.cn/home/explore', - description: `分类 - - | 全部 | 债券 / 可转债 | 基金 | 套利 | 新股 | - | ---- | ------------- | ---- | ---- | ---- | - | | 4 | 7 | 5 | 3 | - - 排序 - - | 最新 | 热门 | 按发表时间 | - | ---- | ---- | ---------- | - | | hot | add\_time | - - 几天内 - - | 30 天 | 7 天 | 当天 | - | ----- | ---- | ---- | - | 30 | 7 | 1 |`, -}; - -async function handler(ctx) { - const category = ctx.req.param('category') ?? ''; - const sort = ctx.req.param('sort') ?? ''; - const day = ctx.req.param('day') ?? ''; - - const rootUrl = 'https://www.jisilu.cn'; - let currentUrl = '', - name = '', - response; - - if (category === 'reply' || category === 'topic') { - if (sort) { - currentUrl = `${rootUrl}/people/${sort}`; - response = await got({ - method: 'get', - url: currentUrl, - }); - name = response.data.match(/(.*) 的个人主页 - 集思录<\/title>/)[1]; - response = await got({ - method: 'get', - url: `${rootUrl}/people/ajax/user_actions/uid-${response.data.match(/var PEOPLE_USER_ID = '(.*)'/)[1]}__actions-${category === 'topic' ? 1 : 2}01__page-0`, - }); - } else { - throw new Error('No user.'); - } - } else { - currentUrl = `${rootUrl}/home/explore/category-${category}__sort_type-${sort}__day-${day}`; - response = await got({ - method: 'get', - url: currentUrl, - }); - } - - const $ = load(response.data); - - $('.nav').prevAll('.aw-item').remove(); - - let items = $('.aw-item') - .slice(0, ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit')) : 30) - .toArray() - .map((item) => { - item = $(item); - const a = item.find('h4 a'); - - return { - title: a.text(), - link: a.attr('href'), - pubDate: timezone( - parseDate( - item - .find('.aw-text-color-999') - .text() - .match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2})/)[1] - ), - +8 - ), - author: category === 'reply' || category === 'topic' ? name : decodeURI(item.find('.aw-user-name').first().attr('href').split('/people/').pop()), - }; - }); - - items = await Promise.all( - items.map((item) => - cache.tryGet(item.link, async () => { - const detailResponse = await got({ - method: 'get', - url: item.link, - }); - - const content = load(detailResponse.data); - - content('.aw-dynamic-topic-more-operate').remove(); - - item.description = content('.aw-question-detail-txt').html() + content('.aw-dynamic-topic-content').html(); - - return item; - }) - ) - ); - - return { - title: `${name ? `${name}的${category === 'topic' ? '主题' : '回复'}` : '广场'} - 集思录`, - link: currentUrl, - item: items, - }; -} diff --git a/lib/routes/jisilu/namespace.ts b/lib/routes/jisilu/namespace.ts index e9fcafcb26404c..62af0d54ea1f77 100644 --- a/lib/routes/jisilu/namespace.ts +++ b/lib/routes/jisilu/namespace.ts @@ -3,5 +3,6 @@ import type { Namespace } from '@/types'; export const namespace: Namespace = { name: '集思录', url: 'jisilu.cn', + description: '一个以数据为本的投资社区', lang: 'zh-CN', }; diff --git a/lib/routes/jisilu/people.ts b/lib/routes/jisilu/people.ts new file mode 100644 index 00000000000000..539cf9bf4329a3 --- /dev/null +++ b/lib/routes/jisilu/people.ts @@ -0,0 +1,95 @@ +import { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import ofetch from '@/utils/ofetch'; + +import { rootUrl, processItems } from './util'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +const actions: { [key: string]: string } = { + questions: '101', + answers: '201', +}; + +export const handler = async (ctx: Context): Promise<Data> => { + const { id, type = 'questions' } = ctx.req.param(); + + if (type && type !== 'answers' && type !== 'questions') { + throw new InvalidParameterError('请填入合法的类型 id,可选值为 `questions` 即 `主题` 或 `answer` 即 `回复`,默认为空,即全部'); + } + + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`/people/${id}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'zh'; + const userId: string | undefined = response.match(/var\sPEOPLE_USER_ID\s=\s'(\d+)';/)?.[1]; + + if (!userId) { + throw new InvalidParameterError('请填入合法的用户 id,参见用户排名 https://www.jisilu.cn/users/'); + } + + const apiUrl: string = new URL(`people/ajax/user_actions/uid-${userId}__actions-${actions[type]}__page-1`, rootUrl).href; + + const detailResponse = await ofetch(apiUrl); + const $$: CheerioAPI = load(detailResponse); + + const items: DataItem[] = await processItems($$, $$('*'), limit); + + const author = $('meta[name="keywords"]').prop('content').split(/,/)[0]; + const feedImage = $('div.aw-logo img').prop('src'); + + return { + title: `${$('title').text()}${type ? ` - ${$(`div#${type} h3`).text()}` : ''}`, + description: $('meta[name="description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/people/:id/:type?', + name: '用户', + url: 'www.jisilu.cn', + maintainers: ['nczitzk'], + handler, + example: '/jisilu/people/天书', + parameters: { + id: '用户 id,可在对应用户页 URL 中找到', + type: '类型,可选值为 `questions` 即 `主题` 或 `answer` 即 `回复`,默认为 `questions` 即 `主题`', + }, + description: `:::tip +若订阅 [天书的主题](https://www.jisilu.cn/people/天书),网址为 \`https://www.jisilu.cn/people/天书\`,请截取 \`https://www.jisilu.cn/people/\` 到末尾的部分 \`天书\` 作为 \`id\` 参数填入,此时目标路由为 [\`/jisilu/people/天书\`](https://rsshub.app/jisilu/people/天书)。 +::: + +:::tip +前往 [用户排名](https://www.jisilu.cn/users/) 查看更多用户。 +::: +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.jisilu.cn/people/:id'], + target: '/people/:id', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/jisilu/topic.ts b/lib/routes/jisilu/topic.ts new file mode 100644 index 00000000000000..e6efc7f927ae23 --- /dev/null +++ b/lib/routes/jisilu/topic.ts @@ -0,0 +1,76 @@ +import { type CheerioAPI, load } from 'cheerio'; +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import ofetch from '@/utils/ofetch'; + +import { rootUrl, processItems } from './util'; + +export const handler = async (ctx: Context): Promise<Data> => { + const { id } = ctx.req.param(); + + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`/topic/${id}`, rootUrl).href; + + const response = await ofetch(targetUrl); + const $: CheerioAPI = load(response); + const language: string = $('html').prop('lang') ?? 'zh'; + + const items: DataItem[] = await processItems($, $('div.aw-question-list'), limit); + + $('div.pagination').remove(); + + const author = $('meta[name="keywords"]').prop('content').split(/,/)[0]; + const feedImage = $('div.aw-logo img').prop('src'); + + return { + title: $('title').text(), + description: $('meta[name="description"]').prop('content'), + link: targetUrl, + item: items, + allowEmpty: true, + image: feedImage, + author, + language, + id: targetUrl, + }; +}; + +export const route: Route = { + path: '/topic/:id', + name: '话题', + url: 'www.jisilu.cn', + maintainers: ['nczitzk'], + handler, + example: '/jisilu/topic/可转债', + parameters: { + id: '话题 id,可在对应话题页 URL 中找到', + }, + description: `:::tip +若订阅 [可转债](https://www.jisilu.cn/topic/可转债),网址为 \`https://www.jisilu.cn/topic/可转债\`,请截取 \`https://www.jisilu.cn/topic/\` 到末尾的部分 \`可转债\` 作为 \`id\` 参数填入,此时目标路由为 [\`/jisilu/topic/可转债\`](https://rsshub.app/jisilu/topic/可转债)。 +::: + +:::tip +前往 [话题广场](https://www.jisilu.cn/topic) 查看更多话题。 +::: +`, + categories: ['finance'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['www.jisilu.cn/topic/:id'], + target: '/topic/:id', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/jisilu/util.ts b/lib/routes/jisilu/util.ts new file mode 100644 index 00000000000000..7254beb1c86c5f --- /dev/null +++ b/lib/routes/jisilu/util.ts @@ -0,0 +1,111 @@ +import { type CheerioAPI, type Cheerio, type Element, load } from 'cheerio'; + +import { type DataItem } from '@/types'; + +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const rootUrl: string = 'https://www.jisilu.cn'; + +const processItems: ($: CheerioAPI, targetEl: Cheerio<Element>, limit: number) => Promise<DataItem[]> = async ($: CheerioAPI, targetEl: Cheerio<Element>, limit: number) => { + const items: DataItem[] = targetEl + .find('div.aw-item') + .toArray() + .map((item): DataItem => { + const $item: Cheerio<Element> = $(item); + + const aEl: Cheerio<Element> = $item.find('h4 a'); + + const title: string = aEl.text(); + const link: string | undefined = aEl.prop('href'); + + const pubDateStr: string | undefined = $item + .find('.aw-text-color-999') + .text() + .match(/(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/)?.[1]; + + const authorEl: Cheerio<Element> = $item.find('a.aw-user-name'); + const author: DataItem['author'] = authorEl.prop('href') + ? [ + { + name: authorEl.text(), + url: authorEl.prop('href'), + }, + ] + : authorEl.text(); + + return { + title, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : undefined, + link, + category: $item + .find('span.aw-question-tags a, a.aw-topic-name') + .toArray() + .map((c) => $(c).text()), + author, + }; + }); + + return ( + await Promise.all( + items.map((item) => { + if (!item.link && typeof item.link !== 'string') { + return item; + } + + return cache.tryGet(item.link, async (): Promise<DataItem> => { + const detailResponse = await ofetch(item.link); + const $$: CheerioAPI = load(detailResponse); + + const title: string = $$('div.aw-mod-head h1').text(); + + if (!title) { + return item; + } + + const isAnswer: boolean = item.link ? /answer_id/.test(item.link) : false; + + const description: string = (isAnswer ? $$('div.markitup-box').last() : $$('div.markitup-box').first()).html() ?? ''; + + const metaStr: string = $$(isAnswer ? 'div.aw-dynamic-topic-meta' : 'div.aw-question-detail-meta') + .find('span.aw-text-color-999') + .text(); + + const pubDateStr = metaStr.match(isAnswer ? /(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/ : /发表时间\s(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/)?.[1]; + const updatedStr = metaStr.match(/最后修改时间\s(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})/)?.[1]; + + const authorEl: Cheerio<Element> = $$(isAnswer ? 'p.publisher a.aw-user-name' : 'div.aw-side-bar-mod-body a.aw-user-name').first(); + const author: DataItem['author'] = authorEl.prop('href') + ? [ + { + name: authorEl.text(), + url: authorEl.prop('href'), + avatar: authorEl.parent().parent().find('img').first().prop('src'), + }, + ] + : authorEl.text(); + + return { + title, + description, + pubDate: pubDateStr ? timezone(parseDate(pubDateStr), +8) : item.pubDate, + link: item.link, + category: item.category, + author, + content: { + html: description, + text: $$('div.aw-question-detail-txt').first().text(), + }, + updated: updatedStr ? timezone(parseDate(updatedStr), +8) : item.updated, + }; + }); + }) + ) + ) + .filter((_): _ is DataItem => true) + .slice(0, limit); +}; + +export { rootUrl, processItems };