From f11e58a47e684526113ad5520c277fba7d099637 Mon Sep 17 00:00:00 2001 From: Tsuyumi Date: Fri, 22 Nov 2024 17:50:35 +0800 Subject: [PATCH] feat: improve usability and short novel update handling - Add chapter display support - Fix chapter route radar - Optimize ranking and search limits - Use novelupdated_at as pubDate for short novels --- lib/routes/syosetu/index.ts | 13 +++++++++++-- lib/routes/syosetu/ranking-isekai.ts | 10 +++++----- lib/routes/syosetu/ranking-r18.ts | 12 ++++++------ lib/routes/syosetu/ranking.ts | 11 +++++------ lib/routes/syosetu/search.ts | 7 ++++--- lib/routes/syosetu/utils.ts | 6 +++--- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/lib/routes/syosetu/index.ts b/lib/routes/syosetu/index.ts index f1336c925c6f08..0779793e3f376a 100644 --- a/lib/routes/syosetu/index.ts +++ b/lib/routes/syosetu/index.ts @@ -24,7 +24,12 @@ export const route: Route = { radar: [ { title: 'Novel Updates', - source: ['novel18.syosetu.com/:ncode', 'ncode.syosetu.com/:ncode'], + source: ['ncode.syosetu.com/:ncode', 'ncode.syosetu.com/:ncode/:chapter'], + target: '/:ncode', + }, + { + title: 'Novel Updates', + source: ['novel18.syosetu.com/:ncode', 'novel18.syosetu.com/:ncode/:chapter'], target: '/:ncode', }, ], @@ -42,6 +47,10 @@ async function handler(ctx: Context): Promise { const chapterUrl = `${baseUrl}/${ncode}`; const item = await fetchChapterContent(chapterUrl); + // Shorts are updated rather than having new chapters + // Use novelupdated_at as pubDate since RSS 2.0 doesn't have updated field + item.pubDate = novel.novelupdated_at; + return { title: novel.title, description: novel.story, @@ -61,7 +70,7 @@ async function handler(ctx: Context): Promise { const chapterNumber = startChapter + index; const chapterUrl = `${baseUrl}/${ncode}/${chapterNumber}`; - const item = await fetchChapterContent(chapterUrl); + const item = await fetchChapterContent(chapterUrl, chapterNumber); return item; }).reverse() ); diff --git a/lib/routes/syosetu/ranking-isekai.ts b/lib/routes/syosetu/ranking-isekai.ts index 6301b544be78b6..6b99dbc39ecbca 100644 --- a/lib/routes/syosetu/ranking-isekai.ts +++ b/lib/routes/syosetu/ranking-isekai.ts @@ -25,11 +25,11 @@ export function parseIsekaiRankingType(type: string): { period: RankingPeriod; c return { period, category, novelType }; } -function getIsekaiSearchParams(period, category, novelType): SearchParams { +function getIsekaiSearchParams(period, category, novelType, limit): SearchParams { const searchParams: SearchParams = { order: periodToOrder[period], gzip: 5, - lim: 150, + lim: Math.ceil(limit / 2), }; if (novelType !== NovelType.TOTAL) { @@ -56,9 +56,9 @@ function getIsekaiSearchParams(period, category, novelType): SearchParams { export async function handleIsekaiRanking(type: string, limit: number): Promise { const { period, category, novelType } = parseIsekaiRankingType(type); const rankingUrl = `https://yomou.syosetu.com/rank/isekailist/type/${type}`; - const rankingTitle = `[${periodToJapanese[period]}] 異世界転生/転移${isekaiCategoryToJapanese[category]}ランキング - ${novelTypeToJapanese[novelType]}`; + const rankingTitle = `[${periodToJapanese[period]}] 異世界転生/転移${isekaiCategoryToJapanese[category]}ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`; - const searchParams = getIsekaiSearchParams(period, category, novelType); + const searchParams = getIsekaiSearchParams(period, category, novelType, limit); const api = new NarouNovelFetch(); const [tenseiResult, tenniResult] = await Promise.all([new SearchBuilder({ ...searchParams, istensei: 1 }, api).execute(), new SearchBuilder({ ...searchParams, istenni: 1 }, api).execute()]); @@ -86,7 +86,7 @@ export async function handleIsekaiRanking(type: string, limit: number): Promise< return { title: `小説家になろう - ${rankingTitle}`, link: rankingUrl, - item: (items as DataItem[]).slice(0, limit), + item: items as DataItem[], language: 'ja', }; } diff --git a/lib/routes/syosetu/ranking-r18.ts b/lib/routes/syosetu/ranking-r18.ts index 1d497bf40a09db..e17d0ed6bc8bc7 100644 --- a/lib/routes/syosetu/ranking-r18.ts +++ b/lib/routes/syosetu/ranking-r18.ts @@ -1,4 +1,4 @@ -import { Route, Data } from '@/types'; +import { Route, Data, DataItem } from '@/types'; import { art } from '@/utils/render'; import path from 'node:path'; import { Context } from 'hono'; @@ -136,9 +136,9 @@ function parseRankingType(type: string): { period: RankingPeriod; novelType: Nov }; } -function getRankingTitle(type: string): string { +function getRankingTitle(type: string, limit: number): string { const { period, novelType } = parseRankingType(type); - return `${periodToJapanese[period]}${novelTypeToJapanese[novelType]}ランキング`; + return `${periodToJapanese[period]}${novelTypeToJapanese[novelType]}ランキング BEST${limit}`; } async function handler(ctx: Context): Promise { @@ -152,7 +152,7 @@ async function handler(ctx: Context): Promise { const searchParams: SearchParams = { gzip: 5, - lim: 300, + lim: limit, order: periodToOrder[period], }; @@ -180,9 +180,9 @@ async function handler(ctx: Context): Promise { })); return { - title: `小説家になろう (${sub}) - ${getRankingTitle(type)}`, + title: `小説家になろう (${sub}) - ${getRankingTitle(type, limit)}`, link: rankingUrl, - item: items.slice(0, limit), + item: items as DataItem[], language: 'ja', }; } diff --git a/lib/routes/syosetu/ranking.ts b/lib/routes/syosetu/ranking.ts index fe6070a818b14b..330577d94284c5 100644 --- a/lib/routes/syosetu/ranking.ts +++ b/lib/routes/syosetu/ranking.ts @@ -1,4 +1,4 @@ -import { Route, Data } from '@/types'; +import { Route, Data, DataItem } from '@/types'; import { art } from '@/utils/render'; import path from 'node:path'; import { Context } from 'hono'; @@ -212,7 +212,7 @@ async function handler(ctx: Context): Promise { const api = new NarouNovelFetch(); const searchParams: SearchParams = { gzip: 5, - lim: 300, + lim: limit, }; let rankingUrl: string; @@ -223,7 +223,7 @@ async function handler(ctx: Context): Promise { case RankingType.LIST: { const { period, novelType } = parseGeneralRankingType(type); rankingUrl = `https://yomou.syosetu.com/rank/list/type/${type}`; - rankingTitle = `[${periodToJapanese[period]}] 総合ランキング - ${novelTypeToJapanese[novelType]}`; + rankingTitle = `[${periodToJapanese[period]}] 総合ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`; searchParams.order = periodToOrder[period]; if (novelType !== NovelType.TOTAL) { @@ -235,7 +235,7 @@ async function handler(ctx: Context): Promise { case RankingType.GENRE: { const { period, genre, novelType } = parseGenreRankingType(type); rankingUrl = `https://yomou.syosetu.com/rank/genrelist/type/${type}`; - rankingTitle = `[${periodToJapanese[period]}] ${GenreNotation[genre]}ランキング - ${novelTypeToJapanese[novelType]}`; + rankingTitle = `[${periodToJapanese[period]}] ${GenreNotation[genre]}ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`; searchParams.order = periodToOrder[period]; searchParams.genre = genre as Genre; @@ -248,7 +248,6 @@ async function handler(ctx: Context): Promise { case RankingType.ISEKAI: return handleIsekaiRanking(type, limit); - default: throw new InvalidParameterError(`Invalid ranking type: ${type}`); } @@ -269,7 +268,7 @@ async function handler(ctx: Context): Promise { return { title: `小説家になろう - ${rankingTitle}`, link: rankingUrl, - item: items.slice(0, limit), + item: items as DataItem[], language: 'ja', }; } diff --git a/lib/routes/syosetu/search.ts b/lib/routes/syosetu/search.ts index 8a26b60e72b7bb..08c0bc7aaa73f0 100644 --- a/lib/routes/syosetu/search.ts +++ b/lib/routes/syosetu/search.ts @@ -165,12 +165,12 @@ const setIfExists = (value) => value ?? undefined; * @see https://deflis.github.io/node-narou/index.html * @see https://dev.syosetu.com/man/api/ */ -function mapToSearchParams(query: string): SearchParams { +function mapToSearchParams(query: string, limit: number): SearchParams { const params = queryString.parse(query) as NarouSearchParams; const searchParams: SearchParams = { gzip: 5, - lim: 40, + lim: limit, }; searchParams.word = setIfExists(params.word); @@ -251,7 +251,8 @@ async function handler(ctx: Context): Promise { const { sub, query } = ctx.req.param(); const searchUrl = `https://${sub}.syosetu.com/search/search/search.php?${query}`; - const searchParams = mapToSearchParams(query); + const limit = Math.min(Number(ctx.req.query('limit') ?? 40), 40); + const searchParams = mapToSearchParams(query, limit); const builder = createNovelSearchBuilder(sub, searchParams); const result = await builder.execute(); diff --git a/lib/routes/syosetu/utils.ts b/lib/routes/syosetu/utils.ts index 421d594af04121..9970c6dc3d91da 100644 --- a/lib/routes/syosetu/utils.ts +++ b/lib/routes/syosetu/utils.ts @@ -8,7 +8,7 @@ import { NarouNovelFetch, NarouSearchResult, SearchBuilder, SearchBuilderR18 } f export async function fetchNovelInfo(ncode: string): Promise<{ baseUrl: string; novel: NarouSearchResult }> { const api = new NarouNovelFetch(); - const [generalRes, r18Res] = await Promise.all([new SearchBuilder({ gzip: 5, of: 't-s-k-ga-nt' }, api).ncode(ncode).execute(), new SearchBuilderR18({ gzip: 5, of: 't-s-k-ga-nt' }, api).ncode(ncode).execute()]); + const [generalRes, r18Res] = await Promise.all([new SearchBuilder({ gzip: 5, of: 't-s-k-ga-nt-nu' }, api).ncode(ncode).execute(), new SearchBuilderR18({ gzip: 5, of: 't-s-k-ga-nt-nu' }, api).ncode(ncode).execute()]); const isGeneral = generalRes.allcount !== 0; const novelData = isGeneral ? generalRes : r18Res; @@ -24,7 +24,7 @@ export async function fetchNovelInfo(ncode: string): Promise<{ baseUrl: string; }; } -export async function fetchChapterContent(chapterUrl: string): Promise { +export async function fetchChapterContent(chapterUrl: string, chapter?: number): Promise { return (await cache.tryGet(chapterUrl, async () => { const response = await ofetch(chapterUrl, { headers: { @@ -35,7 +35,7 @@ export async function fetchChapterContent(chapterUrl: string): Promise const $ = load(response); - const title = $('.p-novel__title').html() || ''; + const title = `${chapter ? `#${chapter} ` : ''}${$('.p-novel__title').html() || ''}`; const description = $('.p-novel__body').html() || ''; const pubDate = $('meta[name=WWWC]').attr('content');