UNPKG

rsshub

Version:
374 lines (361 loc) 13.8 kB
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 };