rsshub
Version:
Make RSS Great Again!
91 lines (74 loc) • 3.9 kB
text/typescript
import { Data, DataItem } from '@/types';
import { NarouNovelFetch, SearchBuilder, SearchParams, BigGenre } from 'narou';
import { art } from '@/utils/render';
import path from 'node:path';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import { Join } from 'narou/util/type';
import { RankingPeriod, NovelType, periodToJapanese, novelTypeToJapanese, periodToOrder, periodToPointField, IsekaiCategory, isekaiCategoryToJapanese } from './types/ranking';
export function parseIsekaiRankingType(type: string): { period: RankingPeriod; category: IsekaiCategory; novelType: NovelType } {
const [periodStr, categoryStr, novelTypeStr = NovelType.TOTAL] = type.split('_');
const period = periodStr as RankingPeriod;
const category = categoryStr as IsekaiCategory;
const novelType = novelTypeStr as NovelType;
const isValid = [Object.values(RankingPeriod).includes(period), Object.values(IsekaiCategory).includes(category), Object.values(NovelType).includes(novelType)].every(Boolean);
if (!isValid) {
throw new InvalidParameterError(`Invalid isekai ranking type: ${type}`);
}
return { period, category, novelType };
}
function getIsekaiSearchParams(period, category, novelType, limit): SearchParams {
const searchParams: SearchParams = {
order: periodToOrder[period],
gzip: 5,
// Request 20% more items to compensate for potential duplicates between tensei/tenni
lim: Math.ceil((limit / 2) * 1.2),
};
if (novelType !== NovelType.TOTAL) {
searchParams.type = novelType;
}
switch (category) {
case IsekaiCategory.RENAI:
searchParams.biggenre = BigGenre.Renai;
break;
case IsekaiCategory.FANTASY:
searchParams.biggenre = BigGenre.Fantasy;
break;
case IsekaiCategory.OTHER:
searchParams.biggenre = `${BigGenre.Bungei}-${BigGenre.Sf}-${BigGenre.Sonota}` as Join<BigGenre>;
break;
default:
throw new InvalidParameterError(`Invalid Isekai category: ${category}`);
}
return searchParams;
}
export async function handleIsekaiRanking(type: string, limit: number): Promise<Data> {
const { period, category, novelType } = parseIsekaiRankingType(type);
const rankingUrl = `https://yomou.syosetu.com/rank/isekailist/type/${type}`;
const rankingTitle = `[${periodToJapanese[period]}] 異世界転生/転移${isekaiCategoryToJapanese[category]}ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`;
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()]);
const combinedNovels = [...tenseiResult.values, ...tenniResult.values];
const uniqueNovels = [...new Map(combinedNovels.map((novel) => [novel.ncode, novel])).values()];
const pointField = periodToPointField[period];
if (!pointField) {
throw new InvalidParameterError(`Invalid period: ${period}`);
}
const items = uniqueNovels
.toSorted((a, b) => (b[pointField] || 0) - (a[pointField] || 0))
.map((novel, index) => ({
title: `#${index + 1} ${novel.title}`,
link: `https://ncode.syosetu.com/${String(novel.ncode).toLowerCase()}`,
description: art(path.join(__dirname, 'templates/description.art'), {
novel,
}),
author: novel.writer,
category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean),
}));
return {
title: `小説家になろう - ${rankingTitle}`,
link: rankingUrl,
item: items.slice(0, limit) as DataItem[],
language: 'ja',
};
}