Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ const result = await search("異世界")
.genre(Genre.RenaiIsekai)
.order(Order.FavoriteNovelCount)
.limit(10)
.execute();
.execute({
headers: {
"user-agent": "example-client"
}
});

console.log(`${result.allcount}件の小説が見つかりました`);
```
Expand All @@ -61,6 +65,7 @@ console.log(`${result.allcount}件の小説が見つかりました`);
import { search } from "narou/browser";

const result = await search("魔法").execute();

```

## 📖 詳細な API ドキュメント
Expand Down Expand Up @@ -101,7 +106,11 @@ for (const novel of searchResult.values) {
const rankingResult = await ranking()
.date(new Date("2023-04-01"))
.type(RankingType.Daily)
.execute();
.execute({
headers: {
"user-agent": "example-client",
},
});

for (const novel of rankingResult) {
console.log(novel.ncode);
Expand All @@ -113,7 +122,11 @@ for (const novel of rankingResult) {
const rankingResultWithDetail = await ranking()
.date(new Date("2023-04-01"))
.type(RankingType.Daily)
.executeWithFields();
.executeWithFields({
headers: {
"user-agent": "example-client",
},
});

for (const novel of rankingResultWithDetail) {
console.log(novel.ncode);
Expand Down
9 changes: 5 additions & 4 deletions src/narou-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export default class NarouNovelFetch extends NarouNovel {
}

protected async execute<T>(
params: NarouParams,
endpoint: string
params: NarouParams | undefined,
endpoint: string,
fetchOptions: RequestInit = {}
): Promise<T> {
const query = { ...params, out: "json" };
const query = { ...(params ?? {}), out: "json" };

if (query.gzip === undefined) {
query.gzip = 5;
Expand All @@ -36,7 +37,7 @@ export default class NarouNovelFetch extends NarouNovel {
}
});

const res = await (this.fetch ?? fetch)(url);
const res = await (this.fetch ?? fetch)(url, fetchOptions);

if (!query.gzip) {
return (await res.json()) as T;
Expand Down
8 changes: 5 additions & 3 deletions src/narou-jsonp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { jsonp } from "./util/jsonp.js";
*/
export default class NarouNovelJsonp extends NarouNovel {
protected async execute<T>(
params: NarouParams,
endpoint: string
params: NarouParams | undefined,
endpoint: string,
_fetchOptions?: RequestInit
): Promise<T> {
const query = { ...params, out: "jsonp" };
void _fetchOptions;
const query = { ...(params ?? {}), out: "jsonp" };
query.gzip = 0;

const url = new URL(endpoint);
Expand Down
61 changes: 47 additions & 14 deletions src/narou.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,89 +31,122 @@ export default abstract class NarouNovel {
* なろうAPIへのAPIリクエストを実行する
* @param params クエリパラメータ
* @param endpoint APIエンドポイント
* @param fetchOptions fetchのオプション
* @returns 実行結果
*/
protected abstract execute<T>(
params: NarouParams,
endpoint: string
params: NarouParams | undefined,
endpoint: string,
fetchOptions?: RequestInit
): Promise<T>;

/**
* APIへの検索リクエストを実行する
* @param params クエリパラメータ
* @param endpoint APIエンドポイント
* @param fetchOptions fetchのオプション
* @returns 検索結果
*/
protected async executeSearch<T extends keyof NarouSearchResult>(
params: SearchParams,
endpoint = "https://api.syosetu.com/novelapi/api/"
endpoint = "https://api.syosetu.com/novelapi/api/",
fetchOptions?: RequestInit
): Promise<NarouSearchResults<NarouSearchResult, T>> {
return new NarouSearchResults(await this.execute(params, endpoint), params);
return new NarouSearchResults(
await this.execute(params, endpoint, fetchOptions),
params
);
}

/**
* 小説APIへの検索リクエストを実行する
* @param params クエリパラメータ
* @param fetchOptions fetchのオプション
* @returns 検索結果
* @see https://dev.syosetu.com/man/api/
*/
async executeNovel<T extends keyof NarouSearchResult>(
params: SearchParams
params: SearchParams,
fetchOptions?: RequestInit
): Promise<NarouSearchResults<NarouSearchResult, T>> {
return await this.executeSearch(
params,
"https://api.syosetu.com/novelapi/api/"
"https://api.syosetu.com/novelapi/api/",
fetchOptions
);
}

/**
* R18小説APIへの検索リクエストを実行する
* @param params クエリパラメータ
* @param fetchOptions fetchのオプション
* @returns 検索結果
* @see https://dev.syosetu.com/xman/api/
*/
async executeNovel18<T extends keyof NarouSearchResult>(
params: SearchParams
params: SearchParams,
fetchOptions?: RequestInit
): Promise<NarouSearchResults<NarouSearchResult, T>> {
return await this.executeSearch(
params,
"https://api.syosetu.com/novel18api/api/"
"https://api.syosetu.com/novel18api/api/",
fetchOptions
);
}

/**
* ランキングAPIへのリクエストを実行する
* @param params クエリパラメータ
* @param fetchOptions fetchのオプション
* @returns ランキング結果
* @see https://dev.syosetu.com/man/rankapi/
*/
async executeRanking(params: RankingParams): Promise<NarouRankingResult[]> {
return await this.execute(params, "https://api.syosetu.com/rank/rankget/");
async executeRanking(
params: RankingParams,
fetchOptions?: RequestInit
): Promise<NarouRankingResult[]> {
return await this.execute(
params,
"https://api.syosetu.com/rank/rankget/",
fetchOptions
);
}

/**
* 殿堂入りAPiへのリクエストを実行する
* @param params クエリパラメータ
* @param fetchOptions fetchのオプション
* @returns ランキング履歴結果
* @see https://dev.syosetu.com/man/rankinapi/
*/
async executeRankingHistory(
params: RankingHistoryParams
params: RankingHistoryParams,
fetchOptions?: RequestInit
): Promise<RankingHistoryRawResult[]> {
return await this.execute(params, "https://api.syosetu.com/rank/rankin/");
return await this.execute(
params,
"https://api.syosetu.com/rank/rankin/",
fetchOptions
);
}

/**
* ユーザー検索APIへのリクエストを実行する
* @param params クエリパラメータ
* @param fetchOptions fetchのオプション
* @returns 検索結果
* @see https://dev.syosetu.com/man/userapi/
*/
async executeUserSearch<T extends keyof UserSearchResult>(
params: UserSearchParams
params: UserSearchParams,
fetchOptions?: RequestInit
): Promise<NarouSearchResults<UserSearchResult, T>> {
return new NarouSearchResults<UserSearchResult, T>(
await this.execute(params, "https://api.syosetu.com/userapi/api/"),
await this.execute(
params,
"https://api.syosetu.com/userapi/api/",
fetchOptions
),
params
);
}
Expand Down
54 changes: 38 additions & 16 deletions src/ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import type NarouNovel from "./narou.js";
import type { SearchResultFields } from "./narou-search-results.js";
import { addDays, formatDate } from "./util/date.js";

function isRequestInit(value: unknown): value is RequestInit {
return typeof value == "object" && value !== null && !Array.isArray(value);
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strict equality (===) instead of loose equality (==) for type checking. While this works correctly in this case, using === is a best practice in TypeScript/JavaScript to avoid unexpected type coercion.

Suggested change
return typeof value == "object" && value !== null && !Array.isArray(value);
return typeof value === "object" && value !== null && !Array.isArray(value);

Copilot uses AI. Check for mistakes.
}

/**
* なろう小説ランキングAPIのヘルパークラス。
*
Expand Down Expand Up @@ -108,21 +112,22 @@ export default class RankingBuilder {
* 設定されたパラメータに基づき、なろう小説ランキングAPIへのリクエストを実行します。
*
* 返される結果には、Nコード、ポイント、順位が含まれます。
* @param {RequestInit} [fetchOptions] fetch のオプション
* @returns {Promise<NarouRankingResult[]>} ランキング結果の配列
* @see https://dev.syosetu.com/man/rankapi/#output
*/
execute(): Promise<NarouRankingResult[]> {
execute(fetchOptions?: RequestInit): Promise<NarouRankingResult[]> {
const date = formatDate(this.date$);
this.set({ rtype: `${date}-${this.type$}` });
return this.api.executeRanking(this.params as RankingParams);
return this.api.executeRanking(this.params as RankingParams, fetchOptions);
}

/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
*/
async executeWithFields(): Promise<
RankingResult<DefaultSearchResultFields>[]
>;
async executeWithFields(
fetchOptions?: RequestInit
): Promise<RankingResult<DefaultSearchResultFields>[]>;
/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
*
Expand All @@ -131,7 +136,8 @@ export default class RankingBuilder {
* @returns {Promise<RankingResult<SearchResultFields<TFields>>[]>} 詳細情報を含むランキング結果の配列
*/
async executeWithFields<TFields extends Fields>(
fields: TFields | TFields[]
fields: TFields | TFields[],
fetchOptions?: RequestInit
): Promise<RankingResult<SearchResultFields<TFields>>[]>;
/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
Expand All @@ -141,7 +147,8 @@ export default class RankingBuilder {
*/
async executeWithFields(
fields: never[],
opt: OptionalFields | OptionalFields[]
opt: OptionalFields | OptionalFields[],
fetchOptions?: RequestInit
): Promise<RankingResult<DefaultSearchResultFields | "weekly_unique">[]>;
/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
Expand All @@ -153,7 +160,8 @@ export default class RankingBuilder {
*/
async executeWithFields<TFields extends Fields>(
fields: TFields | TFields[],
opt: OptionalFields | OptionalFields[]
opt: OptionalFields | OptionalFields[],
fetchOptions?: RequestInit
): Promise<RankingResult<SearchResultFields<TFields> | "weekly_unique">[]>;
/**
* ランキングAPIを実行し、取得したNコードを元になろう小説APIで詳細情報を取得して結合します。
Expand All @@ -162,16 +170,30 @@ export default class RankingBuilder {
* @template TOpt - オプショナルな取得フィールドの型
* @param fields - 取得するフィールドの配列 (省略時はデフォルトフィールド)
* @param opt - オプショナルな取得フィールド (`weekly` など)
* @returns {Promise<RankingResult<SearchResultFields<TFields>>[]>} 詳細情報を含むランキング結果の配列
*/
* @param fetchOptions fetch のオプション
* @returns {Promise<RankingResult<SearchResultFields<TFields>>[]>} 詳細情報を含むランキング結果の配列
*/
Comment on lines +173 to +175
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent JSDoc indentation. The @param fetchOptions and @returns tags should be aligned with the other JSDoc tags (no leading spaces). The extra leading spaces before these lines break the standard JSDoc formatting convention.

Suggested change
* @param fetchOptions fetch のオプション
* @returns {Promise<RankingResult<SearchResultFields<TFields>>[]>} 詳細情報を含むランキング結果の配列
*/
* @param fetchOptions fetch のオプション
* @returns {Promise<RankingResult<SearchResultFields<TFields>>[]>} 詳細情報を含むランキング結果の配列
*/

Copilot uses AI. Check for mistakes.
async executeWithFields<
TFields extends Fields,
TOpt extends OptionalFields | undefined = undefined
>(
fields: TFields | TFields[] = [],
opt?: TOpt
fields: TFields | TFields[] | RequestInit = [],
opt?: TOpt | RequestInit,
fetchOptions?: RequestInit
): Promise<RankingResult<SearchResultFields<TFields>>[]> {
const ranking = await this.execute();
let opt$ = opt;
let fetchOptions$ = fetchOptions;

if (isRequestInit(fields)) {
fetchOptions$ = fields;
fields = [] as TFields[];
opt$ = undefined;
} else if (isRequestInit(opt)) {
fetchOptions$ = opt;
opt$ = undefined;
}

const ranking = await this.execute(fetchOptions$);
const fields$ = Array.isArray(fields)
? fields.length == 0
? []
Expand All @@ -181,12 +203,12 @@ export default class RankingBuilder {
const rankingNcodes = ranking.map(({ ncode }) => ncode);
const builder = new SearchBuilder({}, this.api);
builder.fields(fields$);
if (opt) {
builder.opt(opt);
if (opt$) {
builder.opt(opt$);
}
builder.ncode(rankingNcodes);
builder.limit(ranking.length);
const result = await builder.execute();
const result = await builder.execute(fetchOptions$);

return ranking.map<
RankingResult<
Expand Down
7 changes: 5 additions & 2 deletions src/search-builder-r18.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ export default class SearchBuilderR18<
/**
* なろう小説APIへの検索リクエストを実行する
* @override
* @param fetchOptions fetchのオプション
* @returns {Promise<NarouSearchResults>} 検索結果
*/
execute(): Promise<NarouSearchResults<NarouSearchResult, T | TOpt>> {
return this.api.executeNovel18(this.params);
execute(
fetchOptions?: RequestInit
): Promise<NarouSearchResults<NarouSearchResult, T | TOpt>> {
return this.api.executeNovel18(this.params, fetchOptions);
Comment on lines +34 to +37
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the new fetchOptions parameter added to the execute() method. Consider adding a test similar to the one in test/ranking.test.ts:32-54 that verifies fetchOptions (e.g., custom headers) are properly passed through to the underlying API call.

Copilot uses AI. Check for mistakes.
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/search-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,10 +472,11 @@ export abstract class NovelSearchBuilderBase<

/**
* なろう小説APIへの検索リクエストを実行する
* @param fetchOptions fetchのオプション
* @returns {Promise<NarouSearchResults>} 検索結果
*/
execute(): Promise<NarouSearchResults<NarouSearchResult, T>> {
return this.api.executeNovel(this.params);
execute(fetchOptions?: RequestInit): Promise<NarouSearchResults<NarouSearchResult, T>> {
return this.api.executeNovel(this.params, fetchOptions);
}
Comment on lines +478 to 480
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the new fetchOptions parameter added to the execute() method. Consider adding a test similar to the one in test/ranking.test.ts:32-54 that verifies fetchOptions (e.g., custom headers) are properly passed through to the underlying API call.

Copilot uses AI. Check for mistakes.
}

Expand Down
10 changes: 8 additions & 2 deletions src/user-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,15 @@ export default class UserSearchBuilder<

/**
* なろう小説APIへのリクエストを実行する
* @param fetchOptions fetchのオプション
* @returns ランキング
*/
execute(): Promise<NarouSearchResults<UserSearchResult, TField>> {
return this.api.executeUserSearch(this.params as UserSearchParams);
execute(
fetchOptions?: RequestInit
): Promise<NarouSearchResults<UserSearchResult, TField>> {
return this.api.executeUserSearch(
this.params as UserSearchParams,
fetchOptions
);
}
Comment on lines +108 to 115
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the new fetchOptions parameter added to the execute() method. Consider adding a test similar to the one in test/ranking.test.ts:32-54 that verifies fetchOptions (e.g., custom headers) are properly passed through to the underlying API call.

Copilot uses AI. Check for mistakes.
}
Loading
Loading