diff --git a/lib/routes/ifun/n/category.ts b/lib/routes/ifun/n/category.ts new file mode 100644 index 00000000000000..b7f78de1fabeaa --- /dev/null +++ b/lib/routes/ifun/n/category.ts @@ -0,0 +1,102 @@ +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import ofetch from '@/utils/ofetch'; + +import { author, language, rootUrl, processItems } from './util'; + +export const handler = async (ctx: Context): Promise => { + const { id } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = rootUrl; + const apiUrl: string = new URL(`api/articles/${id ? 'categoryId' : 'all'}`, rootUrl).href; + const apiCategoryUrl: string = new URL('api/categories/all', rootUrl).href; + + const apiResponse = await ofetch(apiUrl, { + query: { + datasrc: id ? 'categoriesall' : 'articles', + current: 1, + size: limit, + categoryId: id, + }, + }); + + const apiCategoryResponse = await ofetch(apiCategoryUrl, { + query: { + datasrc: 'categories', + }, + }); + + const categoryName: string = apiCategoryResponse.data.find((item) => item.categoryid === id)?.category; + + const items: DataItem[] = processItems(apiResponse.data.records, limit); + + return { + title: `${author}${categoryName ? ` - ${categoryName}` : ''}`, + description: categoryName, + link: targetUrl, + item: items, + allowEmpty: true, + author, + language, + }; +}; + +export const route: Route = { + path: '/n/category/:id?', + name: '盐选故事分类', + url: 'n.ifun.cool', + maintainers: ['nczitzk'], + handler, + example: '/ifun/n/category', + parameters: { + id: '分类 id,默认为空,即全部,见下表', + }, + description: ` +| 名称 | ID | +| -------- | --- | +| 全部 | | +| 通告 | 1 | +| 故事盐选 | 2 | +| 趣集精选 | 3 | + `, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['n.ifun.cool'], + target: '/n/category/:id?', + }, + { + title: '全部', + source: ['n.ifun.cool'], + target: '/n/category', + }, + { + title: '通告', + source: ['n.ifun.cool'], + target: '/n/category/1', + }, + { + title: '盐选故事', + source: ['n.ifun.cool'], + target: '/n/category/2', + }, + { + title: '趣集精选', + source: ['n.ifun.cool'], + target: '/n/category/3', + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/ifun/n/search.ts b/lib/routes/ifun/n/search.ts new file mode 100644 index 00000000000000..035855c2aa8a6d --- /dev/null +++ b/lib/routes/ifun/n/search.ts @@ -0,0 +1,73 @@ +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import ofetch from '@/utils/ofetch'; + +import { author, language, rootUrl, processItems } from './util'; + +export const handler = async (ctx: Context): Promise => { + const { keywords } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`search-result/?s=${keywords}`, rootUrl).href; + const apiUrl: string = new URL('api/articles/searchkeywords', rootUrl).href; + + const apiResponse = await ofetch(apiUrl, { + query: { + keywords, + current: 1, + size: limit, + }, + }); + + const items: DataItem[] = processItems(apiResponse.data.records, limit); + + return { + title: `${author} - ${keywords}`, + description: keywords, + link: targetUrl, + item: items, + allowEmpty: true, + author, + language, + }; +}; + +export const route: Route = { + path: '/n/search/:keywords', + name: '盐选故事搜索', + url: 'n.ifun.cool', + maintainers: ['nczitzk'], + handler, + example: '/ifun/n/search/NPC', + parameters: { + keywords: '搜索关键字', + }, + description: `:::tip +若订阅 [关键词:NPC](https://n.ifun.cool/search-result/?s=NPC),网址为 \`https://n.ifun.cool/search-result/?s=NPC\`,请截取 \`s\` 的值 \`NPC\` 作为 \`keywords\` 参数填入,此时目标路由为 [\`/ifun/n/search/NPC\`](https://rsshub.app/ifun/n/search/NPC)。 +::: + `, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['n.ifun.cool/search-result'], + target: (_, url) => { + const urlObj = new URL(url); + const keywords = urlObj.searchParams.get('s'); + + return `/ifun/n/search/${keywords}`; + }, + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/ifun/n/tag.ts b/lib/routes/ifun/n/tag.ts new file mode 100644 index 00000000000000..122cc34ce21d4f --- /dev/null +++ b/lib/routes/ifun/n/tag.ts @@ -0,0 +1,76 @@ +import { type Context } from 'hono'; + +import { type DataItem, type Route, type Data, ViewType } from '@/types'; + +import ofetch from '@/utils/ofetch'; + +import { author, language, rootUrl, processItems } from './util'; + +export const handler = async (ctx: Context): Promise => { + const { name } = ctx.req.param(); + const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10); + + const targetUrl: string = new URL(`article-list/1?tagName=${name}`, rootUrl).href; + const apiUrl: string = new URL('api/articles/tagId', rootUrl).href; + + const apiResponse = await ofetch(apiUrl, { + query: { + datasrc: 'tagid', + tagname: name, + current: 1, + size: limit, + }, + }); + + const items: DataItem[] = processItems(apiResponse.data.records, limit); + + return { + title: `${author} - ${name}`, + description: name, + link: targetUrl, + item: items, + allowEmpty: true, + author, + language, + }; +}; + +export const route: Route = { + path: '/n/tag/:name', + name: '盐选故事专栏', + url: 'n.ifun.cool', + maintainers: ['nczitzk'], + handler, + example: '/ifun/n/tag/zhihu', + parameters: { + name: '专栏 id,可在对应专栏页 URL 中找到', + }, + description: `:::tip +若订阅 [zhihu](https://n.ifun.cool/article-list/2?tagName=zhihu),网址为 \`https://n.ifun.cool/article-list/2?tagName=zhihu\`,请截取 \`tagName\` 的值 \`zhihu\` 作为 \`name\` 参数填入,此时目标路由为 [\`/ifun/n/tag/zhihu\`](https://rsshub.app/ifun/n/tag/zhihu)。 + +更多专栏请见 [盐选故事专栏](https://n.ifun.cool/tags)。 +::: + `, + categories: ['new-media'], + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: true, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + radar: [ + { + source: ['n.ifun.cool/article-list/1'], + target: (_, url) => { + const urlObj = new URL(url); + const name = urlObj.searchParams.get('tagName'); + + return `/ifun/n/tag/${name}`; + }, + }, + ], + view: ViewType.Articles, +}; diff --git a/lib/routes/ifun/n/util.ts b/lib/routes/ifun/n/util.ts new file mode 100644 index 00000000000000..5f1bfa14d85913 --- /dev/null +++ b/lib/routes/ifun/n/util.ts @@ -0,0 +1,34 @@ +import { type DataItem } from '@/types'; + +import { parseDate } from '@/utils/parse-date'; + +const author: string = '趣集'; +const language: string = 'zh-CN'; +const rootUrl: string = 'https://n.ifun.cool'; + +const processItems: (items: any[], limit: number) => DataItem[] = (items: any[], limit: number) => + items.slice(0, limit).map((item): DataItem => { + const title: string = item.title; + const description: string = item.content; + const guid: string = `ifun-n-${item.id}`; + + const author: DataItem['author'] = item.author; + + return { + title, + description, + pubDate: parseDate(item.createtime), + link: item.id ? new URL(`articles/${item.id}`, rootUrl).href : undefined, + category: [...new Set([item.category, item.tag].filter(Boolean))], + author, + guid, + id: guid, + content: { + html: description, + text: description, + }, + language, + }; + }); + +export { author, language, rootUrl, processItems }; diff --git a/lib/routes/ifun/namespace.ts b/lib/routes/ifun/namespace.ts new file mode 100644 index 00000000000000..2fa5dca071e641 --- /dev/null +++ b/lib/routes/ifun/namespace.ts @@ -0,0 +1,9 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '趣集', + url: 'ifun.cool', + categories: ['new-media'], + description: '全面的找书、学习资源导航平台,它整合了电子书和科研文档的搜索功能,方便用户进行学习资料的检索和分享,为用户提供一站式的读书学习体验。', + lang: 'zh-CN', +};