rsshub 1.0.0-master.f9ec483 → 1.0.0-master.fa1dcc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config.ts +42 -1
- package/lib/middleware/access-control.ts +3 -3
- package/lib/middleware/cache.ts +5 -3
- package/lib/middleware/template.test.ts +6 -0
- package/lib/middleware/template.tsx +3 -1
- package/lib/router.js +0 -6
- package/lib/routes/141jav/index.ts +1 -1
- package/lib/routes/141ppv/index.ts +1 -1
- package/lib/routes/36kr/hot-list.ts +1 -1
- package/lib/routes/36kr/index.ts +1 -1
- package/lib/routes/78dm/index.ts +429 -43
- package/lib/routes/78dm/namespace.ts +2 -0
- package/lib/routes/78dm/templates/description.art +17 -0
- package/lib/routes/abc/index.ts +10 -9
- package/lib/routes/aeon/category.ts +1 -1
- package/lib/routes/aeon/type.ts +1 -1
- package/lib/routes/afr/latest.ts +69 -0
- package/lib/routes/afr/namespace.ts +7 -0
- package/lib/routes/afr/navigation.ts +75 -0
- package/lib/routes/afr/query.ts +349 -0
- package/lib/routes/afr/utils.ts +80 -0
- package/lib/routes/aibase/discover.ts +1 -1
- package/lib/routes/aibase/news.ts +1 -1
- package/lib/routes/aibase/topic.ts +1 -1
- package/lib/routes/air-level/index.ts +49 -0
- package/lib/routes/air-level/levelrank.ts +65 -0
- package/lib/routes/air-level/namespace.ts +12 -0
- package/lib/routes/ajcass/namespace.ts +11 -0
- package/lib/routes/ajcass/shxyj.ts +73 -0
- package/lib/routes/alpinelinux/pkgs.ts +2 -2
- package/lib/routes/anime1/anime.ts +1 -1
- package/lib/routes/apnews/utils.ts +6 -2
- package/lib/routes/appleinsider/index.ts +1 -1
- package/lib/routes/aqara/region.ts +1 -1
- package/lib/routes/asus/bios.ts +0 -1
- package/lib/routes/baoyu/index.ts +57 -0
- package/lib/routes/baoyu/namespace.ts +8 -0
- package/lib/routes/bilibili/article.ts +37 -14
- package/lib/routes/bilibili/dynamic.ts +15 -7
- package/lib/routes/bilibili/followings-dynamic.ts +2 -2
- package/lib/routes/bilibili/manga-update.ts +3 -0
- package/lib/routes/bilibili/ranking.ts +188 -117
- package/lib/routes/bilibili/reply.ts +2 -0
- package/lib/routes/bilibili/templates/description.art +2 -0
- package/lib/routes/bing/daily-wallpaper.ts +5 -1
- package/lib/routes/bsky/posts.ts +0 -1
- package/lib/routes/bsky/templates/post.art +8 -1
- package/lib/routes/buaa/lib/space/newbook.ts +1 -1
- package/lib/routes/c114/namespace.ts +2 -0
- package/lib/routes/c114/roll.ts +71 -42
- package/lib/routes/cags/edu/index.ts +84 -0
- package/lib/routes/cags/namespace.ts +9 -0
- package/lib/routes/caixin/article.ts +1 -1
- package/lib/routes/caixin/blog.ts +2 -3
- package/lib/routes/caixin/category.ts +1 -1
- package/lib/routes/caixin/latest.ts +12 -16
- package/lib/routes/caixin/namespace.ts +1 -1
- package/lib/routes/caixin/utils-fulltext.ts +51 -0
- package/lib/routes/caixin/utils.ts +37 -37
- package/lib/routes/cara/likes.ts +1 -1
- package/lib/routes/cara/portfolio.ts +1 -1
- package/lib/routes/cara/timeline.ts +1 -1
- package/lib/routes/ccfa/index.ts +11 -11
- package/lib/routes/chub/characters.ts +1 -1
- package/lib/routes/cnbeta/namespace.ts +1 -1
- package/lib/routes/cngold/index.ts +195 -0
- package/lib/routes/cngold/namespace.ts +9 -0
- package/lib/routes/cnki/author.ts +120 -48
- package/lib/routes/cnki/utils.ts +4 -4
- package/lib/routes/coomer/artist.ts +1 -1
- package/lib/routes/coomer/namespace.ts +1 -1
- package/lib/routes/coomer/posts.ts +2 -2
- package/lib/routes/coomer/utils.ts +1 -1
- package/lib/routes/cpcaauto/index.ts +22 -22
- package/lib/routes/cs/zzkx.ts +1 -1
- package/lib/routes/csdn/blog.ts +17 -13
- package/lib/routes/cybersecurityventures/namespace.ts +7 -0
- package/lib/routes/cybersecurityventures/news.ts +121 -0
- package/lib/routes/cybersecurityventures/types.ts +17 -0
- package/lib/routes/daily/discussed.ts +22 -21
- package/lib/routes/daily/index.ts +14 -11
- package/lib/routes/daily/namespace.ts +1 -1
- package/lib/routes/daily/source.ts +186 -0
- package/lib/routes/daily/upvoted.ts +17 -17
- package/lib/routes/daily/user.ts +6 -23
- package/lib/routes/daily/utils.ts +27 -32
- package/lib/routes/dataguidance/index.ts +19 -30
- package/lib/routes/dcfever/news.ts +1 -1
- package/lib/routes/dcfever/reviews.ts +1 -1
- package/lib/routes/dcfever/trading-search.ts +1 -1
- package/lib/routes/dcfever/trading.ts +1 -1
- package/lib/routes/deeplearning/namespace.ts +3 -1
- package/lib/routes/deeplearning/templates/description.art +21 -0
- package/lib/routes/deeplearning/the-batch.ts +296 -0
- package/lib/routes/deepmind/blog.ts +1 -1
- package/lib/routes/digitalcameraworld/news.ts +1 -1
- package/lib/routes/discord/discord-api.ts +44 -2
- package/lib/routes/discord/search.ts +106 -0
- package/lib/routes/dn/news.ts +1 -1
- package/lib/routes/dockerhub/repositories.ts +42 -0
- package/lib/routes/dongqiudi/daily.ts +1 -1
- package/lib/routes/dw/namespace.ts +6 -0
- package/lib/routes/dw/news.ts +89 -0
- package/lib/routes/dw/rss.ts +62 -0
- package/lib/routes/dw/templates/description.art +23 -0
- package/lib/routes/dw/templates/liveblog.art +13 -0
- package/lib/routes/dw/templates/video.art +14 -0
- package/lib/routes/dw/utils.ts +172 -0
- package/lib/routes/ecnu/jwc.ts +5 -1
- package/lib/routes/espn/news.ts +1 -1
- package/lib/routes/fashionnetwork/namespace.ts +1 -1
- package/lib/routes/flyert/forum.ts +99 -0
- package/lib/routes/flyert/namespace.ts +1 -0
- package/lib/routes/flyert/templates/description.art +21 -0
- package/lib/routes/flyert/util.ts +188 -0
- package/lib/routes/follow/profile.ts +2 -2
- package/lib/routes/foresightnews/article.ts +1 -1
- package/lib/routes/foresightnews/column.ts +1 -1
- package/lib/routes/foresightnews/index.ts +3 -1
- package/lib/routes/foresightnews/news.ts +1 -1
- package/lib/routes/freecomputerbooks/index.ts +0 -10
- package/lib/routes/ftm/index.ts +1 -1
- package/lib/routes/furaffinity/art.ts +85 -0
- package/lib/routes/furaffinity/browse.ts +58 -0
- package/lib/routes/furaffinity/commissions.ts +55 -0
- package/lib/routes/furaffinity/home.ts +80 -0
- package/lib/routes/furaffinity/journal-comments.ts +64 -0
- package/lib/routes/furaffinity/journals.ts +56 -0
- package/lib/routes/furaffinity/namespace.ts +7 -0
- package/lib/routes/furaffinity/search.ts +72 -0
- package/lib/routes/furaffinity/shouts.ts +56 -0
- package/lib/routes/furaffinity/status.ts +58 -0
- package/lib/routes/furaffinity/submission-comments.ts +64 -0
- package/lib/{routes-deprecated/furaffinity/user.js → routes/furaffinity/user.ts} +53 -41
- package/lib/routes/furaffinity/watchers.ts +62 -0
- package/lib/routes/furaffinity/watching.ts +62 -0
- package/lib/routes/google/developers.ts +71 -0
- package/lib/routes/google/news.ts +26 -6
- package/lib/routes/gov/cbirc/index.ts +35 -35
- package/lib/routes/hamel/index.ts +81 -0
- package/lib/routes/hamel/namespace.ts +7 -0
- package/lib/routes/hellogithub/volume.ts +3 -0
- package/lib/routes/hket/index.ts +13 -1
- package/lib/routes/hostmonit/cloudflareyesv6.ts +1 -1
- package/lib/routes/hrbeu/sec/list.ts +80 -0
- package/lib/routes/hrbust/jwzx.ts +6 -0
- package/lib/routes/hunau/gfxy/index.ts +1 -1
- package/lib/routes/hunau/ied.ts +1 -1
- package/lib/routes/hunau/jwc.ts +1 -1
- package/lib/routes/hunau/xky/index.ts +1 -1
- package/lib/routes/hust/gs.ts +4 -3
- package/lib/routes/idolmaster/namespace.ts +7 -0
- package/lib/routes/idolmaster/news.ts +112 -0
- package/lib/routes/ieee/author.ts +95 -0
- package/lib/routes/ikea/cn/low-price.ts +2 -1
- package/lib/routes/imdb/chart.ts +1 -1
- package/lib/routes/inspirehep/author.ts +9 -1
- package/lib/routes/isct/namespace.ts +11 -0
- package/lib/routes/isct/news.ts +66 -0
- package/lib/routes/javtrailers/casts.ts +41 -0
- package/lib/routes/javtrailers/categories.ts +40 -0
- package/lib/routes/javtrailers/namespace.ts +7 -0
- package/lib/routes/javtrailers/studios.ts +39 -0
- package/lib/routes/javtrailers/templates/description.art +31 -0
- package/lib/routes/javtrailers/types.ts +59 -0
- package/lib/routes/javtrailers/utils.ts +48 -0
- package/lib/routes/jiemian/list.ts +1 -1
- package/lib/routes/jiuyangongshe/community.ts +1 -1
- package/lib/routes/jlu/phy/index.ts +55 -0
- package/lib/routes/joins/chinese.ts +13 -13
- package/lib/routes/kcna/news.ts +7 -5
- package/lib/routes/keep/user.ts +1 -1
- package/lib/routes/langchain/index.ts +74 -0
- package/lib/routes/langchain/namespace.ts +7 -0
- package/lib/routes/last-origin/namespace.ts +7 -0
- package/lib/routes/last-origin/news.ts +65 -0
- package/lib/routes/linkresearcher/index.ts +108 -46
- package/lib/routes/linkresearcher/namespace.ts +8 -2
- package/lib/routes/linkresearcher/templates/bilingual.art +7 -0
- package/lib/routes/linkresearcher/types.ts +103 -0
- package/lib/routes/liulinblog/itnews.ts +1 -1
- package/lib/routes/lofter/collection.ts +1 -1
- package/lib/routes/lofter/tag.ts +1 -1
- package/lib/routes/logrocket/index.ts +71 -0
- package/lib/routes/logrocket/namespace.ts +7 -0
- package/lib/routes/m-78/namespace.ts +7 -0
- package/lib/routes/m-78/news.ts +115 -0
- package/lib/routes/m-78/types.ts +13 -0
- package/lib/routes/mastodon/account-id.ts +1 -1
- package/lib/routes/mastodon/acct.ts +1 -1
- package/lib/routes/mastodon/timeline-local.ts +2 -2
- package/lib/routes/mastodon/timeline-remote.ts +2 -2
- package/lib/routes/mastodon/utils.ts +1 -1
- package/lib/routes/metacritic/index.ts +1 -1
- package/lib/routes/mi/utils.ts +1 -1
- package/lib/routes/mihoyo/bbs/official.ts +2 -1
- package/lib/routes/misskey/utils.ts +19 -1
- package/lib/routes/mittrchina/index.ts +2 -2
- package/lib/routes/natgeo/natgeo.ts +25 -24
- package/lib/routes/nbd/daily.ts +1 -1
- package/lib/routes/nielsberglund/index.ts +78 -0
- package/lib/routes/nielsberglund/namespace.ts +7 -0
- package/lib/routes/nytimes/rss.ts +57 -0
- package/lib/routes/openrice/chart.ts +68 -0
- package/lib/routes/openrice/templates/chart.art +9 -0
- package/lib/routes/openrice/voting.ts +84 -0
- package/lib/routes/oschina/news.ts +26 -7
- package/lib/routes/patreon/feed.ts +126 -0
- package/lib/routes/patreon/namespace.ts +7 -0
- package/lib/routes/patreon/templates/description.art +39 -0
- package/lib/routes/patreon/types.ts +387 -0
- package/lib/routes/pixiv/api/get-illust-detail.ts +22 -0
- package/lib/routes/pixiv/novel-api/content/nsfw.ts +73 -0
- package/lib/routes/pixiv/novel-api/content/sfw.ts +63 -0
- package/lib/routes/pixiv/novel-api/content/types.ts +254 -0
- package/lib/routes/pixiv/novel-api/content/utils.ts +133 -0
- package/lib/routes/pixiv/novel-api/series/nsfw.ts +84 -0
- package/lib/routes/pixiv/novel-api/series/sfw.ts +70 -0
- package/lib/routes/pixiv/novel-api/series/types.ts +94 -0
- package/lib/routes/pixiv/novel-api/user-novels/nsfw.ts +82 -0
- package/lib/routes/pixiv/novel-api/user-novels/sfw.ts +74 -0
- package/lib/routes/pixiv/novel-api/user-novels/types.ts +133 -0
- package/lib/routes/pixiv/novel-series.ts +52 -0
- package/lib/routes/pixiv/novels.ts +69 -44
- package/lib/routes/pixiv/utils.ts +3 -0
- package/lib/routes/plurk/anonymous.ts +1 -1
- package/lib/routes/plurk/hotlinks.ts +1 -1
- package/lib/routes/plurk/news.ts +1 -1
- package/lib/routes/plurk/search.ts +1 -1
- package/lib/routes/qstheory/index.ts +121 -0
- package/lib/routes/qstheory/magazine.ts +64 -0
- package/lib/routes/qstheory/namespace.ts +7 -0
- package/lib/routes/qstheory/utils.ts +25 -0
- package/lib/routes/ruc/ai.ts +1 -1
- package/lib/routes/scmp/coronavirus.ts +1 -1
- package/lib/routes/scu/jwc/tzgg.ts +75 -0
- package/lib/routes/scu/namespace.ts +7 -0
- package/lib/routes/scu/scupi/_utils.ts +62 -0
- package/lib/routes/scu/scupi/notice.ts +41 -0
- package/lib/routes/solidot/namespace.ts +1 -1
- package/lib/routes/stbu/jsjxy.ts +4 -1
- package/lib/routes/steam/search.ts +7 -0
- package/lib/routes/sycl/feeds.ts +1 -1
- package/lib/routes/syosetu/dev.ts +67 -0
- package/lib/routes/syosetu/index.ts +85 -0
- package/lib/routes/syosetu/namespace.ts +2 -2
- package/lib/routes/syosetu/ranking-isekai.ts +93 -0
- package/lib/routes/syosetu/ranking-r18.ts +192 -0
- package/lib/routes/syosetu/ranking.ts +285 -0
- package/lib/routes/syosetu/search.ts +161 -0
- package/lib/routes/syosetu/templates/description.art +73 -0
- package/lib/routes/syosetu/types/ranking-r18.ts +60 -0
- package/lib/routes/syosetu/types/ranking.ts +67 -0
- package/lib/routes/syosetu/types/search.ts +138 -0
- package/lib/routes/syosetu/utils.ts +50 -0
- package/lib/routes/telegram/channel.ts +31 -1
- package/lib/routes/telegram/scripts/get-telegram-session.mjs +16 -6
- package/lib/routes/telegram/stickerpack.ts +1 -1
- package/lib/routes/telegram/tglib/channel.ts +12 -9
- package/lib/routes/telegram/tglib/client.ts +13 -9
- package/lib/routes/test/index.ts +4 -0
- package/lib/routes/theblockbeats/index.ts +1 -1
- package/lib/routes/thegradient/index.ts +80 -0
- package/lib/routes/thegradient/namespace.ts +7 -0
- package/lib/routes/theinitium/app.ts +4 -0
- package/lib/routes/thepaper/channel.ts +1 -1
- package/lib/routes/thepaper/factpaper.ts +1 -1
- package/lib/routes/thepaper/featured.ts +1 -1
- package/lib/routes/thepaper/list.ts +1 -1
- package/lib/routes/thepaper/user.ts +174 -0
- package/lib/routes/tongji/sem/_utils.ts +65 -0
- package/lib/routes/tongji/sem/notice.ts +58 -0
- package/lib/routes/toutiao/a-bogus.ts +540 -0
- package/lib/routes/toutiao/namespace.ts +7 -0
- package/lib/routes/toutiao/templates/video.art +7 -0
- package/lib/routes/toutiao/types.ts +443 -0
- package/lib/routes/toutiao/user.ts +127 -0
- package/lib/routes/twitter/api/mobile-api/api.ts +8 -0
- package/lib/routes/twitter/api/web-api/utils.ts +21 -9
- package/lib/routes/twitter/namespace.ts +1 -0
- package/lib/routes/twitter/utils.ts +7 -1
- package/lib/routes/vertikal/latest.ts +73 -0
- package/lib/routes/vertikal/namespace.ts +7 -0
- package/lib/routes/vice/topic.ts +1 -1
- package/lib/routes/vocus/publication.ts +1 -1
- package/lib/routes/vocus/user.ts +1 -1
- package/lib/routes/wallstreetcn/calendar.ts +90 -0
- package/lib/routes/wallstreetcn/hot.ts +2 -2
- package/lib/routes/wallstreetcn/live.ts +1 -1
- package/lib/routes/wallstreetcn/news.ts +5 -3
- package/lib/routes/weibo/timeline.ts +3 -2
- package/lib/routes/wellcee/rent.ts +1 -1
- package/lib/routes/wufazhuce/namespace.ts +7 -0
- package/lib/routes/wufazhuce/one.ts +90 -0
- package/lib/routes/xiaoheihe/discount.ts +72 -28
- package/lib/routes/xiaohongshu/user.ts +40 -3
- package/lib/routes/xiaohongshu/util.ts +112 -105
- package/lib/routes/xsijishe/rank.ts +33 -7
- package/lib/routes/xueqiu/cookies.ts +13 -18
- package/lib/routes/yahoo/news/listid.ts +4 -1
- package/lib/routes/yahoo/news/provider-helper.ts +4 -1
- package/lib/routes/yahoo/news/provider.ts +4 -1
- package/lib/routes/yande/post.ts +13 -3
- package/lib/routes/youtube/user.ts +37 -25
- package/lib/routes/zhonglun/index.ts +5 -6
- package/lib/routes/zjut/cs/index.ts +1 -1
- package/lib/utils/got.ts +2 -1
- package/lib/utils/helpers.test.ts +13 -1
- package/lib/utils/helpers.ts +10 -0
- package/lib/utils/ofetch.ts +3 -0
- package/lib/utils/request-rewriter/fetch.test.ts +94 -0
- package/lib/utils/request-rewriter/fetch.ts +14 -1
- package/lib/views/error.tsx +1 -1
- package/package.json +40 -37
- package/lib/routes/78dm/templates/image.art +0 -1
- package/lib/routes/deeplearning/thebatch.ts +0 -74
- package/lib/routes/syosetu/chapter.ts +0 -87
- package/lib/routes/xiaohongshu/notes.ts +0 -39
- package/lib/routes-deprecated/furaffinity/browse.js +0 -44
- package/lib/routes-deprecated/furaffinity/commissions.js +0 -39
- package/lib/routes-deprecated/furaffinity/favorites.js +0 -56
- package/lib/routes-deprecated/furaffinity/gallery.js +0 -56
- package/lib/routes-deprecated/furaffinity/home.js +0 -72
- package/lib/routes-deprecated/furaffinity/journal-comments.js +0 -50
- package/lib/routes-deprecated/furaffinity/journals.js +0 -49
- package/lib/routes-deprecated/furaffinity/scraps.js +0 -56
- package/lib/routes-deprecated/furaffinity/search.js +0 -56
- package/lib/routes-deprecated/furaffinity/shouts.js +0 -40
- package/lib/routes-deprecated/furaffinity/status.js +0 -38
- package/lib/routes-deprecated/furaffinity/submission-comments.js +0 -50
- package/lib/routes-deprecated/furaffinity/watchers.js +0 -47
- package/lib/routes-deprecated/furaffinity/watching.js +0 -47
- package/lib/routes-deprecated/ieee/author.js +0 -30
- package/lib/routes-deprecated/qstheory/index.js +0 -122
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Route, Data, DataItem } from '@/types';
|
|
2
|
+
import { art } from '@/utils/render';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Context } from 'hono';
|
|
5
|
+
import { SearchBuilderR18, SearchParams, NarouNovelFetch } from 'narou';
|
|
6
|
+
import InvalidParameterError from '@/errors/types/invalid-parameter';
|
|
7
|
+
import { getCurrentPath } from '@/utils/helpers';
|
|
8
|
+
import { RankingPeriod, periodToJapanese, novelTypeToJapanese, periodToOrder, NovelType, SyosetuSub, syosetuSubToJapanese, syosetuSubToNocgenre } from './types/ranking-r18';
|
|
9
|
+
|
|
10
|
+
const __dirname = getCurrentPath(import.meta.url);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Implementation of "Syosetu" R18 Rankings
|
|
14
|
+
*
|
|
15
|
+
* While "Syosetu" only provides ranking API for "Syosetu o yomou" (general audience),
|
|
16
|
+
* equivalent ranking functionality can be achieved using the point-based sorting in the search API.
|
|
17
|
+
*
|
|
18
|
+
* This implementation utilizes the 'order' parameter (e.g., dailypoint, weeklypoint)
|
|
19
|
+
* of the search API to replicate ranking functionality across all Syosetu subsidiary sites.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const getParameters = () => {
|
|
23
|
+
// Generate options for sub parameter
|
|
24
|
+
const subOptions = Object.entries(SyosetuSub).map(([, value]) => ({
|
|
25
|
+
value,
|
|
26
|
+
label: syosetuSubToJapanese[value],
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Generate period options
|
|
30
|
+
const periodOptions = Object.entries(RankingPeriod).map(([key, value]) => ({
|
|
31
|
+
value,
|
|
32
|
+
label: `${periodToJapanese[value]} (${key})`,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// Generate novel type options
|
|
36
|
+
const novelTypeOptions = Object.entries(NovelType).map(([key, value]) => ({
|
|
37
|
+
value,
|
|
38
|
+
label: `${novelTypeToJapanese[value]} (${key})`,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
sub: {
|
|
43
|
+
description: 'Target site for R18 rankings',
|
|
44
|
+
options: subOptions,
|
|
45
|
+
},
|
|
46
|
+
type: {
|
|
47
|
+
description: 'Detailed ranking type (format: period_noveltype)',
|
|
48
|
+
options: periodOptions.flatMap((period) =>
|
|
49
|
+
novelTypeOptions.map((type) => ({
|
|
50
|
+
value: `${period.value}_${type.value}`,
|
|
51
|
+
label: `${period.label} ${type.label}`,
|
|
52
|
+
}))
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getBest5RadarItems = () =>
|
|
59
|
+
Object.entries(SyosetuSub).flatMap(([, domain]) =>
|
|
60
|
+
Object.values(RankingPeriod).map((period) => ({
|
|
61
|
+
title: `${syosetuSubToJapanese[domain]} ${periodToJapanese[period]}ランキング BEST5`,
|
|
62
|
+
source: [`${domain === SyosetuSub.MOONLIGHT_BL ? SyosetuSub.MOONLIGHT : domain}.syosetu.com/rank/${domain === SyosetuSub.MOONLIGHT_BL ? 'bltop' : 'top'}`],
|
|
63
|
+
target: `/rankingr18/${domain}/${period}_${NovelType.TOTAL}?limit=5`,
|
|
64
|
+
}))
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
export const route: Route = {
|
|
68
|
+
path: '/rankingr18/:sub/:type',
|
|
69
|
+
categories: ['reading'],
|
|
70
|
+
example: '/syosetu/rankingr18/noc/daily_total?limit=50',
|
|
71
|
+
parameters: getParameters(),
|
|
72
|
+
features: {
|
|
73
|
+
requireConfig: false,
|
|
74
|
+
requirePuppeteer: false,
|
|
75
|
+
antiCrawler: false,
|
|
76
|
+
supportBT: false,
|
|
77
|
+
supportPodcast: false,
|
|
78
|
+
supportScihub: false,
|
|
79
|
+
},
|
|
80
|
+
name: 'R18 Rankings',
|
|
81
|
+
url: 'syosetu.com/site/group',
|
|
82
|
+
maintainers: ['SnowAgar25'],
|
|
83
|
+
handler,
|
|
84
|
+
description: `
|
|
85
|
+
| Period | Description | 説明 |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| daily | Daily Ranking | 日間ランキング |
|
|
88
|
+
| weekly | Weekly Ranking | 週間ランキング |
|
|
89
|
+
| monthly | Monthly Ranking | 月間ランキング |
|
|
90
|
+
| quarter | Quarterly Ranking | 四半期ランキング |
|
|
91
|
+
| yearly | Yearly Ranking | 年間ランキング |
|
|
92
|
+
|
|
93
|
+
| Novel Type | Description | 説明 |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| total | All Works | 総合 |
|
|
96
|
+
| t | Short Stories | 短編 |
|
|
97
|
+
| r | Ongoing Series | 連載中 |
|
|
98
|
+
| er | Completed Series | 完結済 |
|
|
99
|
+
|
|
100
|
+
:::tip
|
|
101
|
+
Combine Period and Novel Type with \`_\`.
|
|
102
|
+
For example: \`daily_total\`, \`weekly_r\`, \`monthly_er\`
|
|
103
|
+
:::`,
|
|
104
|
+
radar: [
|
|
105
|
+
{
|
|
106
|
+
source: ['noc.syosetu.com/rank/list/type/:type'],
|
|
107
|
+
target: '/rankingr18/noc/:type',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
source: ['mid.syosetu.com/rank/list/type/:type'],
|
|
111
|
+
target: '/rankingr18/mid/:type',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
source: ['mnlt.syosetu.com/rank/list/type/:type'],
|
|
115
|
+
target: '/rankingr18/mnlt/:type',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
source: ['mnlt.syosetu.com/rank/bllist/type/:type'],
|
|
119
|
+
target: '/rankingr18/mnlt-bl/:type',
|
|
120
|
+
},
|
|
121
|
+
...getBest5RadarItems(),
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function parseRankingType(type: string): { period: RankingPeriod; novelType: NovelType } {
|
|
126
|
+
const [periodStr, novelTypeStr] = type.split('_');
|
|
127
|
+
|
|
128
|
+
const period = periodStr as RankingPeriod;
|
|
129
|
+
const novelType = novelTypeStr as NovelType;
|
|
130
|
+
|
|
131
|
+
const isValid = [Object.values(RankingPeriod).includes(period), Object.values(NovelType).includes(novelType)].every(Boolean);
|
|
132
|
+
|
|
133
|
+
if (!isValid) {
|
|
134
|
+
throw new InvalidParameterError(`Invalid ranking type: ${type}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
period: periodStr as RankingPeriod,
|
|
139
|
+
novelType: novelTypeStr as NovelType,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getRankingTitle(type: string, limit: number): string {
|
|
144
|
+
const { period, novelType } = parseRankingType(type);
|
|
145
|
+
return `${periodToJapanese[period]}${novelTypeToJapanese[novelType]}ランキング BEST${limit}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function handler(ctx: Context): Promise<Data> {
|
|
149
|
+
const { sub, type } = ctx.req.param();
|
|
150
|
+
const baseUrl = `https://${sub === SyosetuSub.MOONLIGHT_BL ? SyosetuSub.MOONLIGHT : sub}.syosetu.com`;
|
|
151
|
+
const rankingUrl = `${baseUrl}/rank/list/type/${type}`;
|
|
152
|
+
const api = new NarouNovelFetch();
|
|
153
|
+
|
|
154
|
+
const limit = Math.min(Number(ctx.req.query('limit') ?? 300), 300);
|
|
155
|
+
const { period, novelType } = parseRankingType(type);
|
|
156
|
+
|
|
157
|
+
const searchParams: SearchParams = {
|
|
158
|
+
gzip: 5,
|
|
159
|
+
lim: limit,
|
|
160
|
+
order: periodToOrder[period],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// TOTAL: Skip type filter to get all types combined
|
|
164
|
+
if (novelType !== NovelType.TOTAL) {
|
|
165
|
+
searchParams.type = novelType;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!(sub in syosetuSubToNocgenre)) {
|
|
169
|
+
throw new InvalidParameterError(`Invalid subsite: ${sub}`);
|
|
170
|
+
}
|
|
171
|
+
const nocgenre = syosetuSubToNocgenre[sub];
|
|
172
|
+
|
|
173
|
+
const builder = new SearchBuilderR18(searchParams, api).r18Site(nocgenre);
|
|
174
|
+
const result = await builder.execute();
|
|
175
|
+
|
|
176
|
+
const items = result.values.map((novel, index) => ({
|
|
177
|
+
title: `#${index + 1} ${novel.title}`,
|
|
178
|
+
link: `https://novel18.syosetu.com/${String(novel.ncode).toLowerCase()}`,
|
|
179
|
+
description: art(path.join(__dirname, 'templates', 'description.art'), {
|
|
180
|
+
novel,
|
|
181
|
+
}),
|
|
182
|
+
author: novel.writer,
|
|
183
|
+
category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean),
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
title: `小説家になろう (${sub}) - ${getRankingTitle(type, limit)}`,
|
|
188
|
+
link: rankingUrl,
|
|
189
|
+
item: items as DataItem[],
|
|
190
|
+
language: 'ja',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Route, Data, DataItem } from '@/types';
|
|
2
|
+
import { art } from '@/utils/render';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Context } from 'hono';
|
|
5
|
+
import { Genre, SearchBuilder, SearchParams, NarouNovelFetch, GenreNotation } from 'narou';
|
|
6
|
+
import InvalidParameterError from '@/errors/types/invalid-parameter';
|
|
7
|
+
import { getCurrentPath } from '@/utils/helpers';
|
|
8
|
+
import { handleIsekaiRanking } from './ranking-isekai';
|
|
9
|
+
import { RankingPeriod, periodToJapanese, novelTypeToJapanese, periodToOrder, RankingType, NovelType, isekaiCategoryToJapanese, IsekaiCategory } from './types/ranking';
|
|
10
|
+
|
|
11
|
+
const __dirname = getCurrentPath(import.meta.url);
|
|
12
|
+
|
|
13
|
+
const getParameters = () => {
|
|
14
|
+
// Generate ranking type options
|
|
15
|
+
const rankingTypeOptions = [
|
|
16
|
+
{ value: RankingType.LIST, label: '総合ランキング (General Ranking)' },
|
|
17
|
+
{ value: RankingType.GENRE, label: 'ジャンル別ランキング (Genre Ranking)' },
|
|
18
|
+
{ value: RankingType.ISEKAI, label: '異世界転生/転移ランキング (Isekai Ranking)' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Generate period options
|
|
22
|
+
const periodOptions = Object.entries(RankingPeriod).map(([key, value]) => ({
|
|
23
|
+
value,
|
|
24
|
+
label: `${periodToJapanese[value]} (${key})`,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Generate novel type options
|
|
28
|
+
const novelTypeOptions = Object.entries(NovelType).map(([key, value]) => ({
|
|
29
|
+
value,
|
|
30
|
+
label: `${novelTypeToJapanese[value]} (${key})`,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Generate genre options
|
|
34
|
+
const genreOptions = Object.entries(Genre)
|
|
35
|
+
.filter(([, value]) => typeof value === 'number') // Filter out reverse mappings
|
|
36
|
+
.map(([key, value]) => ({
|
|
37
|
+
value: value.toString(),
|
|
38
|
+
label: key,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Generate isekai category options
|
|
42
|
+
const isekaiOptions = Object.entries(IsekaiCategory).map(([key, value]) => ({
|
|
43
|
+
value,
|
|
44
|
+
label: `${isekaiCategoryToJapanese[value]} (${key})`,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
listType: {
|
|
49
|
+
description: 'Ranking type',
|
|
50
|
+
options: rankingTypeOptions,
|
|
51
|
+
},
|
|
52
|
+
type: {
|
|
53
|
+
description: 'Detailed ranking type, can be found in Syosetu ranking URLs',
|
|
54
|
+
options: [
|
|
55
|
+
// General ranking options
|
|
56
|
+
...periodOptions.flatMap((period) =>
|
|
57
|
+
novelTypeOptions.map((novelType) => ({
|
|
58
|
+
value: `${period.value}_${novelType.value}`,
|
|
59
|
+
label: `${RankingType.LIST} - [${periodToJapanese[period.value]}] 総合ランキング - ${novelTypeToJapanese[novelType.value]}`,
|
|
60
|
+
}))
|
|
61
|
+
),
|
|
62
|
+
// Genre ranking options
|
|
63
|
+
...periodOptions.flatMap((period) =>
|
|
64
|
+
genreOptions.flatMap((genre) =>
|
|
65
|
+
novelTypeOptions.map((novelType) => ({
|
|
66
|
+
value: `${period.value}_${genre.value}_${novelType.value}`,
|
|
67
|
+
label: `${RankingType.GENRE} - [${periodToJapanese[period.value]}] ${GenreNotation[genre.value]}ランキング - ${novelTypeToJapanese[novelType.value]}`,
|
|
68
|
+
}))
|
|
69
|
+
)
|
|
70
|
+
),
|
|
71
|
+
// Isekai ranking options
|
|
72
|
+
...periodOptions.flatMap((period) =>
|
|
73
|
+
isekaiOptions.flatMap((category) =>
|
|
74
|
+
novelTypeOptions.map((novelType) => ({
|
|
75
|
+
value: `${period.value}_${category.value}_${novelType.value}`,
|
|
76
|
+
label: `${RankingType.ISEKAI} - [${periodToJapanese[period.value]}] 異世界転生/転移${isekaiCategoryToJapanese[category.value]}ランキング - ${novelTypeToJapanese[novelType.value]}`,
|
|
77
|
+
}))
|
|
78
|
+
)
|
|
79
|
+
),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const getBest5RadarItems = () => {
|
|
86
|
+
// List
|
|
87
|
+
const periodRankings = Object.values(RankingPeriod).map((period) => ({
|
|
88
|
+
title: `${periodToJapanese[period]}ランキング BEST5`,
|
|
89
|
+
source: ['yomou.syosetu.com/rank/top/'],
|
|
90
|
+
target: `/ranking/list/${period}_total?limit=5`,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// Genre
|
|
94
|
+
const genreRankings = Object.entries(Genre)
|
|
95
|
+
.filter(([, value]) => typeof value === 'number' && value !== Genre.SonotaReplay && value !== Genre.NonGenre)
|
|
96
|
+
.map(([, value]) => ({
|
|
97
|
+
title: `[${periodToJapanese.daily}] ${GenreNotation[value]}ランキング BEST5`,
|
|
98
|
+
source: ['yomou.syosetu.com/rank/top/'],
|
|
99
|
+
target: `/ranking/genre/daily_${value}_total?limit=5`,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
// Isekai
|
|
103
|
+
const isekaiRankings = Object.values(IsekaiCategory).map((category) => ({
|
|
104
|
+
title: `[${periodToJapanese.daily}] 異世界転生/転移${isekaiCategoryToJapanese[category]}ランキング BEST5`,
|
|
105
|
+
source: ['yomou.syosetu.com/rank/top/'],
|
|
106
|
+
target: `/ranking/isekai/daily_${category}_total?limit=5`,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
return [...periodRankings, ...genreRankings, ...isekaiRankings];
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const route: Route = {
|
|
113
|
+
path: '/ranking/:listType/:type',
|
|
114
|
+
categories: ['reading'],
|
|
115
|
+
example: '/syosetu/ranking/list/daily_total?limit=50',
|
|
116
|
+
parameters: getParameters(),
|
|
117
|
+
features: {
|
|
118
|
+
requireConfig: false,
|
|
119
|
+
requirePuppeteer: false,
|
|
120
|
+
antiCrawler: false,
|
|
121
|
+
supportBT: false,
|
|
122
|
+
supportPodcast: false,
|
|
123
|
+
supportScihub: false,
|
|
124
|
+
},
|
|
125
|
+
name: 'Rankings',
|
|
126
|
+
url: 'yomou.syosetu.com/rank/top',
|
|
127
|
+
maintainers: ['SnowAgar25'],
|
|
128
|
+
handler,
|
|
129
|
+
description: `
|
|
130
|
+
| Keyword | Description | 説明 |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| list | Overall Ranking | 総合ランキング |
|
|
133
|
+
| genre | Genre Ranking | ジャンル別ランキング |
|
|
134
|
+
| isekai | Isekai/Reincarnation/Transfer Ranking | 異世界転生/転移ランキング |
|
|
135
|
+
|
|
136
|
+
| Period | Description |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| daily | Daily Ranking |
|
|
139
|
+
| weekly | Weekly Ranking |
|
|
140
|
+
| monthly | Monthly Ranking |
|
|
141
|
+
| quarter | Quarterly Ranking |
|
|
142
|
+
| yearly | Yearly Ranking |
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
| Type | Description |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| total | All Works |
|
|
148
|
+
| t | Short Stories |
|
|
149
|
+
| r | Ongoing Series |
|
|
150
|
+
| er | Completed Series |
|
|
151
|
+
|
|
152
|
+
:::warning
|
|
153
|
+
Please note that novel type options may vary depending on the ranking category.
|
|
154
|
+
|
|
155
|
+
ランキングの種類によって、小説タイプが異なる場合がございますのでご注意ください。
|
|
156
|
+
:::
|
|
157
|
+
|
|
158
|
+
:::danger 注意事項
|
|
159
|
+
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.
|
|
160
|
+
|
|
161
|
+
「注目度ランキング」については、API が非公開で検索 API でも同様の結果を得ることができないため、本 Route ではサポートしておりません。
|
|
162
|
+
:::
|
|
163
|
+
|
|
164
|
+
:::tip 異世界転生/転移ランキングについて (Isekai)
|
|
165
|
+
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.
|
|
166
|
+
|
|
167
|
+
集計の結果、同じポイントの作品が複数存在する場合、Syosetu ではランダムで順位が決定されるため、本 Route の順位と異なる場合があります。
|
|
168
|
+
:::
|
|
169
|
+
`,
|
|
170
|
+
radar: [
|
|
171
|
+
{
|
|
172
|
+
source: ['yomou.syosetu.com/rank/list/type/:type'],
|
|
173
|
+
target: '/ranking/list/:type',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
source: ['yomou.syosetu.com/rank/genrelist/type/:type'],
|
|
177
|
+
target: '/ranking/genre/:type',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
source: ['yomou.syosetu.com/rank/isekailist/type/:type'],
|
|
181
|
+
target: '/ranking/isekai/:type',
|
|
182
|
+
},
|
|
183
|
+
...getBest5RadarItems(),
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
function parseGeneralRankingType(type: string): { period: RankingPeriod; novelType: NovelType } {
|
|
188
|
+
const [periodStr, novelTypeStr] = type.split('_');
|
|
189
|
+
|
|
190
|
+
const period = periodStr as RankingPeriod;
|
|
191
|
+
const novelType = novelTypeStr as NovelType;
|
|
192
|
+
|
|
193
|
+
const isValid = [Object.values(RankingPeriod).includes(period), Object.values(NovelType).includes(novelType)].every(Boolean);
|
|
194
|
+
|
|
195
|
+
if (!isValid) {
|
|
196
|
+
throw new InvalidParameterError(`Invalid general ranking type: ${type}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { period, novelType };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseGenreRankingType(type: string): { period: RankingPeriod; genre: number; novelType: NovelType } {
|
|
203
|
+
const [periodStr, genreStr, novelTypeStr = NovelType.TOTAL] = type.split('_');
|
|
204
|
+
|
|
205
|
+
const period = periodStr as RankingPeriod;
|
|
206
|
+
const genre = Number(genreStr) as Genre;
|
|
207
|
+
const novelType = novelTypeStr as NovelType;
|
|
208
|
+
|
|
209
|
+
const isValid = [Object.values(RankingPeriod).includes(period), Object.values(Genre).includes(genre), Object.values(NovelType).includes(novelType), genre !== Genre.SonotaReplay, genre !== Genre.NonGenre].every(Boolean);
|
|
210
|
+
|
|
211
|
+
if (!isValid) {
|
|
212
|
+
throw new InvalidParameterError(`Invalid genre ranking type: ${type}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { period, genre, novelType };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function handler(ctx: Context): Promise<Data> {
|
|
219
|
+
const { listType, type } = ctx.req.param();
|
|
220
|
+
const rankingType = listType as RankingType;
|
|
221
|
+
const limit = Math.min(Number(ctx.req.query('limit') ?? 300), 300);
|
|
222
|
+
|
|
223
|
+
const api = new NarouNovelFetch();
|
|
224
|
+
const searchParams: SearchParams = {
|
|
225
|
+
gzip: 5,
|
|
226
|
+
lim: limit,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
let rankingUrl: string;
|
|
230
|
+
let rankingTitle: string;
|
|
231
|
+
|
|
232
|
+
// Build search parameters and titles based on ranking type
|
|
233
|
+
switch (rankingType) {
|
|
234
|
+
case RankingType.LIST: {
|
|
235
|
+
const { period, novelType } = parseGeneralRankingType(type);
|
|
236
|
+
rankingUrl = `https://yomou.syosetu.com/rank/list/type/${type}`;
|
|
237
|
+
rankingTitle = `[${periodToJapanese[period]}] 総合ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`;
|
|
238
|
+
|
|
239
|
+
searchParams.order = periodToOrder[period];
|
|
240
|
+
if (novelType !== NovelType.TOTAL) {
|
|
241
|
+
searchParams.type = novelType;
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case RankingType.GENRE: {
|
|
247
|
+
const { period, genre, novelType } = parseGenreRankingType(type);
|
|
248
|
+
rankingUrl = `https://yomou.syosetu.com/rank/genrelist/type/${type}`;
|
|
249
|
+
rankingTitle = `[${periodToJapanese[period]}] ${GenreNotation[genre]}ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`;
|
|
250
|
+
|
|
251
|
+
searchParams.order = periodToOrder[period];
|
|
252
|
+
searchParams.genre = genre as Genre;
|
|
253
|
+
if (novelType !== NovelType.TOTAL) {
|
|
254
|
+
searchParams.type = novelType;
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case RankingType.ISEKAI:
|
|
260
|
+
return handleIsekaiRanking(type, limit);
|
|
261
|
+
|
|
262
|
+
default:
|
|
263
|
+
throw new InvalidParameterError(`Invalid ranking type: ${type}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const builder = new SearchBuilder(searchParams, api);
|
|
267
|
+
const result = await builder.execute();
|
|
268
|
+
|
|
269
|
+
const items = result.values.map((novel, index) => ({
|
|
270
|
+
title: `#${index + 1} ${novel.title}`,
|
|
271
|
+
link: `https://ncode.syosetu.com/${String(novel.ncode).toLowerCase()}`,
|
|
272
|
+
description: art(path.join(__dirname, 'templates', 'description.art'), {
|
|
273
|
+
novel,
|
|
274
|
+
}),
|
|
275
|
+
author: novel.writer,
|
|
276
|
+
category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean),
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
title: `小説家になろう - ${rankingTitle}`,
|
|
281
|
+
link: rankingUrl,
|
|
282
|
+
item: items as DataItem[],
|
|
283
|
+
language: 'ja',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Route, Data } from '@/types';
|
|
2
|
+
import { art } from '@/utils/render';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Context } from 'hono';
|
|
5
|
+
import { Genre, GenreNotation, NarouNovelFetch, NovelTypeParam, Order, R18Site, SearchBuilder, SearchBuilderR18, SearchParams } from 'narou';
|
|
6
|
+
import queryString from 'query-string';
|
|
7
|
+
import { Join } from 'narou/util/type';
|
|
8
|
+
import InvalidParameterError from '@/errors/types/invalid-parameter';
|
|
9
|
+
import { SyosetuSub, NarouSearchParams, syosetuSubToJapanese } from './types/search';
|
|
10
|
+
|
|
11
|
+
import { getCurrentPath } from '@/utils/helpers';
|
|
12
|
+
const __dirname = getCurrentPath(import.meta.url);
|
|
13
|
+
|
|
14
|
+
export const route: Route = {
|
|
15
|
+
path: '/search/:sub/:query',
|
|
16
|
+
categories: ['reading'],
|
|
17
|
+
example: '/syosetu/search/noc/word=ハーレム¬word=&type=r&mintime=&maxtime=&minlen=30000&maxlen=&min_globalpoint=&max_globalpoint=&minlastup=&maxlastup=&minfirstup=&maxfirstup=&isgl=1¬bl=1&order=new?limit=5',
|
|
18
|
+
parameters: {
|
|
19
|
+
sub: {
|
|
20
|
+
description: 'The target Syosetu subsite.',
|
|
21
|
+
options: Object.entries(SyosetuSub).map(([, value]) => ({
|
|
22
|
+
value,
|
|
23
|
+
label: syosetuSubToJapanese[value],
|
|
24
|
+
})),
|
|
25
|
+
},
|
|
26
|
+
query: 'Search parameters in Syosetu format.',
|
|
27
|
+
},
|
|
28
|
+
features: {
|
|
29
|
+
requireConfig: false,
|
|
30
|
+
requirePuppeteer: false,
|
|
31
|
+
antiCrawler: false,
|
|
32
|
+
supportBT: false,
|
|
33
|
+
supportPodcast: false,
|
|
34
|
+
supportScihub: false,
|
|
35
|
+
},
|
|
36
|
+
name: 'Search',
|
|
37
|
+
maintainers: ['SnowAgar25'],
|
|
38
|
+
handler,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const setIfExists = (value) => value ?? undefined;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This function converts query string generated by Syosetu website into API-compatible format.
|
|
45
|
+
* It is not intended for users to freely adjust values.
|
|
46
|
+
*
|
|
47
|
+
* @see https://deflis.github.io/node-narou/index.html
|
|
48
|
+
* @see https://dev.syosetu.com/man/api/
|
|
49
|
+
*/
|
|
50
|
+
function mapToSearchParams(query: string, limit: number): SearchParams {
|
|
51
|
+
const params = queryString.parse(query) as NarouSearchParams;
|
|
52
|
+
|
|
53
|
+
const searchParams: SearchParams = {
|
|
54
|
+
gzip: 5,
|
|
55
|
+
lim: limit,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
searchParams.word = setIfExists(params.word);
|
|
59
|
+
searchParams.notword = setIfExists(params.notword);
|
|
60
|
+
|
|
61
|
+
searchParams.title = setIfExists(params.title);
|
|
62
|
+
searchParams.ex = setIfExists(params.ex);
|
|
63
|
+
searchParams.keyword = setIfExists(params.keyword);
|
|
64
|
+
searchParams.wname = setIfExists(params.wname);
|
|
65
|
+
|
|
66
|
+
searchParams.sasie = setIfExists(params.sasie);
|
|
67
|
+
searchParams.iszankoku = setIfExists(params.iszankoku);
|
|
68
|
+
searchParams.isbl = setIfExists(params.isbl);
|
|
69
|
+
searchParams.isgl = setIfExists(params.isgl);
|
|
70
|
+
searchParams.istensei = setIfExists(params.istensei);
|
|
71
|
+
searchParams.istenni = setIfExists(params.istenni);
|
|
72
|
+
|
|
73
|
+
searchParams.stop = setIfExists(params.stop);
|
|
74
|
+
searchParams.notzankoku = setIfExists(params.notzankoku);
|
|
75
|
+
searchParams.notbl = setIfExists(params.notbl);
|
|
76
|
+
searchParams.notgl = setIfExists(params.notgl);
|
|
77
|
+
searchParams.nottensei = setIfExists(params.nottensei);
|
|
78
|
+
searchParams.nottenni = setIfExists(params.nottenni);
|
|
79
|
+
|
|
80
|
+
searchParams.minlen = setIfExists(params.minlen);
|
|
81
|
+
searchParams.maxlen = setIfExists(params.maxlen);
|
|
82
|
+
|
|
83
|
+
searchParams.type = setIfExists(params.type as NovelTypeParam);
|
|
84
|
+
searchParams.order = setIfExists(params.order as Order);
|
|
85
|
+
searchParams.genre = setIfExists(params.genre as Join<Genre> | Genre);
|
|
86
|
+
searchParams.nocgenre = setIfExists(params.nocgenre as Join<R18Site> | R18Site);
|
|
87
|
+
|
|
88
|
+
if (params.mintime || params.maxtime) {
|
|
89
|
+
searchParams.time = `${params.mintime || ''}-${params.maxtime || ''}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return searchParams;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isGeneral = (sub: string): boolean => sub === SyosetuSub.YOMOU;
|
|
96
|
+
|
|
97
|
+
function createNovelSearchBuilder(sub: string, searchParams: SearchParams) {
|
|
98
|
+
if (isGeneral(sub)) {
|
|
99
|
+
return new SearchBuilder(searchParams, new NarouNovelFetch());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const r18Params = { ...searchParams };
|
|
103
|
+
|
|
104
|
+
switch (sub) {
|
|
105
|
+
case SyosetuSub.NOCTURNE:
|
|
106
|
+
r18Params.nocgenre = R18Site.Nocturne;
|
|
107
|
+
break;
|
|
108
|
+
case SyosetuSub.MOONLIGHT:
|
|
109
|
+
// If either 女性向け/BL is chosen, nocgenre will be in query string
|
|
110
|
+
// If no specific genre selected, include both
|
|
111
|
+
if (!r18Params.nocgenre) {
|
|
112
|
+
r18Params.nocgenre = [R18Site.MoonLight, R18Site.MoonLightBL].join('-') as Join<R18Site>;
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
case SyosetuSub.MIDNIGHT:
|
|
116
|
+
r18Params.nocgenre = R18Site.Midnight;
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
throw new InvalidParameterError('Invalid Syosetu subsite.\nValid subsites are: yomou, noc, mnlt, mid');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return new SearchBuilderR18(r18Params, new NarouNovelFetch());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function handler(ctx: Context): Promise<Data> {
|
|
126
|
+
const { sub, query } = ctx.req.param();
|
|
127
|
+
const searchUrl = `https://${sub}.syosetu.com/search/search/search.php?${query}`;
|
|
128
|
+
|
|
129
|
+
const limit = Math.min(Number(ctx.req.query('limit') ?? 40), 40);
|
|
130
|
+
const searchParams = mapToSearchParams(query, limit);
|
|
131
|
+
const builder = createNovelSearchBuilder(sub, searchParams);
|
|
132
|
+
const result = await builder.execute();
|
|
133
|
+
|
|
134
|
+
const items = result.values.map((novel) => ({
|
|
135
|
+
title: novel.title,
|
|
136
|
+
link: `https://${isGeneral(sub) ? 'ncode' : 'novel18'}.syosetu.com/${String(novel.ncode).toLowerCase()}`,
|
|
137
|
+
description: art(path.join(__dirname, 'templates', 'description.art'), {
|
|
138
|
+
novel,
|
|
139
|
+
genreText: GenreNotation[novel.genre],
|
|
140
|
+
}),
|
|
141
|
+
// Skip pubDate - search results prioritize search sequence over timestamps
|
|
142
|
+
// pubDate: novel.general_lastup,
|
|
143
|
+
author: novel.writer,
|
|
144
|
+
// Split by whitespace characters(\s), slash(/), full-width slash(/)
|
|
145
|
+
category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean),
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
const searchTerms: string[] = [];
|
|
149
|
+
if (searchParams.word) {
|
|
150
|
+
searchTerms.push(searchParams.word);
|
|
151
|
+
}
|
|
152
|
+
if (searchParams.notword) {
|
|
153
|
+
searchTerms.push(`-${searchParams.notword}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
title: searchTerms.length > 0 ? `Syosetu Search: ${searchTerms.join(' ')}` : 'Syosetu Search',
|
|
158
|
+
link: searchUrl,
|
|
159
|
+
item: items,
|
|
160
|
+
};
|
|
161
|
+
}
|