rsshub
Version:
Make RSS Great Again!
374 lines (361 loc) • 13.8 kB
JavaScript
import { n as init_esm_shims, t as __dirname } from "./esm-shims-CzJ_djXG.mjs";
import { t as art } from "./render-BQo6B4tL.mjs";
import { t as invalid_parameter_default } from "./invalid-parameter-rr4AgGpp.mjs";
import path from "node:path";
import { BigGenre, Genre, GenreNotation, NarouNovelFetch, SearchBuilder } from "narou";
//#region lib/routes/syosetu/types/ranking.ts
init_esm_shims();
let RankingPeriod = /* @__PURE__ */ function(RankingPeriod$1) {
RankingPeriod$1["DAILY"] = "daily";
RankingPeriod$1["WEEKLY"] = "weekly";
RankingPeriod$1["MONTHLY"] = "monthly";
RankingPeriod$1["QUARTER"] = "quarter";
RankingPeriod$1["YEARLY"] = "yearly";
RankingPeriod$1["TOTAL"] = "total";
return RankingPeriod$1;
}({});
let NovelType$1 = /* @__PURE__ */ function(NovelType$2) {
NovelType$2["TOTAL"] = "total";
NovelType$2["SHORT"] = "t";
NovelType$2["ONGOING"] = "r";
NovelType$2["COMPLETE"] = "er";
return NovelType$2;
}({});
let RankingType = /* @__PURE__ */ function(RankingType$1) {
RankingType$1["LIST"] = "list";
RankingType$1["GENRE"] = "genre";
RankingType$1["ISEKAI"] = "isekai";
return RankingType$1;
}({});
const periodToOrder = {
[RankingPeriod.DAILY]: "dailypoint",
[RankingPeriod.WEEKLY]: "weeklypoint",
[RankingPeriod.MONTHLY]: "monthlypoint",
[RankingPeriod.QUARTER]: "quarterpoint",
[RankingPeriod.YEARLY]: "yearlypoint",
[RankingPeriod.TOTAL]: "hyoka"
};
const periodToPointField = {
[RankingPeriod.DAILY]: "pt",
[RankingPeriod.WEEKLY]: "weekly_point",
[RankingPeriod.MONTHLY]: "monthly_point",
[RankingPeriod.QUARTER]: "quarter_point",
[RankingPeriod.YEARLY]: "yearly_point",
[RankingPeriod.TOTAL]: "global_point"
};
const periodToJapanese = {
[RankingPeriod.DAILY]: "日間",
[RankingPeriod.WEEKLY]: "週間",
[RankingPeriod.MONTHLY]: "月間",
[RankingPeriod.QUARTER]: "四半期",
[RankingPeriod.YEARLY]: "年間",
[RankingPeriod.TOTAL]: "累計"
};
const novelTypeToJapanese = {
[NovelType$1.TOTAL]: "すべて",
[NovelType$1.SHORT]: "短編",
[NovelType$1.ONGOING]: "連載中",
[NovelType$1.COMPLETE]: "完結済"
};
let IsekaiCategory = /* @__PURE__ */ function(IsekaiCategory$1) {
IsekaiCategory$1["RENAI"] = "1";
IsekaiCategory$1["FANTASY"] = "2";
IsekaiCategory$1["OTHER"] = "o";
return IsekaiCategory$1;
}({});
const isekaiCategoryToJapanese = {
[IsekaiCategory.RENAI]: "〔恋愛〕",
[IsekaiCategory.FANTASY]: "〔ファンタジー〕",
[IsekaiCategory.OTHER]: "〔文芸・SF・その他〕"
};
//#endregion
//#region lib/routes/syosetu/ranking-isekai.ts
function parseIsekaiRankingType(type) {
const [periodStr, categoryStr, novelTypeStr = NovelType$1.TOTAL] = type.split("_");
const period = periodStr;
const category = categoryStr;
const novelType = novelTypeStr;
if (![
Object.values(RankingPeriod).includes(period),
Object.values(IsekaiCategory).includes(category),
Object.values(NovelType$1).includes(novelType)
].every(Boolean)) throw new invalid_parameter_default(`Invalid isekai ranking type: ${type}`);
return {
period,
category,
novelType
};
}
function getIsekaiSearchParams(period, category, novelType, limit) {
const searchParams = {
order: periodToOrder[period],
gzip: 5,
lim: Math.ceil(limit / 2 * 1.2)
};
if (novelType !== NovelType$1.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}`;
break;
default: throw new invalid_parameter_default(`Invalid Isekai category: ${category}`);
}
return searchParams;
}
async function handleIsekaiRanking(type, limit) {
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 invalid_parameter_default(`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-e1f33e28.art"), { novel }),
author: novel.writer,
category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean)
}));
return {
title: `小説家になろう - ${rankingTitle}`,
link: rankingUrl,
item: items.slice(0, limit),
language: "ja"
};
}
//#endregion
//#region lib/routes/syosetu/ranking.ts
init_esm_shims();
const getParameters = () => {
const rankingTypeOptions = [
{
value: RankingType.LIST,
label: "総合ランキング (General Ranking)"
},
{
value: RankingType.GENRE,
label: "ジャンル別ランキング (Genre Ranking)"
},
{
value: RankingType.ISEKAI,
label: "異世界転生/転移ランキング (Isekai Ranking)"
}
];
const periodOptions = Object.entries(RankingPeriod).map(([key, value]) => ({
value,
label: `${periodToJapanese[value]} (${key})`
}));
const novelTypeOptions = Object.entries(NovelType$1).map(([key, value]) => ({
value,
label: `${novelTypeToJapanese[value]} (${key})`
}));
const genreOptions = Object.entries(Genre).filter(([, value]) => typeof value === "number").map(([key, value]) => ({
value: value.toString(),
label: key
}));
const isekaiOptions = Object.entries(IsekaiCategory).map(([key, value]) => ({
value,
label: `${isekaiCategoryToJapanese[value]} (${key})`
}));
return {
listType: {
description: "Ranking type",
options: rankingTypeOptions
},
type: {
description: "Detailed ranking type, can be found in Syosetu ranking URLs",
options: [
...periodOptions.flatMap((period) => novelTypeOptions.map((novelType) => ({
value: `${period.value}_${novelType.value}`,
label: `${RankingType.LIST} - [${periodToJapanese[period.value]}] 総合ランキング - ${novelTypeToJapanese[novelType.value]}`
}))),
...periodOptions.flatMap((period) => genreOptions.flatMap((genre) => novelTypeOptions.map((novelType) => ({
value: `${period.value}_${genre.value}_${novelType.value}`,
label: `${RankingType.GENRE} - [${periodToJapanese[period.value]}] ${GenreNotation[genre.value]}ランキング - ${novelTypeToJapanese[novelType.value]}`
})))),
...periodOptions.flatMap((period) => isekaiOptions.flatMap((category) => novelTypeOptions.map((novelType) => ({
value: `${period.value}_${category.value}_${novelType.value}`,
label: `${RankingType.ISEKAI} - [${periodToJapanese[period.value]}] 異世界転生/転移${isekaiCategoryToJapanese[category.value]}ランキング - ${novelTypeToJapanese[novelType.value]}`
}))))
]
}
};
};
const getBest5RadarItems = () => {
const periodRankings = Object.values(RankingPeriod).map((period) => ({
title: `${periodToJapanese[period]}ランキング BEST5`,
source: ["yomou.syosetu.com/rank/top/"],
target: `/ranking/list/${period}_total?limit=5`
}));
const genreRankings = Object.entries(Genre).filter(([, value]) => typeof value === "number" && value !== Genre.SonotaReplay && value !== Genre.NonGenre).map(([, value]) => ({
title: `[${periodToJapanese.daily}] ${GenreNotation[value]}ランキング BEST5`,
source: ["yomou.syosetu.com/rank/top/"],
target: `/ranking/genre/daily_${value}_total?limit=5`
}));
const isekaiRankings = Object.values(IsekaiCategory).map((category) => ({
title: `[${periodToJapanese.daily}] 異世界転生/転移${isekaiCategoryToJapanese[category]}ランキング BEST5`,
source: ["yomou.syosetu.com/rank/top/"],
target: `/ranking/isekai/daily_${category}_total?limit=5`
}));
return [
...periodRankings,
...genreRankings,
...isekaiRankings
];
};
const route = {
path: "/ranking/:listType/:type",
categories: ["reading"],
example: "/syosetu/ranking/list/daily_total?limit=50",
parameters: getParameters(),
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false
},
name: "Rankings",
url: "yomou.syosetu.com/rank/top",
maintainers: ["SnowAgar25"],
handler,
description: `
| Keyword | Description | 説明 |
| --- | --- | --- |
| list | Overall Ranking | 総合ランキング |
| genre | Genre Ranking | ジャンル別ランキング |
| isekai | Isekai/Reincarnation/Transfer Ranking | 異世界転生/転移ランキング |
| Period | Description |
| --- | --- |
| daily | Daily Ranking |
| weekly | Weekly Ranking |
| monthly | Monthly Ranking |
| quarter | Quarterly Ranking |
| yearly | Yearly Ranking |
| Type | Description |
| --- | --- |
| total | All Works |
| t | Short Stories |
| r | Ongoing Series |
| er | Completed Series |
::: warning
Please note that novel type options may vary depending on the ranking category.
ランキングの種類によって、小説タイプが異なる場合がございますのでご注意ください。
:::
::: danger 注意事項
The "注目度ランキング" (Attention Ranking) is not supported as syosetu does not provide a public API for this feature and the results cannot be replicated through the search API.
「注目度ランキング」については、API が非公開で検索 API でも同様の結果を得ることができないため、本 Route ではサポートしておりません。
:::
::: tip 異世界転生/転移ランキングについて (Isekai)
When multiple works have the same points, their order may differ from syosetu's ranking as syosetu randomizes the order for works with identical points.
集計の結果、同じポイントの作品が複数存在する場合、Syosetu ではランダムで順位が決定されるため、本 Route の順位と異なる場合があります。
:::
`,
radar: [
{
source: ["yomou.syosetu.com/rank/list/type/:type"],
target: "/ranking/list/:type"
},
{
source: ["yomou.syosetu.com/rank/genrelist/type/:type"],
target: "/ranking/genre/:type"
},
{
source: ["yomou.syosetu.com/rank/isekailist/type/:type"],
target: "/ranking/isekai/:type"
},
...getBest5RadarItems()
]
};
function parseGeneralRankingType(type) {
const [periodStr, novelTypeStr] = type.split("_");
const period = periodStr;
const novelType = novelTypeStr;
if (![Object.values(RankingPeriod).includes(period), Object.values(NovelType$1).includes(novelType)].every(Boolean)) throw new invalid_parameter_default(`Invalid general ranking type: ${type}`);
return {
period,
novelType
};
}
function parseGenreRankingType(type) {
const [periodStr, genreStr, novelTypeStr = NovelType$1.TOTAL] = type.split("_");
const period = periodStr;
const genre = Number(genreStr);
const novelType = novelTypeStr;
if (![
Object.values(RankingPeriod).includes(period),
Object.values(Genre).includes(genre),
Object.values(NovelType$1).includes(novelType),
genre !== Genre.SonotaReplay,
genre !== Genre.NonGenre
].every(Boolean)) throw new invalid_parameter_default(`Invalid genre ranking type: ${type}`);
return {
period,
genre,
novelType
};
}
async function handler(ctx) {
const { listType, type } = ctx.req.param();
const rankingType = listType;
const limit = Math.min(Number(ctx.req.query("limit") ?? 300), 300);
const api = new NarouNovelFetch();
const searchParams = {
gzip: 5,
lim: limit
};
let rankingUrl;
let rankingTitle;
switch (rankingType) {
case RankingType.LIST: {
const { period, novelType } = parseGeneralRankingType(type);
rankingUrl = `https://yomou.syosetu.com/rank/list/type/${type}`;
rankingTitle = `[${periodToJapanese[period]}] 総合ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`;
searchParams.order = periodToOrder[period];
if (novelType !== NovelType$1.TOTAL) searchParams.type = novelType;
break;
}
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]} BEST${limit}`;
searchParams.order = periodToOrder[period];
searchParams.genre = genre;
if (novelType !== NovelType$1.TOTAL) searchParams.type = novelType;
break;
}
case RankingType.ISEKAI: return handleIsekaiRanking(type, limit);
default: throw new invalid_parameter_default(`Invalid ranking type: ${type}`);
}
const items = (await new SearchBuilder(searchParams, api).execute()).values.map((novel, index) => ({
title: `#${index + 1} ${novel.title}`,
link: `https://ncode.syosetu.com/${String(novel.ncode).toLowerCase()}`,
description: art(path.join(__dirname, "templates/description-e1f33e28.art"), { novel }),
author: novel.writer,
category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean)
}));
return {
title: `小説家になろう - ${rankingTitle}`,
link: rankingUrl,
item: items,
language: "ja"
};
}
//#endregion
export { route };