UNPKG

narou

Version:
1 lines 90.7 kB
{"version":3,"sources":["../src/index.browser.ts","../src/narou-search-results.ts","../src/narou.ts","../src/util/jsonp.ts","../src/narou-jsonp.ts","../src/params.ts","../src/search-builder.ts","../src/search-builder-r18.ts","../src/util/date.ts","../src/ranking.ts","../src/ranking-history.ts","../src/user-search.ts"],"sourcesContent":["import type NarouNovel from \"./narou.js\";\nimport NarouNovelJsonp from \"./narou-jsonp.js\";\nimport SearchBuilder from \"./search-builder.js\";\nimport SearchBuilderR18 from \"./search-builder-r18.js\";\nimport RankingBuilder from \"./ranking.js\";\nimport { formatRankingHistory, RankingHistoryResult } from \"./ranking-history.js\";\nimport UserSearchBuilder from \"./user-search.js\";\n\nexport * from \"./index.common.js\";\nexport { NarouNovelJsonp };\n\nconst narouNovelJsonp = new NarouNovelJsonp();\n\n/**\n * なろう小説 API で小説を検索する\n * @param {string} [word] 検索ワード\n * @returns {SearchBuilder}\n * @see https://dev.syosetu.com/man/api/\n */\nexport function search(\n word = \"\",\n api: NarouNovel = narouNovelJsonp\n): SearchBuilder {\n const builder = new SearchBuilder({}, api);\n if (word != \"\") builder.word(word);\n return builder;\n}\n\n/**\n * 18禁小説 API で小説を検索する\n * @param {string} [word] 検索ワード\n * @returns {SearchBuilder}\n * @see https://dev.syosetu.com/xman/api/\n */\nexport function searchR18(\n word = \"\",\n api: NarouNovel = narouNovelJsonp\n): SearchBuilderR18 {\n const builder = new SearchBuilderR18({}, api);\n if (word != \"\") builder.word(word);\n return builder;\n}\n\n/**\n * なろうユーザ検索 API でユーザを検索する\n * @param {string} [word] - 検索ワード\n * @returns {UserSearchBuilder}\n * @see https://dev.syosetu.com/man/userapi/\n */\nexport function searchUser(word = \"\", api: NarouNovel = narouNovelJsonp) {\n const builder = new UserSearchBuilder({}, api);\n if (word != \"\") builder.word(word);\n return builder;\n}\n\n\n/**\n * なろう小説ランキング API でランキングを取得する\n * @returns {RankingBuilder}\n * @see https://dev.syosetu.com/man/rankapi/\n */\nexport function ranking(api: NarouNovel = narouNovelJsonp): RankingBuilder {\n const builder = new RankingBuilder({}, api);\n return builder;\n}\n\n/**\n * なろう殿堂入り API でランキング履歴を取得する\n * @param {string} ncode 小説のNコード\n * @see https://dev.syosetu.com/man/rankinapi/\n */\nexport async function rankingHistory(\n ncode: string,\n api: NarouNovel = narouNovelJsonp\n): Promise<RankingHistoryResult[]> {\n const result = await api.executeRankingHistory({ ncode });\n if (Array.isArray(result)) {\n return result.map(formatRankingHistory);\n } else {\n throw new Error(result);\n }\n}\n\nexport default {\n search,\n searchR18,\n searchUser,\n ranking,\n rankingHistory,\n};\n","import type {\n BooleanNumber as BooleanNumber,\n Genre,\n R18Site,\n SearchParams,\n Fields,\n BigGenre,\n R18Fields,\n OptionalFields,\n UserFields,\n UserSearchParams,\n} from \"./params.js\";\n\n/**\n * なろう小説API検索結果\n */\nexport default class NarouSearchResults<T, TKey extends keyof T> {\n /**\n * 検索結果数\n */\n allcount: number;\n /**\n * 結果表示上限数\n */\n limit: number;\n /**\n * 結果表示開始数\n */\n start: number;\n /**\n * 結果表示の現在ページ(=start/limit)\n */\n page: number;\n /**\n * 今回取得できた検索結果の数\n */\n length: number;\n /**\n * 検索結果\n */\n values: readonly Pick<T, TKey>[];\n\n /**\n * @constractor\n * @private\n */\n constructor(\n [header, ...result]: [{ allcount: number }, ...Pick<T, TKey>[]],\n params: SearchParams | UserSearchParams\n ) {\n const count = header.allcount;\n const limit = params.lim ?? 20;\n const start = params.st ?? 0;\n\n this.allcount = count;\n this.limit = limit;\n this.start = start;\n this.page = start / limit;\n this.length = result.length;\n this.values = result;\n }\n}\n\n/**\n * 小説情報\n * @see https://dev.syosetu.com/man/api/#output\n * @see https://dev.syosetu.com/xman/api/#output\n */\nexport interface NarouSearchResult {\n /** 小説名 */\n title: string;\n /** Nコード */\n ncode: string;\n /** 作者のユーザID(数値) */\n userid: number;\n /** 作者名 */\n writer: string;\n /** 小説のあらすじ */\n story: string;\n /** 掲載サイト */\n nocgenre: R18Site;\n /** 大ジャンル */\n biggenre: BigGenre;\n /** ジャンル */\n genre: Genre;\n /** キーワード */\n keyword: string;\n /** 初回掲載日 YYYY-MM-DD HH:MM:SSの形式 */\n general_firstup: string;\n /** 最終掲載日 YYYY-MM-DD HH:MM:SSの形式 */\n general_lastup: string;\n /** 連載の場合は1、短編の場合は2 */\n novel_type: NovelType;\n /** 連載の場合は1、短編の場合は2 */\n noveltype: NovelType;\n /** 短編小説と完結済小説は0となっています。連載中は1です。 */\n end: End;\n /** 全掲載話数です。短編の場合は1です。 */\n general_all_no: number;\n /** 小説文字数です。スペースや改行は文字数としてカウントしません。 */\n length: number;\n /** 読了時間(分単位)です。読了時間は小説文字数÷500を切り上げした数値です。 */\n time: number;\n /** 長期連載中は1、それ以外は0です。 */\n isstop: BooleanNumber;\n /** 登録必須キーワードに「R15」が含まれる場合は1、それ以外は0です。 */\n isr15: BooleanNumber;\n /** 登録必須キーワードに「ボーイズラブ」が含まれる場合は1、それ以外は0です。 */\n isbl: BooleanNumber;\n /** 登録必須キーワードに「ガールズラブ」が含まれる場合は1、それ以外は0です。 */\n isgl: BooleanNumber;\n /** 登録必須キーワードに「残酷な描写あり」が含まれる場合は1、それ以外は0です。 */\n iszankoku: BooleanNumber;\n /** 登録必須キーワードに「異世界転生」が含まれる場合は1、それ以外は0です。 */\n istensei: BooleanNumber;\n /** 登録必須キーワードに「異世界転移」が含まれる場合は1、それ以外は0です。 */\n istenni: BooleanNumber;\n /** 総合得点(=(ブックマーク数×2)+評価点) */\n global_point: number;\n /**\n * 日間ポイント\n * ランキング集計時点から過去24時間以内で新たに登録されたブックマークや評価が対象\n */\n daily_point: number;\n /**\n * 週間ポイント\n * ランキング集計時点から過去7日以内で新たに登録されたブックマークや評価が対象\n */\n weekly_point: number;\n /**\n * 月間ポイント\n * ランキング集計時点から過去30日以内で新たに登録されたブックマークや評価が対象\n */\n monthly_point: number;\n /**\n * 四半期ポイント\n * ランキング集計時点から過去90日以内で新たに登録されたブックマークや評価が対象\n */\n quarter_point: number;\n /**\n * 年間ポイント\n * ランキング集計時点から過去365日以内で新たに登録されたブックマークや評価が対象\n */\n yearly_point: number;\n /** ブックマーク数 */\n fav_novel_cnt: number;\n /** 感想数 */\n impression_cnt: number;\n /** レビュー数 */\n review_cnt: number;\n /** 評価ポイント */\n all_point: number;\n /** 評価者数 */\n all_hyoka_cnt: number;\n /** 挿絵の数 */\n sasie_cnt: number;\n /**\n * 会話率\n * @see https://dev.syosetu.com/man/kaiwa/\n */\n kaiwaritu: number;\n /**\n * 小説の更新日時\n */\n novelupdated_at: string;\n /**\n * 最終更新日時\n * システム用で小説更新時とは関係ありません\n */\n updated_at: string;\n /** 週間ユニークユーザー数 */\n weekly_unique: number;\n}\n\n/**\n * ユーザ情報\n * @see https://dev.syosetu.com/man/userapi/#output\n */\nexport interface UserSearchResult {\n /** ユーザID */\n userid: number;\n /** ユーザ名 */\n name: string;\n /** ユーザ名のフリガナ */\n yomikata: string;\n /**\n * ユーザ名のフリガナの頭文字\n * ひらがな以外の場合はnullまたは空文字となります。\n */\n name1st: string;\n /** 小説投稿数 */\n novel_cnt: number;\n /** レビュー投稿数 */\n review_cnt: number;\n /**\n * 小説累計文字数\n * スペースや改行は文字数としてカウントしません。\n */\n novel_length: number;\n /**\n * 総合評価ポイントの合計\n * 投稿済小説でそれぞれ獲得した総合評価ポイントの合計です。\n */\n sum_global_point: number;\n}\n\n/**\n * noveltype/novel_typeの値ヘルパー\n */\nexport const NovelType = {\n /** 連載 */\n Rensai: 1,\n /** 短編 */\n Tanpen: 2,\n} as const;\nexport type NovelType = typeof NovelType[keyof typeof NovelType];\n\n/**\n * endの値ヘルパー\n */\nexport const End = {\n /** 短編小説と完結済小説 */\n KanketsuOrTanpen: 0,\n /** 連載中 */\n Rensai: 1,\n} as const;\nexport type End = typeof End[keyof typeof End];\n\nexport type SearchResultFields<T extends Fields> = {\n [K in keyof typeof Fields]: typeof Fields[K] extends T ? K : never;\n}[keyof typeof Fields];\n\nexport type SearchResultOptionalFields<T extends OptionalFields> = {\n [K in keyof typeof OptionalFields]: typeof OptionalFields[K] extends T\n ? K\n : never;\n}[keyof typeof OptionalFields];\n\nexport type SearchResultR18Fields<T extends R18Fields> = {\n [K in keyof typeof R18Fields]: typeof R18Fields[K] extends T ? K : never;\n}[keyof typeof R18Fields];\n\nexport type UserSearchResultFields<T extends UserFields> = {\n [K in keyof typeof UserFields]: typeof UserFields[K] extends T ? K : never;\n}[keyof typeof UserFields];\n\nexport type PickedNarouSearchResult<T extends keyof NarouSearchResult> = Pick<\n NarouSearchResult,\n T\n>;\n","import type { NarouRankingResult } from \"./narou-ranking-results.js\";\nimport NarouSearchResults from \"./narou-search-results.js\";\nimport type {\n NarouSearchResult,\n UserSearchResult,\n} from \"./narou-search-results.js\";\nimport type {\n RankingHistoryParams,\n RankingParams,\n SearchParams,\n UserSearchParams,\n} from \"./params.js\";\nimport type { RankingHistoryRawResult } from \"./ranking-history.js\";\n\n/**\n * なろう小説APIへのリクエストパラメータ\n */\nexport type NarouParams =\n | SearchParams\n | RankingParams\n | RankingHistoryParams\n | UserSearchParams;\n\n/**\n * なろう小説APIへのリクエストを実行する\n * @class NarouNovel\n * @private\n */\nexport default abstract class NarouNovel {\n /**\n * なろうAPIへのAPIリクエストを実行する\n * @param params クエリパラメータ\n * @param endpoint APIエンドポイント\n * @returns 実行結果\n */\n protected abstract execute<T>(\n params: NarouParams,\n endpoint: string\n ): Promise<T>;\n\n /**\n * APIへの検索リクエストを実行する\n * @param params クエリパラメータ\n * @param endpoint APIエンドポイント\n * @returns 検索結果\n */\n protected async executeSearch<T extends keyof NarouSearchResult>(\n params: SearchParams,\n endpoint = \"https://api.syosetu.com/novelapi/api/\"\n ): Promise<NarouSearchResults<NarouSearchResult, T>> {\n return new NarouSearchResults(await this.execute(params, endpoint), params);\n }\n\n /**\n * 小説APIへの検索リクエストを実行する\n * @param params クエリパラメータ\n * @returns 検索結果\n * @see https://dev.syosetu.com/man/api/\n */\n async executeNovel<T extends keyof NarouSearchResult>(\n params: SearchParams\n ): Promise<NarouSearchResults<NarouSearchResult, T>> {\n return await this.executeSearch(\n params,\n \"https://api.syosetu.com/novelapi/api/\"\n );\n }\n\n /**\n * R18小説APIへの検索リクエストを実行する\n * @param params クエリパラメータ\n * @returns 検索結果\n * @see https://dev.syosetu.com/xman/api/\n */\n async executeNovel18<T extends keyof NarouSearchResult>(\n params: SearchParams\n ): Promise<NarouSearchResults<NarouSearchResult, T>> {\n return await this.executeSearch(\n params,\n \"https://api.syosetu.com/novel18api/api/\"\n );\n }\n\n /**\n * ランキングAPIへのリクエストを実行する\n * @param params クエリパラメータ\n * @returns ランキング結果\n * @see https://dev.syosetu.com/man/rankapi/\n */\n async executeRanking(params: RankingParams): Promise<NarouRankingResult[]> {\n return await this.execute(params, \"https://api.syosetu.com/rank/rankget/\");\n }\n\n /**\n * 殿堂入りAPiへのリクエストを実行する\n * @param params クエリパラメータ\n * @returns ランキング履歴結果\n * @see https://dev.syosetu.com/man/rankinapi/\n */\n async executeRankingHistory(\n params: RankingHistoryParams\n ): Promise<RankingHistoryRawResult[]> {\n return await this.execute(params, \"https://api.syosetu.com/rank/rankin/\");\n }\n\n /**\n * ユーザー検索APIへのリクエストを実行する\n * @param params クエリパラメータ\n * @returns 検索結果\n * @see https://dev.syosetu.com/man/userapi/\n */\n async executeUserSearch<T extends keyof UserSearchResult>(\n params: UserSearchParams\n ): Promise<NarouSearchResults<UserSearchResult, T>> {\n return new NarouSearchResults<UserSearchResult, T>(\n await this.execute(params, \"https://api.syosetu.com/userapi/api/\"),\n params\n );\n }\n}\n","/**\n * MIT license\n */\n\n// Callback index.\nlet count = 0;\n\ntype CallbackId<Prefix extends string = string> = `${Prefix}${number}`;\n\ndeclare global {\n interface Window {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [key: CallbackId]: (data: any) => void;\n }\n}\n\n/**\n * JSONP呼び出しのオプション設定\n */\nexport type JsonpOption = {\n /**\n * コールバック関数名のプレフィックス\n * @default \"__jp\"\n */\n prefix?: string;\n \n /**\n * コールバック関数名を指定するURLパラメータ名\n * @default \"callback\"\n */\n param?: string;\n \n /**\n * タイムアウト時間(ミリ秒)\n * @default 15000\n */\n timeout?: number;\n};\n\nconst noop = function () { };\n\n/**\n * JSONPリクエストを実行してデータを取得します。\n * \n * @param url - リクエスト先のURL\n * @param options - JSONP呼び出しのオプション設定\n * @returns JSONPリクエストの結果をPromiseで返します\n * @throws {Error} タイムアウトが発生した場合、\"Timeout\"メッセージのエラーをスローします\n * \n * @example\n * ```typescript\n * // 基本的な使用方法\n * const data = await jsonp<ResponseType>('https://example.com/api');\n * \n * // オプション指定\n * const data = await jsonp<ResponseType>('https://example.com/api', {\n * prefix: 'customPrefix',\n * param: 'callbackParam',\n * timeout: 10000\n * });\n * ```\n */\nexport function jsonp<T>(\n url: string,\n { prefix = \"__jp\", param = \"callback\", timeout = 15000 }: JsonpOption = {}\n): Promise<T> {\n return new Promise(function (resolve, reject) {\n // 最初のscriptタグを取得し、そのタグの直前に新しいscriptタグを挿入するための参照を取得\n // これにより、ページの構造を大きく変えることなくscriptを追加できる\n const targetChild = document.getElementsByTagName(\"script\").item(0);\n const target = targetChild?.parentNode ?? document.head;\n\n // ユニークなコールバック関数名を生成\n const id: CallbackId = `${prefix}${count++}`;\n \n // リソース解放用の関数を定義\n // スクリプトタグの削除、コールバック関数のクリーンアップ、タイマーのクリアを行う\n const cleanup = function () {\n // Remove the script tag.\n if (script && script.parentNode) {\n script.parentNode.removeChild(script);\n }\n\n // コールバック関数を空の関数に置き換えてメモリリークを防止\n window[id] = noop;\n\n if (timer) {\n clearTimeout(timer);\n }\n };\n\n // タイムアウト処理の設定\n // 指定された時間内にレスポンスがない場合はエラーとして処理\n const timer =\n timeout > 0\n ? setTimeout(() => {\n cleanup();\n reject(new Error(\"Timeout\"));\n }, timeout)\n : undefined;\n\n // サーバーからのレスポンスを処理するコールバック関数\n const callback = (data: T) => {\n cleanup();\n resolve(data);\n };\n \n // グローバルスコープにコールバック関数を登録\n // これによりJSONPのレスポンスから関数が呼び出せるようになる\n window[id] = callback;\n\n // JSONPリクエスト用のscriptタグを作成\n const script = document.createElement(\"script\");\n const urlObj = new URL(url);\n \n // URLにコールバック関数名をパラメータとして追加\n urlObj.searchParams.set(param, id);\n script.setAttribute(\"src\", urlObj.toString());\n \n // DOMにscriptタグを挿入し、リクエストを開始\n target.insertBefore(script, targetChild);\n });\n}\n","import NarouNovel from \"./narou.js\";\nimport type { NarouParams } from \"./narou.js\";\nimport { jsonp } from \"./util/jsonp.js\";\n\n/**\n * なろう小説APIへのリクエストを実行する\n */\nexport default class NarouNovelJsonp extends NarouNovel {\n protected async execute<T>(\n params: NarouParams,\n endpoint: string\n ): Promise<T> {\n const query = { ...params, out: \"jsonp\" };\n query.gzip = 0;\n\n const url = new URL(endpoint);\n\n Object.entries(query).forEach(([key, value]) => {\n if (value !== undefined) {\n url.searchParams.append(key, value.toString());\n }\n });\n\n return await jsonp(url.toString());\n }\n}\n","import type {\n NarouSearchResult,\n UserSearchResult,\n} from \"./narou-search-results.js\";\nimport type { Join } from \"./util/type.js\";\n\nexport const RankingType = {\n Daily: \"d\",\n Weekly: \"w\",\n Monthly: \"m\",\n Quarterly: \"q\",\n} as const;\nexport type RankingType = (typeof RankingType)[keyof typeof RankingType];\n\n/**\n * すべてのAPIで共通のクエリパラメータ\n */\nexport interface ParamsBase {\n /**\n * gzip圧縮してgzipファイルとして返します。\n * gzip圧縮レベルを1~5で指定できます。\n * 転送量上限を減らすためにも推奨\n */\n gzip?: GzipLevel;\n /**\n * 出力形式を指定\n * 本ライブラリはJSONとJSONPのみ対応\n */\n out?: \"json\" | \"jsonp\";\n}\n\n/**\n * 検索APIで共通のクエリパラメータ\n */\nexport interface ParamsBaseWithOrder<TOrder extends string> extends ParamsBase {\n /**\n * 出力する項目を個別に指定できます。未指定時は全項目出力されます。\n * 転送量軽減のため、このパラメータの使用が推奨されます。\n */\n of?: string;\n /**\n * 最大出力数を指定できます。指定しない場合は20件になります。\n */\n lim?: number;\n /**\t表示開始位置の指定です。 */\n st?: number;\n /** 出力順序を指定できます。 */\n order?: TOrder;\n}\n\n/**\n * メソッドにパラメータを指定する際のヘルパー。\n * @see https://dev.syosetu.com/man/api/\n * @see https://dev.syosetu.com/xman/atom/\n */\nexport interface SearchParams extends ParamsBaseWithOrder<Order> {\n word?: string;\n notword?: string;\n title?: BooleanNumber;\n ex?: BooleanNumber;\n keyword?: BooleanNumber;\n wname?: BooleanNumber;\n\n biggenre?: Join<BigGenre> | BigGenre;\n notbiggenre?: Join<BigGenre> | BigGenre;\n genre?: Join<Genre> | Genre;\n notgenre?: Join<Genre> | Genre;\n userid?: Join<number> | number;\n\n nocgenre?: Join<R18Site> | R18Site;\n notnocgenre?: Join<R18Site> | R18Site;\n xid?: Join<number> | number;\n\n isr15?: BooleanNumber;\n isbl?: BooleanNumber;\n isgl?: BooleanNumber;\n iszankoku?: BooleanNumber;\n istensei?: BooleanNumber;\n istenni?: BooleanNumber;\n istt?: BooleanNumber;\n\n notr15?: BooleanNumber;\n notbl?: BooleanNumber;\n notgl?: BooleanNumber;\n notzankoku?: BooleanNumber;\n nottensei?: BooleanNumber;\n nottenni?: BooleanNumber;\n\n minlen?: number;\n maxlen?: number;\n length?: number | Join<number | \"\">;\n\n kaiwaritu?: number | string;\n sasie?: number | string;\n\n mintime?: number;\n maxtime?: number;\n time?: number | string;\n\n ncode?: string | Join<string>;\n\n type?: NovelTypeParam;\n\n buntai?: BuntaiParam | Join<BuntaiParam>;\n\n stop?: StopParam;\n\n ispickup?: typeof BooleanNumber.True;\n lastup?: string;\n lastupdate?: string;\n\n opt?: Join<OptionalFields>;\n}\n\nexport interface RankingParams extends ParamsBase {\n rtype: `${string}-${RankingType}`;\n}\n\nexport interface RankingHistoryParams extends ParamsBase {\n ncode: string;\n}\n\n/**\n * ユーザー検索パラメータ\n */\nexport interface UserSearchParams extends ParamsBaseWithOrder<UserOrder> {\n /** 単語を指定できます。半角または全角スペースで区切るとAND抽出になります。部分一致でHITします。検索の対象はユーザ名とユーザ名のフリガナです。 */\n word?: string;\n /** 含みたくない単語を指定できます。スペースで区切ることにより含ませない単語を増やせます。部分一致で除外されます。除外の対象はユーザ名とユーザ名のフリガナです。 */\n notword?: string;\n /** ユーザIDで抽出可能。 */\n userid?: number;\n /** 抽出するユーザのユーザ名のフリガナの頭文字を指定できます。頭文字はユーザ名のフリガナをひらがなに変換し、最初の1文字が「ぁ」~「ん」の場合に対象となります。 */\n name1st?: string;\n /** 抽出するユーザの小説投稿数の下限を指定できます。小説投稿件数が指定された数値以上のユーザを抽出します。 */\n minnovel?: number;\n /** 抽出するユーザの小説投稿数の上限を指定できます。小説投稿件数が指定された数値以下のユーザを抽出します。 */\n maxnovel?: number;\n /** 抽出するユーザのレビュー投稿数の下限を指定できます。レビュー投稿件数が指定された数値以上のユーザを抽出します。 */\n minreview?: number;\n /** 抽出するユーザのレビュー投稿数の上限を指定できます。レビュー投稿件数が指定された数値以下のユーザを抽出します。 */\n maxreview?: number;\n}\n\nexport const BooleanNumber = {\n True: 1,\n False: 0,\n} as const;\nexport type BooleanNumber = (typeof BooleanNumber)[keyof typeof BooleanNumber];\n\nexport type SearchResultFieldNames = keyof NarouSearchResult;\n\n/**\n * なろう小説APIのofパラメータに指定できる出力する項目\n * @see https://dev.syosetu.com/man/api/#output\n */\nexport const Fields = {\n /** 小説名 */\n title: \"t\",\n /** Nコード */\n ncode: \"n\",\n /** 作者のユーザID(数値) */\n userid: \"u\",\n /** 作者名 */\n writer: \"w\",\n /** 小説のあらすじ */\n story: \"s\",\n /** 大ジャンル */\n biggenre: \"bg\",\n /** ジャンル */\n genre: \"g\",\n /** キーワード */\n keyword: \"k\",\n /** 初回掲載日 */\n general_firstup: \"gf\",\n /** 最終掲載日 */\n general_lastup: \"gl\",\n /** 連載の場合は1、短編の場合は2 */\n noveltype: \"nt\",\n /** 短編小説と完結済小説は0となっています。連載中は1です。 */\n end: \"e\",\n /** 全掲載部分数 */\n general_all_no: \"ga\",\n /** 小説文字数 */\n length: \"l\",\n /** 読了時間(分単位) */\n time: \"ti\",\n /** 長期連載停止中 */\n isstop: \"i\",\n /** 登録必須キーワードに「R15」が含まれる場合は1、それ以外は0です。 */\n isr15: \"isr\",\n /** 登録必須キーワードに「ボーイズラブ」が含まれる場合は1、それ以外は0です。 */\n isbl: \"ibl\",\n /** 登録必須キーワードに「ガールズラブ」が含まれる場合は1、それ以外は0です。 */\n isgl: \"igl\",\n /** 登録必須キーワードに「残酷な描写あり」が含まれる場合は1、それ以外は0です。 */\n iszankoku: \"izk\",\n /** 登録必須キーワードに「異世界転生」が含まれる場合は1、それ以外は0です。 */\n istensei: \"its\",\n /** 登録必須キーワードに「異世界転移」が含まれる場合は1、それ以外は0です。 */\n istenni: \"iti\",\n /** 総合評価ポイント */\n global_point: \"gp\",\n /** 日間ポイント */\n daily_point: \"dp\",\n /** 週間ポイント */\n weekly_point: \"wp\",\n /** 月間ポイント */\n monthly_point: \"mp\",\n /** 四半期ポイント */\n quarter_point: \"qp\",\n /** 年間ポイント */\n yearly_point: \"yp\",\n /** ブックマーク数 */\n fav_novel_cnt: \"f\",\n /** 感想数 */\n impression_cnt: \"imp\",\n /** レビュー数 */\n review_cnt: \"r\",\n /** 評価ポイント */\n all_point: \"a\",\n /** 評価者数 */\n all_hyoka_cnt: \"ah\",\n /** 挿絵の数 */\n sasie_cnt: \"sa\",\n /** 会話率 */\n kaiwaritu: \"ka\",\n /** 小説の更新日時 */\n novelupdated_at: \"nu\",\n /**\n * 最終更新日時\n * システム用で小説更新時とは関係ありません\n */\n updated_at: \"ua\",\n} as const;\n\nexport type Fields = (typeof Fields)[keyof Omit<\n NarouSearchResult,\n \"novel_type\" | \"weekly_unique\" | \"nocgenre\"\n>];\n\n/**\n * なろうR18小説APIのofパラメータに指定できる出力する項目\n * @see https://dev.syosetu.com/xman/api/#output\n */\nexport const R18Fields = {\n /** 小説名 */\n title: \"t\",\n /** Nコード */\n ncode: \"n\",\n /** 作者のユーザID(数値) */\n userid: \"u\",\n /** 作者名 */\n writer: \"w\",\n /** 小説のあらすじ */\n story: \"s\",\n /** 掲載サイト */\n nocgenre: \"ng\",\n /** キーワード */\n keyword: \"k\",\n /** 初回掲載日 */\n general_firstup: \"gf\",\n /** 最終掲載日 */\n general_lastup: \"gl\",\n /** 連載の場合は1、短編の場合は2 */\n noveltype: \"nt\",\n /** 短編小説と完結済小説は0となっています。連載中は1です。 */\n end: \"e\",\n /** 全掲載部分数 */\n general_all_no: \"ga\",\n /** 小説文字数 */\n length: \"l\",\n /** 読了時間(分単位) */\n time: \"ti\",\n /** 長期連載停止中 */\n isstop: \"i\",\n /** 登録必須キーワードに「ボーイズラブ」が含まれる場合は1、それ以外は0です。 */\n isbl: \"ibl\",\n /** 登録必須キーワードに「ガールズラブ」が含まれる場合は1、それ以外は0です。 */\n isgl: \"igl\",\n /** 登録必須キーワードに「残酷な描写あり」が含まれる場合は1、それ以外は0です。 */\n iszankoku: \"izk\",\n /** 登録必須キーワードに「異世界転生」が含まれる場合は1、それ以外は0です。 */\n istensei: \"its\",\n /** 登録必須キーワードに「異世界転移」が含まれる場合は1、それ以外は0です。 */\n istenni: \"iti\",\n /** 総合評価ポイント */\n global_point: \"gp\",\n /** 日間ポイント */\n daily_point: \"dp\",\n /** 週間ポイント */\n weekly_point: \"wp\",\n /** 月間ポイント */\n monthly_point: \"mp\",\n /** 四半期ポイント */\n quarter_point: \"qp\",\n /** 年間ポイント */\n yearly_point: \"yp\",\n /** R18ブックマーク数 */\n fav_novel_cnt: \"f\",\n /** 感想数 */\n impression_cnt: \"imp\",\n /** レビュー数 */\n review_cnt: \"r\",\n /** 評価ポイント */\n all_point: \"a\",\n /** 評価者数 */\n all_hyoka_cnt: \"ah\",\n /** 挿絵の数 */\n sasie_cnt: \"sa\",\n /** 会話率 */\n kaiwaritu: \"ka\",\n /** 小説の更新日時 */\n novelupdated_at: \"nu\",\n /**\n * 最終更新日時\n * システム用で小説更新時とは関係ありません\n */\n updated_at: \"ua\",\n} as const;\n\nexport type R18Fields = (typeof R18Fields)[keyof Omit<\n NarouSearchResult,\n \"novel_type\" | \"weekly_unique\" | \"biggenre\" | \"genre\" | \"isr15\"\n>];\n\n/**\n * オプション項目\n */\nexport const OptionalFields = {\n /**\n * 週間ユニークユーザ[項目名:weekly_unique]が追加されます。\n * 週間ユニークユーザは前週の日曜日から土曜日分のユニークの合計です。\n * 毎週火曜日早朝に更新されます。\n */\n weekly_unique: \"weekly\",\n} as const;\n\nexport type OptionalFields = (typeof OptionalFields)[keyof Pick<\n NarouSearchResult,\n \"weekly_unique\"\n>];\n\n/**\n * ユーザ検索APIのofパラメータに指定できる出力する項目\n * @see https://dev.syosetu.com/man/userapi/#output\n */\nexport const UserFields = {\n /** ユーザID */\n userid: \"u\",\n /** ユーザ名 */\n name: \"n\",\n /** ユーザ名のフリガナ */\n yomikata: \"y\",\n /** ユーザ名のフリガナの頭文字 */\n name1st: \"1\",\n /** 小説投稿数 */\n novel_cnt: \"nc\",\n /** レビュー投稿数 */\n review_cnt: \"rc\",\n /** 小説累計文字数 */\n novel_length: \"nl\",\n /** 総合評価ポイントの合計 */\n sum_global_point: \"sg\",\n} as const;\nexport type UserFields = (typeof UserFields)[keyof UserSearchResult];\n\n/**\n * 出力順序\n */\nexport const Order = {\n /** ブックマーク数の多い順 */\n FavoriteNovelCount: \"favnovelcnt\",\n /** レビュー数の多い順 */\n ReviewCount: \"favnovelcnt\",\n /** 総合ポイントの高い順 */\n HyokaDesc: \"hyoka\",\n /** 総合ポイントの低い順 */\n HyokaAsc: \"hyokaasc\",\n /** 感想の多い順 */\n ImpressionCount: \"impressioncnt\",\n /** 評価者数の多い順 */\n HyokaCountDesc: \"hyokacnt\",\n /** 評価者数の少ない順 */\n HyokaCountAsc: \"hyokacntasc\",\n /** 週間ユニークユーザの多い順 */\n Weekly: \"weekly\",\n /** 小説本文の文字数が多い順 */\n LengthDesc: \"lengthdesc\",\n /** 小説本文の文字数が少ない順 */\n LengthAsc: \"lengthasc\",\n /** Nコードが新しい順 */\n NCodeDesc: \"ncodedesc\",\n /** 新着更新順 */\n New: \"new\",\n /** 古い順 */\n Old: \"old\",\n /** 日間ポイントの高い順 */\n DailyPoint: \"dailypoint\",\n /** 週間ポイントの高い順 */\n WeeklyPoint: \"weeklypoint\",\n /** 月間ポイントの高い順 */\n MonthlyPoint: \"monthlypoint\",\n /** 四半期ポイントの高い順 */\n QuarterPoint: \"quarterpoint\",\n /** 年間ポイントの高い順 */\n YearlyPoint: \"yearlypoint\",\n /** 初回掲載順 */\n GeneralFirstUp: \"generalfirstup\",\n} as const;\n\nexport type Order = (typeof Order)[keyof typeof Order];\n\n/** R18掲載サイト */\nexport const R18Site = {\n /** ノクターンノベルズ(男性向け) */\n Nocturne: 1,\n /** ムーンライトノベルズ(女性向け) */\n MoonLight: 2,\n /** ムーンライトノベルズ(BL) */\n MoonLightBL: 3,\n /** ミッドナイトノベルズ(大人向け) */\n Midnight: 4,\n} as const;\n\nexport type R18Site = (typeof R18Site)[keyof typeof R18Site];\n\n/** R18掲載サイト表記ヘルパー */\nexport const R18SiteNotation: { readonly [K in R18Site]: string } = {\n [R18Site.Nocturne]: \"ノクターンノベルズ(男性向け)\",\n [R18Site.MoonLight]: \"ムーンライトノベルズ(女性向け)\",\n [R18Site.MoonLightBL]: \"ムーンライトノベルズ(BL)\",\n [R18Site.Midnight]: \"ミッドナイトノベルズ(大人向け)\",\n} as const;\n\n/** 大ジャンル */\nexport const BigGenre = {\n /** 恋愛 */\n Renai: 1,\n /** ファンタジー */\n Fantasy: 2,\n /** 文芸 */\n Bungei: 3,\n /** SF */\n Sf: 4,\n /** その他 */\n Sonota: 99,\n /** ノンジャンル */\n NonGenre: 98,\n} as const;\n\nexport type BigGenre = (typeof BigGenre)[keyof typeof BigGenre];\n\n/** 大ジャンル表記ヘルパー */\nexport const BigGenreNotation: { readonly [K in BigGenre]: string } = {\n [BigGenre.Renai]: \"恋愛\",\n [BigGenre.Fantasy]: \"ファンタジー\",\n [BigGenre.Bungei]: \"文芸\",\n [BigGenre.Sf]: \"SF\",\n [BigGenre.Sonota]: \"その他\",\n [BigGenre.NonGenre]: \"ノンジャンル\",\n} as const;\n\n/** ジャンル */\nexport const Genre = {\n /** 異世界〔恋愛〕*/\n RenaiIsekai: 101,\n /** 現実世界〔恋愛〕*/\n RenaiGenjitsusekai: 102,\n /** ハイファンタジー〔ファンタジー〕*/\n FantasyHigh: 201,\n /** ローファンタジー〔ファンタジー〕*/\n FantasyLow: 202,\n /** 純文学〔文芸〕*/\n BungeiJyunbungei: 301,\n /** ヒューマンドラマ〔文芸〕*/\n BungeiHumanDrama: 302,\n /** 歴史〔文芸〕*/\n BungeiHistory: 303,\n /** 推理〔文芸〕*/\n BungeiSuiri: 304,\n /** ホラー〔文芸〕*/\n BungeiHorror: 305,\n /** アクション〔文芸〕*/\n BungeiAction: 306,\n /** コメディー〔文芸〕*/\n BungeiComedy: 307,\n /** VRゲーム〔SF〕*/\n SfVrgame: 401,\n /** 宇宙〔SF〕*/\n SfSpace: 402,\n /** 空想科学〔SF〕*/\n SfKuusoukagaku: 403,\n /** パニック〔SF〕*/\n SfPanic: 404,\n /** 童話〔その他〕*/\n SonotaDouwa: 9901,\n /** 詩〔その他〕*/\n SonotaShi: 9902,\n /** エッセイ〔その他〕*/\n SonotaEssei: 9903,\n /** リプレイ〔その他〕*/\n SonotaReplay: 9904,\n /** その他〔その他〕 */\n SonotaSonota: 9999,\n /** ノンジャンル〔ノンジャンル〕*/\n NonGenre: 9801,\n} as const;\nexport type Genre = (typeof Genre)[keyof typeof Genre];\n\n/** ジャンル表記ヘルパー */\nexport const GenreNotation: { readonly [K in Genre]: string } = {\n [Genre.RenaiIsekai]: \"異世界〔恋愛〕\",\n [Genre.RenaiGenjitsusekai]: \"現実世界〔恋愛〕\",\n [Genre.FantasyHigh]: \"ハイファンタジー〔ファンタジー〕\",\n [Genre.FantasyLow]: \"ローファンタジー〔ファンタジー〕\",\n [Genre.BungeiJyunbungei]: \"純文学〔文芸〕\",\n [Genre.BungeiHumanDrama]: \"ヒューマンドラマ〔文芸〕\",\n [Genre.BungeiHistory]: \"歴史〔文芸〕\",\n [Genre.BungeiSuiri]: \"推理〔文芸〕\",\n [Genre.BungeiHorror]: \"ホラー〔文芸〕\",\n [Genre.BungeiAction]: \"アクション〔文芸〕\",\n [Genre.BungeiComedy]: \"コメディー〔文芸〕\",\n [Genre.SfVrgame]: \"VRゲーム〔SF〕\",\n [Genre.SfSpace]: \"宇宙〔SF〕\",\n [Genre.SfKuusoukagaku]: \"空想科学〔SF〕\",\n [Genre.SfPanic]: \"パニック〔SF〕\",\n [Genre.SonotaDouwa]: \"童話〔その他〕\",\n [Genre.SonotaShi]: \"詩〔その他〕\",\n [Genre.SonotaEssei]: \"エッセイ〔その他〕\",\n [Genre.SonotaReplay]: \"リプレイ〔その他〕\",\n [Genre.SonotaSonota]: \"その他〔その他〕\",\n [Genre.NonGenre]: \"ノンジャンル〔ノンジャンル〕\",\n} as const;\n\n/** 文体指定 */\nexport const BuntaiParam = {\n /** 字下げされておらず、連続改行が多い作品 */\n NoJisageKaigyouOoi: 1,\n /** 字下げされていないが、改行数は平均な作品 */\n NoJisageKaigyoHutsuu: 2,\n /** 字下げが適切だが、連続改行が多い作品 */\n JisageKaigyoOoi: 4,\n /** 字下げが適切でかつ改行数も平均な作品 */\n JisageKaigyoHutsuu: 6,\n} as const;\n\nexport type BuntaiParam = (typeof BuntaiParam)[keyof typeof BuntaiParam];\n\n/** 連載停止中指定 */\nexport const StopParam = {\n /** 長期連載停止中を除きます */\n NoStopping: 1,\n /** 長期連載停止中のみ取得します */\n Stopping: 2,\n} as const;\n\nexport type StopParam = (typeof StopParam)[keyof typeof StopParam];\n\n/** 小説タイプ指定 */\nexport const NovelTypeParam = {\n /** 短編 */\n Short: \"t\",\n /** 連載中 */\n RensaiNow: \"r\",\n /** 完結済連載小説 */\n RensaiEnd: \"er\",\n /** すべての連載小説(連載中および完結済) */\n Rensai: \"re\",\n /** 短編と完結済連載小説 */\n ShortAndRensai: \"ter\",\n} as const;\nexport type NovelTypeParam =\n (typeof NovelTypeParam)[keyof typeof NovelTypeParam];\n\n/** 日付指定パラメータ */\nexport const DateParam = {\n ThisWeek: \"thisweek\",\n LastWeek: \"lastweek\",\n SevenDays: \"sevenday\",\n ThisMonth: \"thismonth\",\n LastMonth: \"lastmonth\",\n};\nexport type DateParam = (typeof DateParam)[keyof typeof DateParam];\n\nexport const UserOrder = {\n /** ユーザIDの新しい順 */\n New: \"new\",\n /** 小説投稿数の多い順 */\n NovelCount: \"novelcnt\",\n /** レビュー投稿数の多い順 */\n ReviewCount: \"reviewcnt\",\n /** 小説累計文字数の多い順 */\n NovelLength: \"novellength\",\n /** 総合評価ポイントの合計の多い順 */\n SumGlobalPoint: \"sumglobalpoint\",\n /** ユーザIDの古い順 */\n Old: \"old\",\n} as const;\nexport type UserOrder = (typeof UserOrder)[keyof typeof UserOrder];\n\nexport type GzipLevel = 0 | 1 | 2 | 3 | 4 | 5;\n","import type NarouNovel from \"./narou.js\";\nimport type {\n NarouSearchResult,\n SearchResultFields,\n SearchResultOptionalFields,\n} from \"./narou-search-results.js\";\nimport type NarouSearchResults from \"./narou-search-results.js\";\nimport type {\n BigGenre,\n SearchResultFieldNames,\n Genre,\n SearchParams,\n Fields,\n Order,\n BuntaiParam,\n NovelTypeParam,\n GzipLevel,\n OptionalFields,\n ParamsBaseWithOrder,\n DateParam,\n} from \"./params.js\";\nimport { BooleanNumber, StopParam } from \"./params.js\";\nimport type { Join } from \"./util/type.js\";\n\nexport type DefaultSearchResultFields = keyof Omit<\n NarouSearchResult,\n \"weekly_unique\" | \"noveltype\" | \"nocgenre\" | \"xid\"\n>;\n\nexport abstract class SearchBuilderBase<\n TParams extends ParamsBaseWithOrder<TOrder>,\n TOrder extends string,\n> {\n /**\n * constructor\n * @private\n * @param params クエリパラメータ\n * @param api NarouNovel インスタンス\n */\n constructor(\n protected params: TParams = {} as TParams,\n protected api: NarouNovel\n ) {}\n\n /**\n * 配列から重複を除去する\n * @protected\n * @static\n * @param array 配列\n * @returns 重複を除去した配列\n */\n protected static distinct<T>(array: readonly T[]): T[] {\n return Array.from(new Set(array));\n }\n\n /**\n * 配列をハイフン区切りの文字列に変換する\n * @protected\n * @static\n * @param n 文字列または数値の配列、あるいは単一の文字列または数値\n * @returns ハイフン区切りの文字列\n */\n protected static array2string<T extends string | number>(\n n: T | readonly T[]\n ): Join<T> {\n if (Array.isArray(n)) {\n return this.distinct(n).join(\"-\") as Join<T>;\n } else {\n return n.toString() as Join<T>;\n }\n }\n\n /**\n * 取得件数を指定する (lim)\n * @param num 取得件数 (1-500)\n * @return {this}\n */\n limit(num: number): this {\n this.set({ lim: num } as TParams);\n return this;\n }\n\n /**\n * 取得開始位置を指定する (st)\n * @param num 取得開始位置 (1-)\n * @return {this}\n */\n start(num: number): this {\n this.set({ st: num } as TParams);\n return this;\n }\n\n /**\n * ページ番号と1ページあたりの件数で取得範囲を指定する\n * @param no ページ番号 (0-)\n * @param count 1ページあたりの件数 (デフォルト: 20)\n * @return {this}\n */\n page(no: number, count = 20): this {\n return this.limit(count).start(no * count);\n }\n\n /**\n * 出力順序を指定する (order)\n * 指定しない場合は新着順となります。\n * @param {TOrder} order 出力順序\n * @return {this}\n */\n order(order: TOrder): this {\n this.set({ order: order } as TParams);\n return this;\n }\n\n /**\n * gzip圧縮レベルを指定する (gzip)\n *\n * 転送量上限を減らすためにも推奨\n * @param {GzipLevel} level gzip圧縮レベル(1~5)\n * @return {this}\n */\n gzip(level: GzipLevel): this {\n this.set({ gzip: level } as TParams);\n return this;\n }\n\n /**\n * クエリパラメータをセットする\n * @protected\n * @param obj セットするパラメータ\n * @return {this}\n */\n protected set(obj: TParams): this {\n this.params = { ...this.params, ...obj };\n return this;\n }\n\n /**\n * クエリパラメータを削除する\n * @protected\n * @param key 削除するパラメータのキー\n * @returns {this}\n */\n protected unset(key: keyof TParams): this {\n delete this.params[key];\n return this;\n }\n}\n\nexport abstract class NovelSearchBuilderBase<\n T extends SearchResultFieldNames,\n> extends SearchBuilderBase<SearchParams, Order> {\n /**\n * 検索語を指定します (word)。\n * 半角または全角スペースで区切るとAND抽出になります。部分一致でHITします。\n * @param word 検索語\n * @return {this}\n */\n word(word: string): this {\n this.set({ word: word });\n return this;\n }\n\n /**\n * 除外したい単語を指定します (notword)。\n * スペースで区切ることにより除外する単語を増やせます。部分一致で除外されます。\n * @param word 除外語\n * @return {this}\n */\n notWord(word: string): this {\n this.set({ notword: word });\n return this;\n }\n\n /**\n * 検索対象を作品名に限定するかどうかを指定します (title)。\n * @param bool trueの場合、作品名を検索対象とする (デフォルト: true)\n * @return {this}\n */\n byTitle(bool = true): this {\n this.set({ title: bool ? BooleanNumber.True : BooleanNumber.False });\n return this;\n }\n\n /**\n * 検索対象をあらすじに限定するかどうかを指定します (ex)。\n * @param bool trueの場合、あらすじを検索対象とする (デフォルト: true)\n * @return {this}\n */\n byOutline(bool = true): this {\n this.set({ ex: bool ? BooleanNumber.True : BooleanNumber.False });\n return this;\n }\n\n /**\n * 検索対象をキーワードに限定するかどうかを指定します (keyword)。\n * @param bool trueの場合、キーワードを検索対象とする (デフォルト: true)\n * @return {this}\n */\n byKeyword(bool = true): this {\n this.set({ keyword: bool ? BooleanNumber.True : BooleanNumber.False });\n return this;\n }\n\n /**\n * 検索対象を作者名に限定するかどうかを指定します (wname)。\n * @param bool trueの場合、作者名を検索対象とする (デフォルト: true)\n * @return {this}\n */\n byAuthor(bool = true): this {\n this.set({ wname: bool ? BooleanNumber.True : BooleanNumber.False });\n return this;\n }\n\n /**\n * ボーイズラブ作品を抽出または除外します (isbl/notbl)。\n * @param bool trueの場合、ボーイズラブ作品を抽出する (デフォルト: true)。falseの場合、除外する。\n * @return {this}\n */\n isBL(bool = true): this {\n if (bool) {\n this.set({ isbl: BooleanNumber.True });\n } else {\n this.set({ notbl: BooleanNumber.True });\n }\n return this;\n }\n\n /**\n * ガールズラブ作品を抽出または除外します (isgl/notgl)。\n * @param bool trueの場合、ガールズラブ作品を抽出する (デフォルト: true)。falseの場合、除外する。\n * @return {this}\n */\n isGL(bool = true): this {\n if (bool) {\n this.set({ isgl: BooleanNumber.True });\n } else {\n this.set({ notgl: BooleanNumber.True });\n }\n return this;\n }\n\n /**\n * 残酷な描写あり作品を抽出または除外します (iszankoku/notzankoku)。\n * @param bool trueの場合、残酷な描写あり作品を抽出する (デフォルト: true)。falseの場合、除外する。\n * @return {this}\n */\n isZankoku(bool = true): this {\n if (bool) {\n this.set({ iszankoku: BooleanNumber.True });\n } else {\n this.set({ notzankoku: BooleanNumber.True });\n }\n return this;\n }\n\n /**\n * 異世界転生作品を抽出または除外します (istensei/nottensei)。\n * @param bool trueの場合、異世界転生作品を抽出する (デフォルト: true)。falseの場合、除外する。\n * @return {this}\n */\n isTensei(bool = true): this {\n if (bool) {\n this.set({ istensei: BooleanNumber.True });\n } else {\n this.set({ nottensei: BooleanNumber.True });\n }\n return this;\n }\n\n /**\n * 異世界転移作品を抽出または除外します (istenni/nottenni)。\n * @param bool trueの場合、異世界転移作品を抽出する (デフォルト: true)。falseの場合、除外する。\n * @return {this}\n */\n isTenni(bool = true): this {\n if (bool) {\n this.set({ istenni: BooleanNumber.True });\n } else {\n this.set({ nottenni: BooleanNumber.True });\n }\n return this;\n }\n\n /**\n * 異世界転生または異世界転移作品を抽出します (istt)。\n * @return {this}\n */\n isTT(): this {\n this.set({ istt: BooleanNumber.True });\n return this;\n }\n\n /**\n * 抽出する作品の文字数を指定します (length)。\n * 範囲指定する場合は、最小文字数と最大文字数をハイフン(-)記号で区切ってください。\n * @param length 文字数、または[最小文字数, 最大文字数]\n * @return {this}\n */\n length(length: number | readonly number[]): this {\n this.set({ length: NovelSearchBuilderBase.array2string(length) });\n return this;\n }\n\n /**\n * 抽出する作品の会話率を%単位で指定します (kaiwaritu)。\n * @param num 会話率(%)\n * @return {this}\n */\n kaiwaritu(num: number): this;\n /**\n * 抽出する作品の会話率を%単位で範囲指定します (kaiwaritu)。\n * @param min 最低会話率(%)\n * @param max 最高会話率(%)\n * @return {this}\n */\n kaiwaritu(min: number, max: number): this;\n\n kaiwaritu(min: number, max?: number): this {\n let n: number | string;\n if (max != null) {\n n = `${min}-${max}`;\n } else {\n n = min;\n }\n this.set({ kaiwaritu: n });\n return this;\n }\n\n /**\n * 抽出する作品の挿絵数を指定します (sasie)。\n * @param num 挿絵数、または[最小挿絵数, 最大挿絵数]\n * @return {this}\n */\n sasie(num: number | readonly number[]): this {\n this.set({ sasie: NovelSearchBuilderBase.array2string(num) });\n return this;\n }\n\n /**\n * 抽出する作品の予想読了時間を分単位で指定します (time)。\n * @param num 読了時間(分)、または[最小読了時間, 最大読了時間]\n * @return {this}\n */\n time(num: number | readonly number[]): this {\n this.set({ time: NovelSearchBuilderBase.array2string(num) });\n return this;\n }\n\n /**\n * Nコードを指定して取得します (ncode)。\n * @param ncodes Nコード、またはNコードの配列\n * @return {this}\n */\n ncode(ncodes: string | readonly string[]): this {\n this.set({ ncode: NovelSearchBuilderBase.array2string(ncodes) });\n return this;\n }\n\n /**\n * 抽出する小説タイプを指定します (type)。\n * @param type 小説タイプ (t: 短編, r: 連載中, er: 完結済連載小説, ter: 短編と完結済連載小説, re: 連載中と完結済連載小説)\n * @return {this}\n */\n type(type: NovelTypeParam): this {\n this.set({ type });\n return this;\n }\n\n /**\n * 抽出する作品の文体を指定します (buntai)。\n * 複数指定する場合はハイフン(-)で区切ってください。\n * @param buntai 文体コード、または文体コードの配列\n * @return {this}\n */\n buntai(buntai: BuntaiParam | readonly BuntaiParam[]): this {\n this.set({ buntai: NovelSearchBuilderBase.array2string(buntai) });\n return this;\n }\n\n /**\n * 連載停止中作品に関する指定をします (stop)。\n * @param bool trueの場合、長期連載停止中のみ取得する (デフォルト: true)。falseの場合、長期連載停止中を除外する。\n * @return {this}\n */\n isStop(bool = true): this {\n this.set({ stop: bool ? StopParam.Stopping : StopParam.NoStopping });\n return this;\n }\n\n /**\n * ピックアップ作品のみを取得します (ispickup)。\n * @return {this}\n */\n isPickup(): this {\n this.set({ ispickup: BooleanNumber.True });\n return this;\n }\n\n /**\n * 最終更新日時を指定します (lastup)。\n * @param date 最終更新日時 (YYYYMMDDhhmmss形式またはUNIXタイムスタンプ)\n * @return {this}\n */\n lastUpdate(date: DateParam): this;\n /**\n * 最終更新日時の範囲を指定します (lastup)。\n * @param from 開始日時 (UNIXタイムスタンプ)\n * @param to 終了日時 (UNIXタイムスタンプ)\n * @return {this}\n */\n lastUpdate(from: number, to: number): this;\n /**\n * 最終更新日時の範囲を指定します (lastup)。\n * @param from 開始日時 (Dateオブジェクト)\n * @param to 終了日時 (Dateオブジェクト)\n * @return {this}\n */\n lastUpdate(from: Date, to: Date): this;\n\n lastUpdate(x: string | number | Date, y?: number | Date): this {\n let date: string;\n if (typeof x == \"string\") {\n date = x;\n } else if (x instanceof Date && y instanceof Date) {\n date = `${Math.floor(x.getTime() / 1000)}-${Math.floor(\n y.getTime() / 1000\n )}`;\n } else {\n date = `${x}-${y}`;\n }\n\n this.set({ lastup: date });\n return this;\n }\n\n /**\n * 作品の更新日時を指定します (lastupdate)。\n * @param date 作品の更新日時 (YYYYMMDDhhmmss形式またはUNIXタイムスタンプ)\n * @return {this}\n */\n lastNovelUpdate(date: DateParam): this;\n /**\n * 作品の更新日時の範囲を指定します (lastupdate)。\n * @param from 開始日時 (UNIXタイムスタンプ)\n * @param to 終了日時 (UNIXタイムスタンプ)\n * @return {this}\n */\n lastNovelUpdate(from: number, to: number): this;\n /**\n * 作品の更新日時の範囲を指定します (lastupdate)。\n * @param from 開始日時 (Dateオブジェクト)\n * @param to 終了日時 (Dateオブジェクト)\n * @return {this}\n */\n lastNovelUpdate(from: Date, to: Date): this;\n\n lastNovelUpdate(x: string | number | Date, y?: number | Date): this {\n let date: string;\n if (typeof x == \"string\") {\n date = x;\n } else if (x instanceof Date && y instanceof Date) {\n date = `${Math.floor(x.getTime() / 1000)}-${Math.floor(\n y.getTime() / 1000\n )}`;\n } else {\n date = `${x}-${y}`;\n }\n\n this.set({ lastupdate: date });\n return this;\n }\n\n /**\n * なろう小説APIへの検索リクエストを実行する\n * @returns {Promise<NarouSearchResults>} 検索結果\n */\n execute(): Promise<NarouSearchResults<NarouSearchResult, T>> {\n return this.api.executeNovel(this.params);\n }\n}\n\n/**\n * 検索ヘルパー\n * @class SearchBuilder\n */\nexport default class SearchBuilder<\n T extends keyof NarouSearchResult = DefaultSearchResultFields,\n TOpt extends keyof NarouSearchResult = never,\n> extends NovelSearchBuilderBase<T | TOpt> {\n /**\n * 大ジャンルを指定して取得します (biggenre)。\n * 複数指定する場合はハイフン(-)で区切ってください。\n * @param genre 大ジャンルコード、または大ジャンルコードの配列\n * @return {this}\n */\n bigGenre(genre: BigGenre | readonly BigGenre[]): this {\n this.set({ biggenre: SearchBuilder.array2string(genre) });\n return this;\n }\n\n /**\n * 除外したい大ジャンルを指定します (notbiggenre)。\n * 複数指定する場合はハイフン(-)で区切ってください。\n * @param genre 除外する大ジャンルコード、または大ジャンルコードの配列\n * @return {this}\n */\n notBigGenre(genre: BigGenre | readonly BigGenre[]): this {\n this.set({ notbiggenre: SearchBuilder.array2string(genre) });\n return this;\n }\n\n /**\n * ジャンルを指定して取得します (genre)。\n * 複数指定する場合はハイフン(-)で区切ってください。\n * @param genre ジャンルコード、またはジャンルコードの配列\n * @return {this}\n */\n genre(genre: Genre | readonly Genre[]): this {\n this.set({ genre: SearchBuilder.array2string(genre) });\n return this;\n }\n\n /**\n * 除外したいジャンルを指定します (notgenre)。\n * 複数指定する場合はハイフン(-)で区切ってください。\n * @param genre 除外するジャンルコード、またはジャンルコードの配列\n * @return {this}\n */\n notGenre(genre: Genre | readonly Genre[]): this {\n this.set({ notgenre: SearchBuilder.array2string(genre) });\n return this;\n }\n\n /**\n * ユーザIDを指定して取得します (userid)。\n * 複数指定する場合はハイフン(-)で区切ってください。\n * @param ids ユーザID、またはユーザIDの配列\n * @return {this}\n */\n userId(ids: number | readonly number[]): this {\n this.set({ userid: SearchBuilder.array2string(ids) });\n return this;\n }\n\n /**\n * R15作品を抽出または除外します (isr15/notr15)。\n * @param bool trueの場合、R15作品を抽出する (デフォルト: true)。falseの場合、除外する。\n * @return {this}\n */\n isR15(bool = true): this {\n if (bool) {\n this.set({ isr15: 1 });\n } else {\n this.set({ notr15: 1 });\n }\n return this;\n }\n\n /**\n * 出力する項目を個別に指定します (of)。\n * 未指定時は全項目出力されます。転送量軽減のため、このパラメータの使用が推奨されます。\n * 複数項目を出力する場合はハイフン(-)記号で区切ってください。\n * @param fields 出力するフィールド名、またはフィールド名の配列\n * @return {SearchBuilder<SearchResultFields<TFields>, TOpt>} 型が更新されたビルダー\n */\n fields<TFields extends Fields>(\n fields: TFields | readonly TFields[]\n ): SearchBuilder<SearchResultFields<TFields>, TOpt> {\n this.set({ of: SearchBuilder.array2string(fields) });\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return this as any;\n }\n\n /**\n * 出力オプション項目を指定します (opt)。\n * 複数項目を出力する場合はハイフン(-)記号で区切ってください。\n * @param option 出力するオプションフィールド名、またはオプションフィールド名の配列\n * @return {SearchBuilder<T, SearchResultOptionalFields<TFields>>} 型が更新されたビルダー\n */\n opt<TFields extends OptionalFields>(\n option: TFields | readonly TFields[]\n ): SearchBuilder<T, SearchResultOptionalFields<TFields>> {\n this.set({ opt: SearchBuilder.array2string(option) });\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return this as any;\n }\n}\n","import { NovelSearchBuilderBase } from \"./search-builder.js\";\nimport type NarouSearchResults from \"./narou-search-results.js\";\nimport type {\n NarouSearchResult,\n SearchResultR18Fields,\n SearchResultOptionalFields,\n} from \"./narou-search-results.js\";\nimport type {\n R18Site,\n SearchResultFieldNames,\n R18Fields,\n OptionalFields,\n} from \"./params.js\";\n\nexport type DefaultR18SearchResultFields = keyof Omit<\n NarouSearchResult,\n \"weekly_unique\" | \"noveltype\" | \"biggenre\" | \"genre\" | \"isr15\" | \"id\"\n>;\n\n/**\n * 18禁API検索ヘルパー\n * @class SearchBuilderR18\n */\nexport default class SearchBuilderR18<\n T extends SearchResultFieldNames = DefaultR18SearchResultFields,\n TOpt extends keyof NarouSearchResult = never\n> extends NovelSearchBuilderBase<T | TOpt> {\n /**\n * なろう小説APIへの検索リクエストを実行する\n * @override\n * @returns {Promise<NarouSearchResults>} 検索結果\n */\n execute(): Promise<NarouSearchResults<NarouSearchResult, T | TOpt>> {\n return this.api.executeNovel18(this.params);\n }\n\n /**\n * 抽出するR18サイトを指定します (nocgenre)。\n * @param sites R18サイトコード、またはR18サイトコードの配列 (1: ノクターンノベルズ, 2: ムーンライトノベルズ(男性向け), 3: ムーンライトノベルズ(BL), 4: ミッドナイトノベルズ)\n * @return {this}\n */\n r18Site(sites: R18Site | readonly R18Site[]) {\n this.set({ nocgenre: NovelSearchBuilderBase.array2string(sites) });\n return this;\n }\n\n /**\n * X-IDを指定して取得します (xid)。\n * @param ids X-ID、またはX-IDの配列\n * @return {this}\n */\n xid(ids: number | readonly number[]) {\n this.set({ xid: NovelSearchBuilderBase.array2string(ids) });\n return this;\n }\n\n /**\n * 出力する項目を個別に指定します (of)。\n * 未指定時は全項目出力されます。転送量軽減のため、このパラメータの使用が推奨されます。\n * @param fields 出力するR18フィールド名、またはR18フィールド名の配列\n * @return {SearchBuilderR18<SearchResultR18Fields<R18Fields>>} 型が更新されたビルダー\n */\n fields<TFields extends R18Fields>(\n fields: TFields | readonly TFields[]\n ): SearchBuilderR18<SearchResultR18Fields<R18Fields>> {\n this.set({ of: NovelSearchBuilderBase.array2string(fields) });\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return this as any;\n }\n\n /**\n * 出力オプション項目を指定します (opt)。\n * @param option 出力するオプションフィールド名、またはオプションフィールド名の配列\n * @return {SearchBuilderR18<T, SearchResultOptionalFields<TFields>>} 型が更新されたビルダー\n */\n opt<TFields extends OptionalFields>(\n option: TFields | readonly TFields[]\n ): SearchBuilderR18<T, SearchResultOptionalFields<TFields>> {\n this.set({ opt: NovelSearchBuilderBase.array2string(option) });\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return this as any;\n }\n}\n","// 日付関連のユーティリティ関数\n\n/**\n * 文字列の日付(yyyyMMdd形式)をDateオブジェクトに変換する\n * @param dateStr yyyyMMdd形式の日付文字列\n * @returns Dateオブジェクト\n */\nexport function parseDate(dateStr: string): Date {\n const year = parseInt(dateStr.substring(0, 4), 10);\n const month = parseInt(dateStr.substring(4, 6), 10) - 1; // JavaScriptの月は0から始まる\n const day = parseInt(dateStr.substring(6, 8), 10);\n\n return new Date(year, month, day, 0, 0, 0, 0);\n}\n\n/**\n * 日付をyyyyMMdd形式の文字列に変換する\n * @param date 日付\n * @returns yyyyMMdd形式の文字列\n */\nexport function formatDate(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n return `${year}${month}${day}`;\n}\n\n/**\n * 指定された日数を加算した新しい日付を返す\n * @param date 元の日付\n * @param days 加算する日数\n * @returns 新しい日付\n */\nexport function addDays(date: Date, days: number): Date {\n const result = new Date(date);\n result.setDate(result.getDate() + days);\n return result;\n}","import type { NarouRankingResult, RankingResult } from \"./narou-ranking-results.js\";\nimport SearchBuilder from \"./search-builder.js\";\nimport type { DefaultSearchResultFields } from \"./search-builder.js\";\nimport type {\n GzipLevel,\n OptionalFields,\n} from \"./params.js\";\nimport {\n RankingParams,\n RankingType,\n Fields,\n} from \"./params.js\";\nimport type NarouNovel from \"./narou.js\";\nimport type { SearchResultFields } from \"./narou-search-results.js\";\nimport { addDays, formatDate } from \"./util/date.js\";\n\n/**\n * なろう小説ランキングAPIのヘルパークラス。\n *\n * ランキング種別や日付を指定してランキングデータを取得します。\n * また、取得したランキングデータに含まれるNコードを元に、\n * なろう小説APIを利用して詳細な小説情報を取得することも可能です。\n *\n * @class RankingBuilder\n * @see https://dev.syosetu.com/man/rankapi/ なろう小説ランキングAPI仕様\n */\nexport default class RankingBuilder {\n /**\n * ランキング集計対象の日付\n * @protected\n */\n protected date$: Date;\n /**\n * ランキング種別\n * @protected\n */\n protected type$: RankingType;\n\n /**\n * constructor\n * @param params - 初期クエリパラメータ\n * @param api - API実行クラスのインスタンス\n * @private\n */\n constructor(\n protected params: Partial<RankingParams> = {},\n protected api: NarouNovel\n ) {\n /**\n * クエリパラメータ\n * @protected\n */\n this.date$ = addDays(new Date(), -1);\n this.type$ = RankingType.Daily;\n }\n\n /**\n * ランキング集計対象の日付を指定します。\n *\n * - 日間: 任意の日付\n * - 週間: 火曜日の日付\n * - 月間・四半期: 1日の日付\n *\n * @param date 集計対象の日付\n * @returns {RankingBuilder} this\n * @see https://dev.syosetu.com/man/rankapi/\n */\n date(date: Date) {\n this.date$ = date;\n return this;\n }\n\n /**\n * ランキング種別を指定します。\n * @param type ランキング種別\n * @returns {RankingBuilder} this\n * @see https://dev.syosetu.com/man/rankapi/\n */\n type(type: RankingType) {\n this.type$ = type;\n return this;\n }\n\n /**\n * gzip圧縮する。\n *\n * 転送量上限を減らすためにも推奨\n * @param {GzipLevel} level gzip圧縮レベル(1~5)\n * @return {RankingBuilder} this\n */\n gzip(level: GzipLevel) {\n this.set({ gzip: level });\n return this;\n }\n\n /**\n * クエリパラメータを内部的にセットします。\n * @param obj - セットするパラメータオブジェクト\n * @returns {RankingBuilder} this\n * @private\n */\n protected set(obj: Partial<RankingParams>) {\n Object.assign(this.params, obj);\n return this;\n }\n\n /**\n * 設定されたパラメータに基づき、なろう小説ランキングAPIへのリクエストを実行します。\n *\n * 返される結果には、Nコード、ポイント、順位が含まれます。\n * @returns {Promise<NarouRankingResult[]>} ランキング結果の配列\n * @see https://dev.syosetu.com/man/rankapi/#output\n */\n execute(): Promise<NarouRankingResult[]> {\n const date = formatDate(this.date$);\n this.set({ rtype: `${date}-${this.type$}` });\n return this.api.executeRanking(this.params as RankingParams);\n }\n\n /**\n * ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。\n */\n async executeWithFields(): Promise<\n RankingResult<DefaultSearchResultFields>[]\n >;\n /**\n