diff --git a/lib/app.ts b/lib/app.ts index df4dc45b0d..1cd4b8d742 100644 --- a/lib/app.ts +++ b/lib/app.ts @@ -15,8 +15,9 @@ import parameter from '@/middleware/parameter'; import logger from '@/utils/logger'; -import initRoutes from '@/init-routes'; +// import initRoutes from '@/init-routes'; import { notFoundHandler, errorHandler } from '@/errors'; +import registry from '@/registry'; process.on('uncaughtException', (e) => { logger.error('uncaughtException: ' + e); @@ -36,7 +37,8 @@ app.use(antiHotlink); app.use(parameter); app.use(cache); -initRoutes(app); +// initRoutes(app); +registry(app); app.notFound(notFoundHandler); app.onError(errorHandler); diff --git a/lib/registry.ts b/lib/registry.ts new file mode 100644 index 0000000000..c555cdc1f0 --- /dev/null +++ b/lib/registry.ts @@ -0,0 +1,57 @@ +import type { Namespace, Route } from '@/types'; +import { directoryImport } from 'directory-import'; +import type { Hono, Handler } from 'hono'; +import * as path from 'node:path'; + +const modules = directoryImport({ + targetDirectoryPath: path.join(__dirname, './routes-new'), + importPattern: /\.ts$/, +}); + +const namespaces: Record< + string, + Namespace & { + routes: Record; + } +> = {}; +for (const module in modules) { + const content = modules[module] as + | { + route: Route; + } + | { + namespace: Namespace; + }; + const namespace = module.split('/')[1]; + if ('namespace' in content) { + namespaces[namespace] = Object.assign( + { + routes: {}, + }, + namespaces[namespace], + content.namespace + ); + } else if ('route' in content) { + if (!namespaces[namespace]) { + namespaces[namespace] = { + name: namespace, + routes: {}, + }; + } + namespaces[namespace].routes[content.route.path] = content.route; + } +} + +export default function (app: Hono) { + for (const namespace in namespaces) { + const subApp = app.basePath(`/${namespace}`); + for (const path in namespaces[namespace].routes) { + const wrapedHandler: Handler = async (ctx) => { + if (!ctx.get('data')) { + ctx.set('data', await namespaces[namespace].routes[path].handler(ctx)); + } + }; + subApp.get(path, wrapedHandler); + } + } +} diff --git a/lib/routes-new/bilibili/bangumi.ts b/lib/routes-new/bilibili/bangumi.ts new file mode 100644 index 0000000000..9aeafcb2c4 --- /dev/null +++ b/lib/routes-new/bilibili/bangumi.ts @@ -0,0 +1,72 @@ +import got from '@/utils/got'; +import type { Route } from '@/types'; + +export const route: Route = { + path: '/bangumi/media/:mediaid', + categories: ['new-media'], + example: '/bilibili/bangumi/media/9192', + parameters: { + mediaid: '番剧媒体 id, 番剧主页 URL 中获取', + }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportRadar: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: '番剧', + maintainers: ['DIYgod'], + handler: async (ctx) => { + let seasonid = ctx.req.param('seasonid'); + const mediaid = ctx.req.param('mediaid'); + + let mediaData; + if (mediaid) { + const response = await got({ + method: 'get', + url: `https://www.bilibili.com/bangumi/media/md${mediaid}`, + }); + mediaData = JSON.parse(response.data.match(/window\.__INITIAL_STATE__=([\S\s]+);\(function\(\)/)[1]) || {}; + seasonid = mediaData.mediaInfo.season_id; + } + const { data } = await got.get(`https://api.bilibili.com/pgc/web/season/section?season_id=${seasonid}`); + + let episodes = []; + if (data.result.main_section && data.result.main_section.episodes) { + episodes = [ + ...episodes, + ...data.result.main_section.episodes.map((item) => ({ + title: `第${item.title}话 ${item.long_title}`, + description: ``, + link: `https://www.bilibili.com/bangumi/play/ep${item.id}`, + })), + ]; + } + + if (data.result.section) { + for (const section of data.result.section) { + if (section.episodes) { + episodes = [ + ...episodes, + ...section.episodes.map((item) => ({ + title: `${item.title} ${item.long_title}`, + description: ``, + link: `https://www.bilibili.com/bangumi/play/ep${item.id}`, + })), + ]; + } + } + } + + return { + title: mediaData?.mediaInfo.title, + link: `https://www.bilibili.com/bangumi/media/md${mediaData?.mediaInfo.media_id}/`, + image: mediaData?.mediaInfo.cover, + description: mediaData?.mediaInfo.evaluate, + item: episodes, + }; + }, +}; diff --git a/lib/routes-new/bilibili/namespace.ts b/lib/routes-new/bilibili/namespace.ts new file mode 100644 index 0000000000..cc6da5657d --- /dev/null +++ b/lib/routes-new/bilibili/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Bilibili', + url: 'www.bilibili.com', +}; diff --git a/lib/types.ts b/lib/types.ts index bc708f7279..a0069a37ec 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,3 +1,5 @@ +import type { Context } from 'hono'; + export type DataItem = { title: string; description?: string; @@ -43,3 +45,36 @@ export type Data = { feedLink?: string; lastBuildDate?: string; }; + +export type Namespace = { + name: string; + url?: string; + categories?: string[]; + description?: string; +}; + +export type Route = { + path: string; + name: string; + url?: string; + maintainers: string[]; + handler: (ctx: Context) => Promise | Data; + example: string; + parameters?: Record; + description?: string; + categories?: string[]; + + features: { + requireConfig?: string[] | false; + requirePuppeteer?: boolean; + antiCrawler?: boolean; + supportRadar?: boolean; + supportBT?: boolean; + supportPodcast?: boolean; + supportScihub?: boolean; + }; + radar?: { + source: string[]; + target?: string; + }; +};