From 69745adfe2906fbe180e54c8c8b07cdcb998cec3 Mon Sep 17 00:00:00 2001 From: karasu Date: Tue, 10 Sep 2024 22:11:09 +0800 Subject: [PATCH] feat(route): kakuyomu (#16679) * feat(route): kakuyomu * remove any types --- lib/routes/kakuyomu/namespace.ts | 6 +++ lib/routes/kakuyomu/types.ts | 6 +++ lib/routes/kakuyomu/works.ts | 73 ++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 lib/routes/kakuyomu/namespace.ts create mode 100644 lib/routes/kakuyomu/types.ts create mode 100644 lib/routes/kakuyomu/works.ts diff --git a/lib/routes/kakuyomu/namespace.ts b/lib/routes/kakuyomu/namespace.ts new file mode 100644 index 0000000000000..ec24f74506b5d --- /dev/null +++ b/lib/routes/kakuyomu/namespace.ts @@ -0,0 +1,6 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'カクヨム', + url: 'kakuyomu.jp', +}; diff --git a/lib/routes/kakuyomu/types.ts b/lib/routes/kakuyomu/types.ts new file mode 100644 index 0000000000000..80e46dab9b6b4 --- /dev/null +++ b/lib/routes/kakuyomu/types.ts @@ -0,0 +1,6 @@ +export interface NextDataEpisode { + __typename: 'Episode'; + id: string; + title: string; + publishedAt: string; +} diff --git a/lib/routes/kakuyomu/works.ts b/lib/routes/kakuyomu/works.ts new file mode 100644 index 0000000000000..a882f51ffad9b --- /dev/null +++ b/lib/routes/kakuyomu/works.ts @@ -0,0 +1,73 @@ +import type { Data, DataItem, Route } from '@/types'; +import { load } from 'cheerio'; +import type { Context } from 'hono'; +import ofetch from '@/utils/ofetch'; +import cache from '@/utils/cache'; +import type { NextDataEpisode } from './types'; +import { parseDate } from '@/utils/parse-date'; + +export const route: Route = { + name: '投稿', + categories: ['reading'], + path: '/works/:id', + example: '/kakuyomu/works/1177354054894027232', + parameters: { + id: '投稿 ID', + }, + maintainers: ['KarasuShin'], + handler, + features: { + supportRadar: true, + }, + radar: [ + { + source: ['kakuyomu.jp/works/:id'], + target: '/works/:id', + }, + ], +}; + +async function handler(ctx: Context): Promise { + const id = ctx.req.param('id'); + const url = `https://kakuyomu.jp/works/${id}`; + const limit = Number.parseInt(ctx.req.query('limit') || '10'); + const $ = load(await ofetch(url)); + + const nextData = JSON.parse($('#__NEXT_DATA__').text()); + + const { + props: { + pageProps: { __APOLLO_STATE__ }, + }, + } = nextData; + + const { + [`Work:${id}`]: { title, catchphrase }, + } = __APOLLO_STATE__; + + const values = Object.values(__APOLLO_STATE__); + const episodes = values.filter((value) => value.__typename === 'Episode') as NextDataEpisode[]; + const items = (await Promise.all( + episodes + .sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)) + .slice(0, limit) + .map((item) => { + const episodeUrl = `https://kakuyomu.jp/works/${id}/episodes/${item.id}`; + return cache.tryGet(episodeUrl, async () => { + const $ = load(await ofetch(episodeUrl)); + const description = $('.widget-episodeBody').html(); + return { + title: item.title, + description, + pubDate: parseDate(item.publishedAt), + }; + }); + }) + )) as DataItem[]; + + return { + title, + description: catchphrase, + item: items, + }; +}