@book000/pixivts 0.60.0-beta.3 → 0.60.0-beta.5

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/dist/index.cjs CHANGED
@@ -1,1524 +1,1605 @@
1
- 'use strict';
2
-
3
- // src/result.ts
4
- var OkResultImpl = class _OkResultImpl {
5
- constructor(value) {
6
- this.value = value;
7
- }
8
- value;
9
- isOk = true;
10
- isErr = false;
11
- map(fn) {
12
- return new _OkResultImpl(fn(this.value));
13
- }
14
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters, @typescript-eslint/no-unused-vars -- F is part of the public API contract; _fn is intentionally unused (OkResult.mapErr is a no-op)
15
- mapErr(_fn) {
16
- return this;
17
- }
18
- andThen(fn) {
19
- return fn(this.value);
20
- }
21
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- _onErr is intentionally unused: OkResult.match always calls onOk
22
- match(onOk, _onErr) {
23
- return onOk(this.value);
24
- }
25
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- _fallback is intentionally unused: OkResult.unwrapOr always returns value
26
- unwrapOr(_fallback) {
27
- return this.value;
28
- }
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/result.ts
3
+ var OkResultImpl = class OkResultImpl {
4
+ value;
5
+ isOk = true;
6
+ isErr = false;
7
+ constructor(value) {
8
+ this.value = value;
9
+ }
10
+ map(fn) {
11
+ return new OkResultImpl(fn(this.value));
12
+ }
13
+ mapErr(_fn) {
14
+ return this;
15
+ }
16
+ andThen(fn) {
17
+ return fn(this.value);
18
+ }
19
+ match(onOk, _onErr) {
20
+ return onOk(this.value);
21
+ }
22
+ unwrapOr(_fallback) {
23
+ return this.value;
24
+ }
29
25
  };
30
- var ErrResultImpl = class _ErrResultImpl {
31
- // eslint-disable-next-line n/handle-callback-err -- 'error' is a stored value, not a Node.js callback error parameter
32
- constructor(error) {
33
- this.error = error;
34
- }
35
- error;
36
- isOk = false;
37
- isErr = true;
38
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters, @typescript-eslint/no-unused-vars -- U is part of the public API contract; _fn is intentionally unused (ErrResult.map is a no-op)
39
- map(_fn) {
40
- return this;
41
- }
42
- mapErr(fn) {
43
- return new _ErrResultImpl(fn(this.error));
44
- }
45
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- _fn is intentionally unused: ErrResult.andThen is a no-op (the success path does not apply)
46
- andThen(_fn) {
47
- return this;
48
- }
49
- match(_onOk, onErr) {
50
- return onErr(this.error);
51
- }
52
- unwrapOr(fallback) {
53
- return fallback;
54
- }
26
+ var ErrResultImpl = class ErrResultImpl {
27
+ error;
28
+ isOk = false;
29
+ isErr = true;
30
+ constructor(error) {
31
+ this.error = error;
32
+ }
33
+ map(_fn) {
34
+ return this;
35
+ }
36
+ mapErr(fn) {
37
+ return new ErrResultImpl(fn(this.error));
38
+ }
39
+ andThen(_fn) {
40
+ return this;
41
+ }
42
+ match(_onOk, onErr) {
43
+ return onErr(this.error);
44
+ }
45
+ unwrapOr(fallback) {
46
+ return fallback;
47
+ }
55
48
  };
49
+ /**
50
+ * Creates a successful `Result<T, never>`.
51
+ *
52
+ * @param value - The success value
53
+ */
56
54
  function ok(value) {
57
- return new OkResultImpl(value);
55
+ return new OkResultImpl(value);
58
56
  }
57
+ /**
58
+ * Creates a failed `Result<never, E>`.
59
+ *
60
+ * @param error - The error value
61
+ */
59
62
  function err(error) {
60
- return new ErrResultImpl(error);
63
+ return new ErrResultImpl(error);
61
64
  }
62
- var ResultAsync = class _ResultAsync {
63
- _promise;
64
- constructor(promise) {
65
- this._promise = promise;
66
- }
67
- // PromiseLike contract — makes `await resultAsync` work
68
- // eslint-disable-next-line unicorn/no-thenable -- ResultAsync intentionally implements PromiseLike to be directly awaitable
69
- then(onfulfilled, onrejected) {
70
- return this._promise.then(onfulfilled, onrejected);
71
- }
72
- /**
73
- * Wraps a `Promise<T>` into a `ResultAsync<T, E>`.
74
- *
75
- * If the promise rejects, `onError` maps the rejection reason to `E`.
76
- *
77
- * @param promise - The promise to wrap
78
- * @param onError - Error mapper
79
- */
80
- static fromPromise(promise, onError) {
81
- return new _ResultAsync(
82
- promise.then(
83
- (v) => ok(v),
84
- (error) => err(onError(error))
85
- )
86
- );
87
- }
88
- /**
89
- * Wraps an already-resolved `Result<T, E>` into a `ResultAsync<T, E>`.
90
- *
91
- * @param result - The result to wrap
92
- */
93
- static fromResult(result) {
94
- return new _ResultAsync(Promise.resolve(result));
95
- }
96
- /**
97
- * Transforms the success value.
98
- *
99
- * If the inner result is `Err`, `fn` is not called.
100
- *
101
- * @param fn - Synchronous mapper
102
- */
103
- map(fn) {
104
- return new _ResultAsync(
105
- // eslint-disable-next-line unicorn/no-array-callback-reference -- r.map(fn) is safe here; fn is a user-supplied mapper, not a DOM/Array method reference
106
- this._promise.then((r) => r.map(fn))
107
- );
108
- }
109
- /**
110
- * Transforms the error value.
111
- *
112
- * If the inner result is `Ok`, `fn` is not called.
113
- *
114
- * @param fn - Synchronous error mapper
115
- */
116
- mapErr(fn) {
117
- return new _ResultAsync(
118
- this._promise.then((r) => r.mapErr(fn))
119
- );
120
- }
121
- /**
122
- * Chains another async operation that may fail.
123
- *
124
- * If the inner result is `Err`, `fn` is not called.
125
- *
126
- * @param fn - Async mapper that returns a `ResultAsync<U, F>`
127
- */
128
- andThen(fn) {
129
- return new _ResultAsync(
130
- this._promise.then(async (r) => {
131
- if (r.isErr) return r;
132
- const next = fn(r.value);
133
- if (next instanceof _ResultAsync) {
134
- return next._promise;
135
- }
136
- return next;
137
- })
138
- );
139
- }
140
- /**
141
- * Pattern-matches on success / failure.
142
- *
143
- * @param onOk - Called with the success value
144
- * @param onErr - Called with the error value
145
- * @returns A `Promise<U>`
146
- */
147
- async match(onOk, onErr) {
148
- const r = await this._promise;
149
- if (r.isOk) return onOk(r.value);
150
- return onErr(r.error);
151
- }
152
- /**
153
- * Returns the success value, or `fallback` if the result is `Err`.
154
- *
155
- * @param fallback - The fallback value
156
- */
157
- async unwrapOr(fallback) {
158
- const r = await this._promise;
159
- if (r.isOk) return r.value;
160
- return fallback;
161
- }
65
+ /**
66
+ * A `PromiseLike<Result<T, E>>` that is directly `await`-able and supports
67
+ * chainable `map / mapErr / andThen` operators.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const result = await ResultAsync.fromPromise(fetch('/api'), networkError)
72
+ * .andThen((res) =>
73
+ * ResultAsync.fromPromise(res.json() as Promise<unknown>, networkError)
74
+ * )
75
+ * ```
76
+ */
77
+ var ResultAsync = class ResultAsync {
78
+ _promise;
79
+ constructor(promise) {
80
+ this._promise = promise;
81
+ }
82
+ then(onfulfilled, onrejected) {
83
+ return this._promise.then(onfulfilled, onrejected);
84
+ }
85
+ /**
86
+ * Wraps a `Promise<T>` into a `ResultAsync<T, E>`.
87
+ *
88
+ * If the promise rejects, `onError` maps the rejection reason to `E`.
89
+ *
90
+ * @param promise - The promise to wrap
91
+ * @param onError - Error mapper
92
+ */
93
+ static fromPromise(promise, onError) {
94
+ return new ResultAsync(promise.then((v) => ok(v), (error) => err(onError(error))));
95
+ }
96
+ /**
97
+ * Wraps an already-resolved `Result<T, E>` into a `ResultAsync<T, E>`.
98
+ *
99
+ * @param result - The result to wrap
100
+ */
101
+ static fromResult(result) {
102
+ return new ResultAsync(Promise.resolve(result));
103
+ }
104
+ /**
105
+ * Transforms the success value.
106
+ *
107
+ * If the inner result is `Err`, `fn` is not called.
108
+ *
109
+ * @param fn - Synchronous mapper
110
+ */
111
+ map(fn) {
112
+ return new ResultAsync(this._promise.then((r) => r.map(fn)));
113
+ }
114
+ /**
115
+ * Transforms the error value.
116
+ *
117
+ * If the inner result is `Ok`, `fn` is not called.
118
+ *
119
+ * @param fn - Synchronous error mapper
120
+ */
121
+ mapErr(fn) {
122
+ return new ResultAsync(this._promise.then((r) => r.mapErr(fn)));
123
+ }
124
+ /**
125
+ * Chains another async operation that may fail.
126
+ *
127
+ * If the inner result is `Err`, `fn` is not called.
128
+ *
129
+ * @param fn - Async mapper that returns a `ResultAsync<U, F>`
130
+ */
131
+ andThen(fn) {
132
+ return new ResultAsync(this._promise.then(async (r) => {
133
+ if (r.isErr) return r;
134
+ const next = fn(r.value);
135
+ if (next instanceof ResultAsync) return next._promise;
136
+ return next;
137
+ }));
138
+ }
139
+ /**
140
+ * Pattern-matches on success / failure.
141
+ *
142
+ * @param onOk - Called with the success value
143
+ * @param onErr - Called with the error value
144
+ * @returns A `Promise<U>`
145
+ */
146
+ async match(onOk, onErr) {
147
+ const r = await this._promise;
148
+ if (r.isOk) return onOk(r.value);
149
+ return onErr(r.error);
150
+ }
151
+ /**
152
+ * Returns the success value, or `fallback` if the result is `Err`.
153
+ *
154
+ * @param fallback - The fallback value
155
+ */
156
+ async unwrapOr(fallback) {
157
+ const r = await this._promise;
158
+ if (r.isOk) return r.value;
159
+ return fallback;
160
+ }
162
161
  };
163
-
164
- // src/errors.ts
162
+ //#endregion
163
+ //#region src/errors.ts
164
+ /**
165
+ * An `Error` subclass that wraps a `PixivError` for use in thrown contexts
166
+ * (e.g. async generators that must throw proper `Error` objects).
167
+ *
168
+ * All `PixivError` properties are spread directly onto this instance so that
169
+ * callers can use `instanceof PixivFetchError` or access `error.type` etc.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * try {
174
+ * for await (const page of result.pages()) { ... }
175
+ * } catch (e) {
176
+ * if (e instanceof PixivFetchError) {
177
+ * console.error(e.pixivError.type)
178
+ * }
179
+ * }
180
+ * ```
181
+ */
165
182
  var PixivFetchError = class extends Error {
166
- /** The underlying structured `PixivError`. */
167
- pixivError;
168
- constructor(pixivError) {
169
- super(`pixiv API error: ${pixivError.type}`);
170
- this.name = "PixivFetchError";
171
- this.pixivError = pixivError;
172
- Object.assign(this, pixivError);
173
- }
183
+ /** The underlying structured `PixivError`. */
184
+ pixivError;
185
+ constructor(pixivError) {
186
+ super(`pixiv API error: ${pixivError.type}`);
187
+ this.name = "PixivFetchError";
188
+ this.pixivError = pixivError;
189
+ Object.assign(this, pixivError);
190
+ }
174
191
  };
192
+ /** Creates a rate-limit error. */
175
193
  function rateLimitError(retryAfter) {
176
- return { type: "rate_limit", retryAfter };
194
+ return {
195
+ type: "rate_limit",
196
+ retryAfter
197
+ };
177
198
  }
199
+ /** Creates an auth-failed error. */
178
200
  function authFailedError(status) {
179
- return { type: "auth_failed", status };
201
+ return {
202
+ type: "auth_failed",
203
+ status
204
+ };
180
205
  }
206
+ /** Creates a network error. */
181
207
  function networkError(cause) {
182
- return { type: "network", cause };
208
+ return {
209
+ type: "network",
210
+ cause
211
+ };
183
212
  }
213
+ /** Creates an API error. */
184
214
  function apiError(status, body) {
185
- return { type: "api_error", status, body };
215
+ return {
216
+ type: "api_error",
217
+ status,
218
+ body
219
+ };
186
220
  }
187
-
188
- // src/paginated.ts
189
- var PaginatedResultAsync = class _PaginatedResultAsync extends ResultAsync {
190
- #http;
191
- #getItems;
192
- constructor(promise, http, getItems) {
193
- super(promise);
194
- this.#http = http;
195
- this.#getItems = getItems;
196
- }
197
- /**
198
- * Creates a `PaginatedResultAsync` from a `ResultAsync`.
199
- *
200
- * @param inner - The first-page result
201
- * @param http - HTTP client for fetching subsequent pages
202
- * @param getItems - Extracts item array from a page
203
- */
204
- static fromResultAsync(inner, http, getItems) {
205
- const promise = Promise.resolve(inner);
206
- return new _PaginatedResultAsync(promise, http, getItems);
207
- }
208
- /**
209
- * Async generator that yields each page starting from the first.
210
- *
211
- * If any page fetch fails, the generator throws a `PixivFetchError`.
212
- *
213
- * @example
214
- * ```ts
215
- * for await (const page of client.illusts.search({ word: 'cat' }).pages()) {
216
- * console.log(page.illusts.length)
217
- * }
218
- * ```
219
- */
220
- async *pages() {
221
- const first = await Promise.resolve(this);
222
- if (first.isErr) throw new PixivFetchError(first.error);
223
- yield first.value;
224
- let nextUrl = first.value.nextUrl;
225
- while (nextUrl !== null) {
226
- const pageResult = await this.#http.getAbsolute(nextUrl);
227
- if (pageResult.isErr) throw new PixivFetchError(pageResult.error);
228
- yield pageResult.value;
229
- nextUrl = pageResult.value.nextUrl;
230
- }
231
- }
232
- /**
233
- * Async generator that yields individual items across all pages.
234
- *
235
- * If any page fetch fails, the generator throws a `PixivFetchError`.
236
- *
237
- * @example
238
- * ```ts
239
- * for await (const illust of client.illusts.search({ word: 'cat' }).items()) {
240
- * console.log(illust.title)
241
- * }
242
- * ```
243
- */
244
- async *items() {
245
- for await (const page of this.pages()) {
246
- for (const item of this.#getItems(page)) {
247
- yield item;
248
- }
249
- }
250
- }
221
+ //#endregion
222
+ //#region src/paginated.ts
223
+ /**
224
+ * A `ResultAsync<TPage, PixivError>` with additional `.pages()` / `.items()`
225
+ * async generators for consuming paginated pixiv list responses.
226
+ *
227
+ * Returned by all resource methods that produce a `nextUrl` field.
228
+ */
229
+ var PaginatedResultAsync = class PaginatedResultAsync extends ResultAsync {
230
+ #http;
231
+ #getItems;
232
+ constructor(promise, http, getItems) {
233
+ super(promise);
234
+ this.#http = http;
235
+ this.#getItems = getItems;
236
+ }
237
+ /**
238
+ * Creates a `PaginatedResultAsync` from a `ResultAsync`.
239
+ *
240
+ * @param inner - The first-page result
241
+ * @param http - HTTP client for fetching subsequent pages
242
+ * @param getItems - Extracts item array from a page
243
+ */
244
+ static fromResultAsync(inner, http, getItems) {
245
+ return new PaginatedResultAsync(Promise.resolve(inner), http, getItems);
246
+ }
247
+ /**
248
+ * Async generator that yields each page starting from the first.
249
+ *
250
+ * If any page fetch fails, the generator throws a `PixivFetchError`.
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * for await (const page of client.illusts.search({ word: 'cat' }).pages()) {
255
+ * console.log(page.illusts.length)
256
+ * }
257
+ * ```
258
+ */
259
+ async *pages() {
260
+ const first = await Promise.resolve(this);
261
+ if (first.isErr) throw new PixivFetchError(first.error);
262
+ yield first.value;
263
+ let nextUrl = first.value.nextUrl;
264
+ while (nextUrl !== null) {
265
+ const pageResult = await this.#http.getAbsolute(nextUrl);
266
+ if (pageResult.isErr) throw new PixivFetchError(pageResult.error);
267
+ yield pageResult.value;
268
+ nextUrl = pageResult.value.nextUrl;
269
+ }
270
+ }
271
+ /**
272
+ * Async generator that yields individual items across all pages.
273
+ *
274
+ * If any page fetch fails, the generator throws a `PixivFetchError`.
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * for await (const illust of client.illusts.search({ word: 'cat' }).items()) {
279
+ * console.log(illust.title)
280
+ * }
281
+ * ```
282
+ */
283
+ async *items() {
284
+ for await (const page of this.pages()) for (const item of this.#getItems(page)) yield item;
285
+ }
251
286
  };
287
+ /**
288
+ * Creates an immediately-failed `PaginatedResultAsync`.
289
+ *
290
+ * Useful when validation or auth fails before any HTTP request is made.
291
+ *
292
+ * @param error - The error to return
293
+ * @param http - HTTP client (used for signature compatibility)
294
+ * @param getItems - Item extractor (used for signature compatibility)
295
+ */
252
296
  function failedPaginated(error, http, getItems) {
253
- return new PaginatedResultAsync(
254
- Promise.resolve(err(error)),
255
- http,
256
- getItems
257
- );
297
+ return new PaginatedResultAsync(Promise.resolve(err(error)), http, getItems);
258
298
  }
259
-
260
- // src/params.ts
299
+ //#endregion
300
+ //#region src/params.ts
301
+ /**
302
+ * Parameter building utilities.
303
+ *
304
+ * Provides camelCase → snake_case conversion and a URLSearchParams builder
305
+ * that replicates the behaviour of the `qs` library used in the legacy code
306
+ * (without the `qs` runtime dependency).
307
+ */
308
+ /**
309
+ * Converts a camelCase string to snake_case.
310
+ *
311
+ * @example
312
+ * camelToSnake('illustId') // 'illust_id'
313
+ * camelToSnake('searchAiType') // 'search_ai_type'
314
+ *
315
+ * @param key - camelCase string
316
+ * @returns snake_case string
317
+ */
261
318
  function camelToSnake(key) {
262
- return key.replaceAll(/([A-Z])/g, (m) => `_${m.toLowerCase()}`);
319
+ return key.replaceAll(/([A-Z])/g, (m) => `_${m.toLowerCase()}`);
263
320
  }
321
+ /**
322
+ * Converts all keys of a plain object from camelCase to snake_case, shallow.
323
+ *
324
+ * Values are preserved as-is; nested objects are NOT recursed into.
325
+ *
326
+ * @param obj - Object with camelCase keys
327
+ * @returns New object with snake_case keys
328
+ */
264
329
  function toSnakeKeys(obj) {
265
- const out = {};
266
- for (const key of Object.keys(obj)) {
267
- out[camelToSnake(key)] = obj[key];
268
- }
269
- return out;
330
+ const out = {};
331
+ for (const key of Object.keys(obj)) out[camelToSnake(key)] = obj[key];
332
+ return out;
270
333
  }
334
+ /**
335
+ * Serialises a record of query parameters into a `URLSearchParams` instance.
336
+ *
337
+ * Rules:
338
+ * - `null` / `undefined` values are skipped.
339
+ * - Arrays are appended with a `[]` suffix: `foo[]=1&foo[]=2` (Rails/pixiv convention).
340
+ * - Booleans are serialised as `'true'` / `'false'`.
341
+ * - Numbers are serialised via `.toString()`.
342
+ *
343
+ * @param params - Key/value pairs to serialise
344
+ * @returns Populated `URLSearchParams`
345
+ */
271
346
  function buildSearchParams(params) {
272
- const usp = new URLSearchParams();
273
- for (const [key, value] of Object.entries(params)) {
274
- if (value === null || value === void 0) continue;
275
- if (Array.isArray(value)) {
276
- for (const item of value) usp.append(`${key}[]`, String(item));
277
- } else {
278
- usp.set(key, String(value));
279
- }
280
- }
281
- return usp;
347
+ const usp = new URLSearchParams();
348
+ for (const [key, value] of Object.entries(params)) {
349
+ if (value === null || value === void 0) continue;
350
+ if (Array.isArray(value)) for (const item of value) usp.append(`${key}[]`, String(item));
351
+ else usp.set(key, String(value));
352
+ }
353
+ return usp;
282
354
  }
355
+ /**
356
+ * Merges camelCase→snake_case conversion with URLSearchParams building.
357
+ *
358
+ * Convenience wrapper used by resource methods.
359
+ *
360
+ * @param params - camelCase record
361
+ * @returns Populated `URLSearchParams` with snake_case keys
362
+ */
283
363
  function buildParams(params) {
284
- return buildSearchParams(toSnakeKeys(params));
364
+ return buildSearchParams(toSnakeKeys(params));
285
365
  }
366
+ /**
367
+ * Converts a snake_case string to lowerCamelCase.
368
+ *
369
+ * Already-camelCase keys pass through unchanged (idempotent).
370
+ *
371
+ * @example
372
+ * snakeToCamel('image_urls') // 'imageUrls'
373
+ * snakeToCamel('x_restrict') // 'xRestrict'
374
+ * snakeToCamel('imageUrls') // 'imageUrls' (no-op)
375
+ *
376
+ * @param key - snake_case string
377
+ * @returns lowerCamelCase string
378
+ */
286
379
  function snakeToCamel(key) {
287
- return key.replaceAll(/_([a-z0-9])/g, (_m, c) => c.toUpperCase());
380
+ return key.replaceAll(/_([a-z0-9])/g, (_m, c) => c.toUpperCase());
288
381
  }
382
+ /**
383
+ * Recursively converts all object keys from snake_case to lowerCamelCase.
384
+ *
385
+ * - Plain objects: keys are converted, values are recursed.
386
+ * - Arrays: each element is recursed.
387
+ * - Primitives / null: returned as-is.
388
+ *
389
+ * All keys are converted uniformly — no path-based exclusions.
390
+ *
391
+ * @param value - Any JSON-compatible value
392
+ * @returns Deep copy with all object keys in lowerCamelCase
393
+ */
289
394
  function camelizeKeys(value) {
290
- if (value === null || typeof value !== "object") return value;
291
- if (Array.isArray(value)) return value.map((v) => camelizeKeys(v));
292
- const out = {};
293
- for (const [k, v] of Object.entries(value)) {
294
- out[snakeToCamel(k)] = camelizeKeys(v);
295
- }
296
- return out;
395
+ if (value === null || typeof value !== "object") return value;
396
+ if (Array.isArray(value)) return value.map((v) => camelizeKeys(v));
397
+ const out = {};
398
+ for (const [k, v] of Object.entries(value)) out[snakeToCamel(k)] = camelizeKeys(v);
399
+ return out;
297
400
  }
401
+ /**
402
+ * Parses a pixiv `next_url` into a typed cursor object.
403
+ *
404
+ * Pass the `next_url` field from any paginated response to extract the
405
+ * cursor parameters needed to resume pagination from a saved position.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * const page = await client.users.bookmarks.illusts({ userId: client.userId })
410
+ * if (page.isOk && page.value.nextUrl) {
411
+ * const cursor = parseNextUrl(page.value.nextUrl)
412
+ * // Resume later:
413
+ * const next = await client.users.bookmarks.illusts({
414
+ * userId: client.userId,
415
+ * maxBookmarkId: cursor.maxBookmarkId,
416
+ * })
417
+ * }
418
+ * ```
419
+ *
420
+ * @param url - The `next_url` string returned by a pixiv list endpoint
421
+ * @returns Typed cursor parameters; fields absent in the URL are `undefined`
422
+ */
298
423
  function parseNextUrl(url) {
299
- const usp = new URL(url).searchParams;
300
- const result = {};
301
- const toNum = (key) => {
302
- const v = usp.get(key);
303
- if (v === null || v === "") return void 0;
304
- const n = Number(v);
305
- return Number.isNaN(n) ? void 0 : n;
306
- };
307
- const maxBookmarkId = toNum("max_bookmark_id");
308
- if (maxBookmarkId !== void 0) result.maxBookmarkId = maxBookmarkId;
309
- const maxBookmarkIdForRecommend = toNum("max_bookmark_id_for_recommend");
310
- if (maxBookmarkIdForRecommend !== void 0)
311
- result.maxBookmarkIdForRecommend = maxBookmarkIdForRecommend;
312
- const minBookmarkIdForRecentIllust = toNum("min_bookmark_id_for_recent_illust");
313
- if (minBookmarkIdForRecentIllust !== void 0)
314
- result.minBookmarkIdForRecentIllust = minBookmarkIdForRecentIllust;
315
- const offset = toNum("offset");
316
- if (offset !== void 0) result.offset = offset;
317
- const lastOrder = toNum("last_order");
318
- if (lastOrder !== void 0) result.lastOrder = lastOrder;
319
- return result;
424
+ const usp = new URL(url).searchParams;
425
+ const result = {};
426
+ const toNum = (key) => {
427
+ const v = usp.get(key);
428
+ if (v === null || v === "") return void 0;
429
+ const n = Number(v);
430
+ return Number.isNaN(n) ? void 0 : n;
431
+ };
432
+ const maxBookmarkId = toNum("max_bookmark_id");
433
+ if (maxBookmarkId !== void 0) result.maxBookmarkId = maxBookmarkId;
434
+ const maxBookmarkIdForRecommend = toNum("max_bookmark_id_for_recommend");
435
+ if (maxBookmarkIdForRecommend !== void 0) result.maxBookmarkIdForRecommend = maxBookmarkIdForRecommend;
436
+ const minBookmarkIdForRecentIllust = toNum("min_bookmark_id_for_recent_illust");
437
+ if (minBookmarkIdForRecentIllust !== void 0) result.minBookmarkIdForRecentIllust = minBookmarkIdForRecentIllust;
438
+ const offset = toNum("offset");
439
+ if (offset !== void 0) result.offset = offset;
440
+ const lastOrder = toNum("last_order");
441
+ if (lastOrder !== void 0) result.lastOrder = lastOrder;
442
+ return result;
320
443
  }
321
-
322
- // src/options.ts
323
- var SearchTarget = {
324
- PARTIAL_MATCH_FOR_TAGS: "partial_match_for_tags",
325
- EXACT_MATCH_FOR_TAGS: "exact_match_for_tags",
326
- TITLE_AND_CAPTION: "title_and_caption",
327
- KEYWORD: "keyword"
444
+ //#endregion
445
+ //#region src/options.ts
446
+ /**
447
+ * Public option constants for @book000/pixivts.
448
+ *
449
+ * Each option is exported as a runtime `const` object for enum-like access
450
+ * (e.g. `BookmarkRestrict.PUBLIC`). Plain string literals are also accepted
451
+ * wherever these values are used as parameters.
452
+ *
453
+ * @example
454
+ * ```ts
455
+ * // Enum-like usage
456
+ * await client.illusts.bookmarkAdd({ illustId: 123, restrict: BookmarkRestrict.PUBLIC })
457
+ *
458
+ * // Plain string literal — also valid
459
+ * await client.illusts.bookmarkAdd({ illustId: 123, restrict: 'public' })
460
+ * ```
461
+ */
462
+ /**
463
+ * Search match target for illust / novel searches.
464
+ *
465
+ * - `partial_match_for_tags` — tags contain the word (default)
466
+ * - `exact_match_for_tags` — tags exactly equal the word
467
+ * - `title_and_caption` — title or caption contains the word
468
+ * - `keyword` — general keyword search (novel only)
469
+ */
470
+ const SearchTarget = {
471
+ PARTIAL_MATCH_FOR_TAGS: "partial_match_for_tags",
472
+ EXACT_MATCH_FOR_TAGS: "exact_match_for_tags",
473
+ TITLE_AND_CAPTION: "title_and_caption",
474
+ KEYWORD: "keyword"
328
475
  };
329
- var SearchSort = {
330
- DATE_DESC: "date_desc",
331
- DATE_ASC: "date_asc",
332
- POPULAR_DESC: "popular_desc"
476
+ /**
477
+ * Sort order for search results.
478
+ *
479
+ * - `date_desc` — newest first (default)
480
+ * - `date_asc` — oldest first
481
+ * - `popular_desc` — most bookmarks first (premium only)
482
+ */
483
+ const SearchSort = {
484
+ DATE_DESC: "date_desc",
485
+ DATE_ASC: "date_asc",
486
+ POPULAR_DESC: "popular_desc"
333
487
  };
334
- var SearchDuration = {
335
- WITHIN_LAST_DAY: "within_last_day",
336
- WITHIN_LAST_WEEK: "within_last_week",
337
- WITHIN_LAST_MONTH: "within_last_month"
488
+ /**
489
+ * Date range filter for search results.
490
+ *
491
+ * - `within_last_day` — past 24 hours
492
+ * - `within_last_week` — past 7 days
493
+ * - `within_last_month` — past 30 days
494
+ */
495
+ const SearchDuration = {
496
+ WITHIN_LAST_DAY: "within_last_day",
497
+ WITHIN_LAST_WEEK: "within_last_week",
498
+ WITHIN_LAST_MONTH: "within_last_month"
338
499
  };
339
- var RankingMode = {
340
- DAY: "day",
341
- DAY_MALE: "day_male",
342
- DAY_FEMALE: "day_female",
343
- WEEK_ORIGINAL: "week_original",
344
- WEEK_ROOKIE: "week_rookie",
345
- WEEK: "week",
346
- MONTH: "month",
347
- DAY_AI: "day_ai",
348
- DAY_R18: "day_r18",
349
- WEEK_R18: "week_r18",
350
- DAY_MALE_R18: "day_male_r18",
351
- DAY_FEMALE_R18: "day_female_r18",
352
- DAY_R18_AI: "day_r18_ai"
500
+ /**
501
+ * Ranking mode for illust rankings.
502
+ *
503
+ * R-18 modes require a premium account with R-18 content enabled.
504
+ */
505
+ const RankingMode = {
506
+ DAY: "day",
507
+ DAY_MALE: "day_male",
508
+ DAY_FEMALE: "day_female",
509
+ WEEK_ORIGINAL: "week_original",
510
+ WEEK_ROOKIE: "week_rookie",
511
+ WEEK: "week",
512
+ MONTH: "month",
513
+ DAY_AI: "day_ai",
514
+ DAY_R18: "day_r18",
515
+ WEEK_R18: "week_r18",
516
+ DAY_MALE_R18: "day_male_r18",
517
+ DAY_FEMALE_R18: "day_female_r18",
518
+ DAY_R18_AI: "day_r18_ai"
353
519
  };
354
- var NovelRankingMode = {
355
- DAY: "day",
356
- WEEK: "week",
357
- DAY_MALE: "day_male",
358
- DAY_FEMALE: "day_female",
359
- WEEK_ROOKIE: "week_rookie",
360
- DAY_R18: "day_r18",
361
- WEEK_R18: "week_r18",
362
- DAY_R18_AI: "day_r18_ai"
520
+ /**
521
+ * Ranking mode for novel rankings.
522
+ *
523
+ * R-18 modes require a premium account with R-18 content enabled.
524
+ */
525
+ const NovelRankingMode = {
526
+ DAY: "day",
527
+ WEEK: "week",
528
+ DAY_MALE: "day_male",
529
+ DAY_FEMALE: "day_female",
530
+ WEEK_ROOKIE: "week_rookie",
531
+ DAY_R18: "day_r18",
532
+ WEEK_R18: "week_r18",
533
+ DAY_R18_AI: "day_r18_ai"
363
534
  };
364
- var BookmarkRestrict = {
365
- PUBLIC: "public",
366
- PRIVATE: "private"
535
+ /**
536
+ * Visibility restriction for bookmarks.
537
+ *
538
+ * - `public` — publicly visible (default)
539
+ * - `private` — visible only to the owner
540
+ */
541
+ const BookmarkRestrict = {
542
+ PUBLIC: "public",
543
+ PRIVATE: "private"
367
544
  };
368
- var FollowRestrict = {
369
- PUBLIC: "public",
370
- PRIVATE: "private"
545
+ /**
546
+ * Visibility restriction for follows.
547
+ *
548
+ * - `public` — publicly visible (default)
549
+ * - `private` — visible only to the owner
550
+ */
551
+ const FollowRestrict = {
552
+ PUBLIC: "public",
553
+ PRIVATE: "private"
371
554
  };
372
- var OSFilter = {
373
- FOR_IOS: "for_ios",
374
- FOR_ANDROID: "for_android"
555
+ /**
556
+ * OS filter used to request works compatible with the given platform.
557
+ *
558
+ * - `for_ios` — iOS-compatible works (default)
559
+ * - `for_android` — Android-compatible works
560
+ */
561
+ const OSFilter = {
562
+ FOR_IOS: "for_ios",
563
+ FOR_ANDROID: "for_android"
375
564
  };
376
- var UserIllustType = {
377
- ILLUST: "illust",
378
- MANGA: "manga"
565
+ /**
566
+ * Work type filter for user illust listings.
567
+ *
568
+ * - `illust` — illustrations only
569
+ * - `manga` — manga only
570
+ */
571
+ const UserIllustType = {
572
+ ILLUST: "illust",
573
+ MANGA: "manga"
379
574
  };
380
-
381
- // src/auth.ts
382
- var T = Array.from(
383
- { length: 64 },
384
- (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32)
385
- );
386
- var S = [
387
- 7,
388
- 12,
389
- 17,
390
- 22,
391
- 7,
392
- 12,
393
- 17,
394
- 22,
395
- 7,
396
- 12,
397
- 17,
398
- 22,
399
- 7,
400
- 12,
401
- 17,
402
- 22,
403
- 5,
404
- 9,
405
- 14,
406
- 20,
407
- 5,
408
- 9,
409
- 14,
410
- 20,
411
- 5,
412
- 9,
413
- 14,
414
- 20,
415
- 5,
416
- 9,
417
- 14,
418
- 20,
419
- 4,
420
- 11,
421
- 16,
422
- 23,
423
- 4,
424
- 11,
425
- 16,
426
- 23,
427
- 4,
428
- 11,
429
- 16,
430
- 23,
431
- 4,
432
- 11,
433
- 16,
434
- 23,
435
- 6,
436
- 10,
437
- 15,
438
- 21,
439
- 6,
440
- 10,
441
- 15,
442
- 21,
443
- 6,
444
- 10,
445
- 15,
446
- 21,
447
- 6,
448
- 10,
449
- 15,
450
- 21
575
+ //#endregion
576
+ //#region src/auth.ts
577
+ const T = Array.from({ length: 64 }, (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32));
578
+ const S = [
579
+ 7,
580
+ 12,
581
+ 17,
582
+ 22,
583
+ 7,
584
+ 12,
585
+ 17,
586
+ 22,
587
+ 7,
588
+ 12,
589
+ 17,
590
+ 22,
591
+ 7,
592
+ 12,
593
+ 17,
594
+ 22,
595
+ 5,
596
+ 9,
597
+ 14,
598
+ 20,
599
+ 5,
600
+ 9,
601
+ 14,
602
+ 20,
603
+ 5,
604
+ 9,
605
+ 14,
606
+ 20,
607
+ 5,
608
+ 9,
609
+ 14,
610
+ 20,
611
+ 4,
612
+ 11,
613
+ 16,
614
+ 23,
615
+ 4,
616
+ 11,
617
+ 16,
618
+ 23,
619
+ 4,
620
+ 11,
621
+ 16,
622
+ 23,
623
+ 4,
624
+ 11,
625
+ 16,
626
+ 23,
627
+ 6,
628
+ 10,
629
+ 15,
630
+ 21,
631
+ 6,
632
+ 10,
633
+ 15,
634
+ 21,
635
+ 6,
636
+ 10,
637
+ 15,
638
+ 21,
639
+ 6,
640
+ 10,
641
+ 15,
642
+ 21
451
643
  ];
452
644
  function md5Bytes(bytes) {
453
- const len = bytes.length;
454
- bytes.push(128);
455
- while (bytes.length % 64 !== 56) bytes.push(0);
456
- const bitLen = len * 8;
457
- for (let i = 0; i < 8; i++) {
458
- bytes.push(i < 4 ? bitLen >>> i * 8 & 255 : 0);
459
- }
460
- let a = 1732584193;
461
- let b = 4023233417;
462
- let c = 2562383102;
463
- let d = 271733878;
464
- for (let chunk = 0; chunk < bytes.length; chunk += 64) {
465
- const M = [];
466
- for (let w = 0; w < 16; w++) {
467
- const off = chunk + w * 4;
468
- M.push(
469
- bytes[off] | bytes[off + 1] << 8 | bytes[off + 2] << 16 | bytes[off + 3] << 24
470
- );
471
- }
472
- let aa = a;
473
- let bb = b;
474
- let cc = c;
475
- let dd = d;
476
- for (let i = 0; i < 64; i++) {
477
- let f;
478
- let g;
479
- if (i < 16) {
480
- f = bb & cc | ~bb & dd;
481
- g = i;
482
- } else if (i < 32) {
483
- f = dd & bb | ~dd & cc;
484
- g = (5 * i + 1) % 16;
485
- } else if (i < 48) {
486
- f = bb ^ cc ^ dd;
487
- g = (3 * i + 5) % 16;
488
- } else {
489
- f = cc ^ (bb | ~dd);
490
- g = 7 * i % 16;
491
- }
492
- const tmp = dd;
493
- dd = cc;
494
- cc = bb;
495
- const sum = Math.trunc(aa + f + M[g] + T[i]);
496
- const rotated = sum << S[i] | sum >>> 32 - S[i];
497
- bb = Math.trunc(bb + rotated);
498
- aa = tmp;
499
- }
500
- a = Math.trunc(a + aa);
501
- b = Math.trunc(b + bb);
502
- c = Math.trunc(c + cc);
503
- d = Math.trunc(d + dd);
504
- }
505
- return [a, b, c, d].map(
506
- (n) => [n & 255, n >>> 8 & 255, n >>> 16 & 255, n >>> 24 & 255].map((byte) => byte.toString(16).padStart(2, "0")).join("")
507
- ).join("");
645
+ const len = bytes.length;
646
+ bytes.push(128);
647
+ while (bytes.length % 64 !== 56) bytes.push(0);
648
+ const bitLen = len * 8;
649
+ for (let i = 0; i < 8; i++) bytes.push(i < 4 ? bitLen >>> i * 8 & 255 : 0);
650
+ let a = 1732584193;
651
+ let b = 4023233417;
652
+ let c = 2562383102;
653
+ let d = 271733878;
654
+ for (let chunk = 0; chunk < bytes.length; chunk += 64) {
655
+ const M = [];
656
+ for (let w = 0; w < 16; w++) {
657
+ const off = chunk + w * 4;
658
+ M.push(bytes[off] | bytes[off + 1] << 8 | bytes[off + 2] << 16 | bytes[off + 3] << 24);
659
+ }
660
+ let aa = a;
661
+ let bb = b;
662
+ let cc = c;
663
+ let dd = d;
664
+ for (let i = 0; i < 64; i++) {
665
+ let f;
666
+ let g;
667
+ if (i < 16) {
668
+ f = bb & cc | ~bb & dd;
669
+ g = i;
670
+ } else if (i < 32) {
671
+ f = dd & bb | ~dd & cc;
672
+ g = (5 * i + 1) % 16;
673
+ } else if (i < 48) {
674
+ f = bb ^ cc ^ dd;
675
+ g = (3 * i + 5) % 16;
676
+ } else {
677
+ f = cc ^ (bb | ~dd);
678
+ g = 7 * i % 16;
679
+ }
680
+ const tmp = dd;
681
+ dd = cc;
682
+ cc = bb;
683
+ const sum = Math.trunc(aa + f + M[g] + T[i]);
684
+ const rotated = sum << S[i] | sum >>> 32 - S[i];
685
+ bb = Math.trunc(bb + rotated);
686
+ aa = tmp;
687
+ }
688
+ a = Math.trunc(a + aa);
689
+ b = Math.trunc(b + bb);
690
+ c = Math.trunc(c + cc);
691
+ d = Math.trunc(d + dd);
692
+ }
693
+ return [
694
+ a,
695
+ b,
696
+ c,
697
+ d
698
+ ].map((n) => [
699
+ n & 255,
700
+ n >>> 8 & 255,
701
+ n >>> 16 & 255,
702
+ n >>> 24 & 255
703
+ ].map((byte) => byte.toString(16).padStart(2, "0")).join("")).join("");
508
704
  }
705
+ /**
706
+ * Produces a hex-encoded MD5 digest of `input`.
707
+ *
708
+ * This is a minimal, spec-compliant implementation (RFC 1321) that avoids
709
+ * any runtime platform dependency.
710
+ *
711
+ * @param input - UTF-8 string to hash
712
+ * @returns Lowercase hex-encoded MD5 digest
713
+ */
509
714
  function md5(input) {
510
- const bytes = [];
511
- for (let i = 0; i < input.length; i++) {
512
- const code = input.codePointAt(i) ?? 0;
513
- if (code < 128) {
514
- bytes.push(code);
515
- } else if (code < 2048) {
516
- bytes.push(192 | code >> 6, 128 | code & 63);
517
- } else {
518
- bytes.push(
519
- 224 | code >> 12,
520
- 128 | code >> 6 & 63,
521
- 128 | code & 63
522
- );
523
- }
524
- }
525
- return md5Bytes(bytes);
715
+ const bytes = [];
716
+ for (let i = 0; i < input.length; i++) {
717
+ const code = input.codePointAt(i) ?? 0;
718
+ if (code < 128) bytes.push(code);
719
+ else if (code < 2048) bytes.push(192 | code >> 6, 128 | code & 63);
720
+ else bytes.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
721
+ }
722
+ return md5Bytes(bytes);
526
723
  }
527
- var CLIENT_ID = "MOBrBDS8blbauoSck0ZfDbtuzpyT";
528
- var CLIENT_SECRET = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj";
529
- var HASH_SECRET = "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c";
530
- var AUTH_URL = "https://oauth.secure.pixiv.net/auth/token";
724
+ const CLIENT_ID = "MOBrBDS8blbauoSck0ZfDbtuzpyT";
725
+ const CLIENT_SECRET = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj";
726
+ const HASH_SECRET = "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c";
727
+ const AUTH_URL = "https://oauth.secure.pixiv.net/auth/token";
728
+ /** Builds the x-client-hash header value for a given UTC timestamp string. */
531
729
  function buildClientHash(localTime) {
532
- return md5(localTime + HASH_SECRET);
730
+ return md5(localTime + HASH_SECRET);
533
731
  }
534
- var AuthManager = class _AuthManager {
535
- #accessToken;
536
- #refreshToken;
537
- userId;
538
- constructor(credentials) {
539
- this.#accessToken = credentials.accessToken;
540
- this.#refreshToken = credentials.refreshToken;
541
- this.userId = credentials.userId;
542
- }
543
- /** Returns the current access token. */
544
- get accessToken() {
545
- return this.#accessToken;
546
- }
547
- /** Returns the current refresh token. */
548
- get refreshToken() {
549
- return this.#refreshToken;
550
- }
551
- /**
552
- * Exchanges the stored refresh token for a fresh access token.
553
- *
554
- * Updates the internal credentials on success.
555
- * Throws if the token endpoint returns a non-200 response.
556
- */
557
- async refresh() {
558
- const localTime = (/* @__PURE__ */ new Date()).toISOString().replace(/Z$/, "+00:00");
559
- const headers = {
560
- "x-client-time": localTime,
561
- "x-client-hash": buildClientHash(localTime),
562
- "app-os": "ios",
563
- "app-os-version": "16.4.1",
564
- "user-agent": "PixivIOSApp/7.16.9 (iOS 16.4.1; iPad13,4)",
565
- "Content-Type": "application/x-www-form-urlencoded"
566
- };
567
- const body = new URLSearchParams({
568
- client_id: CLIENT_ID,
569
- client_secret: CLIENT_SECRET,
570
- get_secure_url: "1",
571
- grant_type: "refresh_token",
572
- refresh_token: this.#refreshToken
573
- }).toString();
574
- const response = await fetch(AUTH_URL, {
575
- method: "POST",
576
- headers,
577
- body
578
- });
579
- if (response.status !== 200) {
580
- throw new Error(
581
- `Failed to refresh pixiv token: HTTP ${response.status}`
582
- );
583
- }
584
- const data = await response.json();
585
- this.#accessToken = data.response.access_token;
586
- this.#refreshToken = data.response.refresh_token;
587
- this.userId = data.user.id;
588
- }
589
- /**
590
- * Creates an `AuthManager` by performing the initial token refresh.
591
- *
592
- * @param refreshToken - Pixiv refresh token
593
- * @returns Initialized `AuthManager`
594
- */
595
- static async login(refreshToken) {
596
- const manager = new _AuthManager({
597
- userId: "",
598
- accessToken: "",
599
- refreshToken
600
- });
601
- await manager.refresh();
602
- return manager;
603
- }
732
+ /**
733
+ * Manages access tokens for the pixiv API.
734
+ *
735
+ * Holds the current access token and refresh token in memory.
736
+ * The refresh() method exchanges the refresh token for a new access token
737
+ * via the pixiv OAuth endpoint.
738
+ */
739
+ var AuthManager = class AuthManager {
740
+ #accessToken;
741
+ #refreshToken;
742
+ userId;
743
+ constructor(credentials) {
744
+ this.#accessToken = credentials.accessToken;
745
+ this.#refreshToken = credentials.refreshToken;
746
+ this.userId = credentials.userId;
747
+ }
748
+ /** Returns the current access token. */
749
+ get accessToken() {
750
+ return this.#accessToken;
751
+ }
752
+ /** Returns the current refresh token. */
753
+ get refreshToken() {
754
+ return this.#refreshToken;
755
+ }
756
+ /**
757
+ * Exchanges the stored refresh token for a fresh access token.
758
+ *
759
+ * Updates the internal credentials on success.
760
+ * Throws if the token endpoint returns a non-200 response.
761
+ */
762
+ async refresh() {
763
+ const localTime = (/* @__PURE__ */ new Date()).toISOString().replace(/Z$/, "+00:00");
764
+ const headers = {
765
+ "x-client-time": localTime,
766
+ "x-client-hash": buildClientHash(localTime),
767
+ "app-os": "ios",
768
+ "app-os-version": "16.4.1",
769
+ "user-agent": "PixivIOSApp/7.16.9 (iOS 16.4.1; iPad13,4)",
770
+ "Content-Type": "application/x-www-form-urlencoded"
771
+ };
772
+ const body = new URLSearchParams({
773
+ client_id: CLIENT_ID,
774
+ client_secret: CLIENT_SECRET,
775
+ get_secure_url: "1",
776
+ grant_type: "refresh_token",
777
+ refresh_token: this.#refreshToken
778
+ }).toString();
779
+ const response = await fetch(AUTH_URL, {
780
+ method: "POST",
781
+ headers,
782
+ body
783
+ });
784
+ if (response.status !== 200) throw new Error(`Failed to refresh pixiv token: HTTP ${response.status}`);
785
+ const data = await response.json();
786
+ this.#accessToken = data.response.access_token;
787
+ this.#refreshToken = data.response.refresh_token;
788
+ this.userId = data.user.id;
789
+ }
790
+ /**
791
+ * Creates an `AuthManager` by performing the initial token refresh.
792
+ *
793
+ * @param refreshToken - Pixiv refresh token
794
+ * @returns Initialized `AuthManager`
795
+ */
796
+ static async login(refreshToken) {
797
+ const manager = new AuthManager({
798
+ userId: "",
799
+ accessToken: "",
800
+ refreshToken
801
+ });
802
+ await manager.refresh();
803
+ return manager;
804
+ }
604
805
  };
605
-
606
- // src/http.ts
607
- var DEFAULT_RETRY = { maxRetries: 3, waitMs: 1e4 };
608
- var BASE_URL = "https://app-api.pixiv.net";
609
- var DEFAULT_HEADERS = {
610
- Host: "app-api.pixiv.net",
611
- "App-OS": "ios",
612
- "App-OS-Version": "14.6",
613
- "User-Agent": "PixivIOSApp/7.13.3 (iOS 14.6; iPhone13,2)",
614
- "Accept-Language": "ja"
806
+ //#endregion
807
+ //#region src/http.ts
808
+ const DEFAULT_RETRY = {
809
+ maxRetries: 3,
810
+ waitMs: 1e4
615
811
  };
812
+ const BASE_URL = "https://app-api.pixiv.net";
813
+ const DEFAULT_HEADERS = {
814
+ Host: "app-api.pixiv.net",
815
+ "App-OS": "ios",
816
+ "App-OS-Version": "14.6",
817
+ "User-Agent": "PixivIOSApp/7.13.3 (iOS 14.6; iPhone13,2)",
818
+ "Accept-Language": "ja"
819
+ };
820
+ /**
821
+ * Parses the `Retry-After` header value into milliseconds.
822
+ *
823
+ * Supports delay-seconds format and HTTP-date format.
824
+ * Falls back to `defaultMs` if the header is absent or unparseable.
825
+ *
826
+ * @param retryAfter - Header value (null if not present)
827
+ * @param defaultMs - Fallback wait time in milliseconds
828
+ * @returns Wait time in milliseconds (clamped to ≥ 0)
829
+ */
616
830
  function parseRetryAfter(retryAfter, defaultMs) {
617
- if (!retryAfter) return defaultMs;
618
- if (/^\d+$/.test(retryAfter.trim())) {
619
- return Number.parseInt(retryAfter, 10) * 1e3;
620
- }
621
- const retryDate = Date.parse(retryAfter);
622
- if (!Number.isNaN(retryDate)) {
623
- return Math.max(0, retryDate - Date.now());
624
- }
625
- return defaultMs;
831
+ if (!retryAfter) return defaultMs;
832
+ if (/^\d+$/.test(retryAfter.trim())) return Number.parseInt(retryAfter, 10) * 1e3;
833
+ const retryDate = Date.parse(retryAfter);
834
+ if (!Number.isNaN(retryDate)) return Math.max(0, retryDate - Date.now());
835
+ return defaultMs;
626
836
  }
627
837
  function headersToRecord(headers) {
628
- const result = {};
629
- for (const [key, value] of headers) {
630
- result[key] = value;
631
- }
632
- return result;
838
+ const result = {};
839
+ for (const [key, value] of headers) result[key] = value;
840
+ return result;
633
841
  }
842
+ /**
843
+ * HTTP client for the pixiv API.
844
+ *
845
+ * All methods return `ResultAsync<T, PixivError>` — no throws.
846
+ * A 429 → retry loop and a 401 → refresh → retry are handled internally.
847
+ */
634
848
  var HttpClient = class {
635
- #auth;
636
- #retry;
637
- #interceptor;
638
- constructor(auth, options) {
639
- this.#auth = auth;
640
- this.#retry = {
641
- maxRetries: options?.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,
642
- waitMs: options?.retry?.waitMs ?? DEFAULT_RETRY.waitMs
643
- };
644
- this.#interceptor = options?.onResponse;
645
- }
646
- /**
647
- * Sends a GET request to the pixiv API.
648
- *
649
- * @param path - API endpoint path (e.g. "/v1/illust/detail")
650
- * @param params - Query parameters as a URLSearchParams instance
651
- * @returns `ResultAsync<T, PixivError>`
652
- */
653
- get(path, params) {
654
- const qs = params ? `?${params.toString()}` : "";
655
- const url = `${BASE_URL}${path}${qs}`;
656
- return this.#send(url, "GET", path, void 0);
657
- }
658
- /**
659
- * Sends a POST request to the pixiv API.
660
- *
661
- * @param path - API endpoint path (e.g. "/v2/illust/bookmark/add")
662
- * @param body - URL-encoded request body string
663
- * @returns `ResultAsync<T, PixivError>`
664
- */
665
- post(path, body) {
666
- const url = `${BASE_URL}${path}`;
667
- return this.#send(url, "POST", path, body);
668
- }
669
- /**
670
- * Fetches a pixiv image URL without an Authorization header.
671
- *
672
- * Uses a browser User-Agent and the pixiv Referer, which are required for
673
- * image CDN access. Retry and interceptor are not applied here.
674
- *
675
- * @param imageUrl - Full image URL
676
- * @returns `ResultAsync<Response, PixivError>`
677
- */
678
- fetchImage(imageUrl) {
679
- return ResultAsync.fromPromise(
680
- fetch(imageUrl, {
681
- headers: {
682
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
683
- Referer: "https://www.pixiv.net/"
684
- }
685
- }),
686
- networkError
687
- ).andThen((response) => {
688
- if (!response.ok) {
689
- return ResultAsync.fromResult(
690
- err(apiError(response.status, null))
691
- );
692
- }
693
- return ResultAsync.fromResult(ok(response));
694
- });
695
- }
696
- /**
697
- * Sends a request to an absolute URL returned in a `next_url` field.
698
- *
699
- * Applies the same retry / interceptor / auth logic as `get()`.
700
- *
701
- * @param absoluteUrl - Full URL including query string
702
- * @returns `ResultAsync<T, PixivError>`
703
- */
704
- getAbsolute(absoluteUrl) {
705
- let endpoint;
706
- try {
707
- endpoint = new URL(absoluteUrl).pathname;
708
- } catch {
709
- endpoint = absoluteUrl;
710
- }
711
- return this.#send(absoluteUrl, "GET", endpoint, void 0);
712
- }
713
- // ---------------------------------------------------------------------------
714
- // Private helpers
715
- // ---------------------------------------------------------------------------
716
- #send(url, method, endpoint, body) {
717
- return new ResultAsync(this.#sendWithRetry(url, method, endpoint, body));
718
- }
719
- async #sendWithRetry(url, method, endpoint, body, allowRefresh = true) {
720
- const maxRetries = Math.max(0, this.#retry.maxRetries);
721
- const waitMs = Math.max(0, this.#retry.waitMs);
722
- let lastRetryAfterMs = 0;
723
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
724
- const requestHeaders = {
725
- ...DEFAULT_HEADERS,
726
- Authorization: `Bearer ${this.#auth.accessToken}`,
727
- ...method === "POST" ? { "Content-Type": "application/x-www-form-urlencoded" } : {}
728
- };
729
- let response;
730
- try {
731
- response = await fetch(url, {
732
- method,
733
- headers: requestHeaders,
734
- body: method === "POST" ? body : void 0
735
- });
736
- } catch (fetchError) {
737
- return err(networkError(fetchError));
738
- }
739
- if (response.status === 429) {
740
- await response.body?.cancel();
741
- const retryAfterMs = parseRetryAfter(
742
- response.headers.get("Retry-After"),
743
- waitMs
744
- );
745
- lastRetryAfterMs = retryAfterMs;
746
- if (attempt < maxRetries) {
747
- await new Promise((resolve) => setTimeout(resolve, retryAfterMs));
748
- continue;
749
- }
750
- return err(rateLimitError(lastRetryAfterMs));
751
- }
752
- if (response.status === 401) {
753
- await response.body?.cancel();
754
- if (allowRefresh) {
755
- try {
756
- await this.#auth.refresh();
757
- } catch {
758
- return err(authFailedError(401));
759
- }
760
- return this.#sendWithRetry(url, method, endpoint, body, false);
761
- }
762
- return err(authFailedError(401));
763
- }
764
- const contentType = response.headers.get("content-type") ?? "";
765
- const text = await response.text();
766
- let data;
767
- const isJson = contentType.includes("application/json");
768
- if (isJson) {
769
- try {
770
- data = camelizeKeys(JSON.parse(text));
771
- } catch {
772
- data = text;
773
- }
774
- } else {
775
- data = text;
776
- }
777
- const responseHeaders = headersToRecord(response.headers);
778
- if (!response.ok) {
779
- return err(apiError(response.status, data));
780
- }
781
- const httpResponse = {
782
- data,
783
- status: response.status,
784
- responseUrl: response.url || void 0};
785
- if (this.#interceptor) {
786
- const record = {
787
- method,
788
- endpoint,
789
- url: response.url || url,
790
- requestHeaders: JSON.stringify(requestHeaders),
791
- requestBody: body ?? null,
792
- responseType: isJson ? "JSON" : "TEXT",
793
- statusCode: response.status,
794
- responseHeaders: JSON.stringify(responseHeaders),
795
- responseBody: text
796
- };
797
- Promise.resolve(this.#interceptor(record)).catch(() => void 0);
798
- }
799
- return ok(httpResponse.data);
800
- }
801
- return err(rateLimitError(lastRetryAfterMs));
802
- }
849
+ #auth;
850
+ #retry;
851
+ #interceptor;
852
+ constructor(auth, options) {
853
+ this.#auth = auth;
854
+ this.#retry = {
855
+ maxRetries: options?.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,
856
+ waitMs: options?.retry?.waitMs ?? DEFAULT_RETRY.waitMs
857
+ };
858
+ this.#interceptor = options?.onResponse;
859
+ }
860
+ /**
861
+ * Sends a GET request to the pixiv API.
862
+ *
863
+ * @param path - API endpoint path (e.g. "/v1/illust/detail")
864
+ * @param params - Query parameters as a URLSearchParams instance
865
+ * @returns `ResultAsync<T, PixivError>`
866
+ */
867
+ get(path, params) {
868
+ const url = `${BASE_URL}${path}${params ? `?${params.toString()}` : ""}`;
869
+ return this.#send(url, "GET", path, void 0);
870
+ }
871
+ /**
872
+ * Sends a POST request to the pixiv API.
873
+ *
874
+ * @param path - API endpoint path (e.g. "/v2/illust/bookmark/add")
875
+ * @param body - URL-encoded request body string
876
+ * @returns `ResultAsync<T, PixivError>`
877
+ */
878
+ post(path, body) {
879
+ const url = `${BASE_URL}${path}`;
880
+ return this.#send(url, "POST", path, body);
881
+ }
882
+ /**
883
+ * Fetches a pixiv image URL without an Authorization header.
884
+ *
885
+ * Uses a browser User-Agent and the pixiv Referer, which are required for
886
+ * image CDN access. Retry and interceptor are not applied here.
887
+ *
888
+ * @param imageUrl - Full image URL
889
+ * @returns `ResultAsync<Response, PixivError>`
890
+ */
891
+ fetchImage(imageUrl) {
892
+ return ResultAsync.fromPromise(fetch(imageUrl, { headers: {
893
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
894
+ Referer: "https://www.pixiv.net/"
895
+ } }), networkError).andThen((response) => {
896
+ if (!response.ok) return ResultAsync.fromResult(err(apiError(response.status, null)));
897
+ return ResultAsync.fromResult(ok(response));
898
+ });
899
+ }
900
+ /**
901
+ * Sends a request to an absolute URL returned in a `next_url` field.
902
+ *
903
+ * Applies the same retry / interceptor / auth logic as `get()`.
904
+ *
905
+ * @param absoluteUrl - Full URL including query string
906
+ * @returns `ResultAsync<T, PixivError>`
907
+ */
908
+ getAbsolute(absoluteUrl) {
909
+ let endpoint;
910
+ try {
911
+ endpoint = new URL(absoluteUrl).pathname;
912
+ } catch {
913
+ endpoint = absoluteUrl;
914
+ }
915
+ return this.#send(absoluteUrl, "GET", endpoint, void 0);
916
+ }
917
+ #send(url, method, endpoint, body) {
918
+ return new ResultAsync(this.#sendWithRetry(url, method, endpoint, body));
919
+ }
920
+ async #sendWithRetry(url, method, endpoint, body, allowRefresh = true) {
921
+ const maxRetries = Math.max(0, this.#retry.maxRetries);
922
+ const waitMs = Math.max(0, this.#retry.waitMs);
923
+ let lastRetryAfterMs = 0;
924
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
925
+ const requestHeaders = {
926
+ ...DEFAULT_HEADERS,
927
+ Authorization: `Bearer ${this.#auth.accessToken}`,
928
+ ...method === "POST" ? { "Content-Type": "application/x-www-form-urlencoded" } : {}
929
+ };
930
+ let response;
931
+ try {
932
+ response = await fetch(url, {
933
+ method,
934
+ headers: requestHeaders,
935
+ body: method === "POST" ? body : void 0
936
+ });
937
+ } catch (fetchError) {
938
+ return err(networkError(fetchError));
939
+ }
940
+ if (response.status === 429) {
941
+ await response.body?.cancel();
942
+ const retryAfterMs = parseRetryAfter(response.headers.get("Retry-After"), waitMs);
943
+ lastRetryAfterMs = retryAfterMs;
944
+ if (attempt < maxRetries) {
945
+ await new Promise((resolve) => setTimeout(resolve, retryAfterMs));
946
+ continue;
947
+ }
948
+ return err(rateLimitError(lastRetryAfterMs));
949
+ }
950
+ if (response.status === 401) {
951
+ await response.body?.cancel();
952
+ if (allowRefresh) {
953
+ try {
954
+ await this.#auth.refresh();
955
+ } catch {
956
+ return err(authFailedError(401));
957
+ }
958
+ return this.#sendWithRetry(url, method, endpoint, body, false);
959
+ }
960
+ return err(authFailedError(401));
961
+ }
962
+ const contentType = response.headers.get("content-type") ?? "";
963
+ const text = await response.text();
964
+ let data;
965
+ const isJson = contentType.includes("application/json");
966
+ if (isJson) try {
967
+ data = camelizeKeys(JSON.parse(text));
968
+ } catch {
969
+ data = text;
970
+ }
971
+ else data = text;
972
+ const responseHeaders = headersToRecord(response.headers);
973
+ if (!response.ok) return err(apiError(response.status, data));
974
+ const httpResponse = {
975
+ data,
976
+ status: response.status,
977
+ headers: responseHeaders,
978
+ requestHeaders,
979
+ requestBody: body ?? null,
980
+ responseUrl: response.url || void 0,
981
+ endpoint
982
+ };
983
+ if (this.#interceptor) {
984
+ const record = {
985
+ method,
986
+ endpoint,
987
+ url: response.url || url,
988
+ requestHeaders: JSON.stringify(requestHeaders),
989
+ requestBody: body ?? null,
990
+ responseType: isJson ? "JSON" : "TEXT",
991
+ statusCode: response.status,
992
+ responseHeaders: JSON.stringify(responseHeaders),
993
+ responseBody: text
994
+ };
995
+ Promise.resolve(this.#interceptor(record)).catch(() => void 0);
996
+ }
997
+ return ok(httpResponse.data);
998
+ }
999
+ return err(rateLimitError(lastRetryAfterMs));
1000
+ }
803
1001
  };
804
-
805
- // src/resources/illusts.ts
1002
+ //#endregion
1003
+ //#region src/resources/illusts.ts
1004
+ /** Methods for the illust API namespace. */
806
1005
  var IllustResource = class {
807
- #http;
808
- constructor(http) {
809
- this.#http = http;
810
- }
811
- /**
812
- * Fetches a single illust by ID.
813
- * GET /v1/illust/detail
814
- *
815
- * @param params - Request parameters
816
- *
817
- * @example
818
- * ```ts
819
- * const result = await client.illusts.detail({ illustId: 12345 })
820
- * if (result.isOk) {
821
- * console.log(result.value.illust.title)
822
- * } else {
823
- * console.error(result.error)
824
- * }
825
- * ```
826
- */
827
- detail(params) {
828
- return this.#http.get(
829
- "/v1/illust/detail",
830
- buildParams({ illustId: params.illustId, filter: params.filter ?? "for_ios" })
831
- );
832
- }
833
- /**
834
- * Fetches related illusts for a given illust.
835
- * GET /v2/illust/related
836
- *
837
- * @param params - Request parameters
838
- */
839
- related(params) {
840
- return PaginatedResultAsync.fromResultAsync(
841
- this.#http.get(
842
- "/v2/illust/related",
843
- buildParams({
844
- illustId: params.illustId,
845
- filter: params.filter ?? "for_ios",
846
- ...params.seedIllustIds ? { seedIllustIds: params.seedIllustIds } : {}
847
- })
848
- ),
849
- this.#http,
850
- (page) => page.illusts
851
- );
852
- }
853
- /**
854
- * Searches for illusts.
855
- * GET /v1/search/illust
856
- *
857
- * @param params - Request parameters
858
- *
859
- * @example
860
- * ```ts
861
- * // Iterate all results across pages
862
- * for await (const illust of client.illusts.search({ word: 'cat' }).items()) {
863
- * console.log(illust.title)
864
- * }
865
- *
866
- * // Fetch only the first page
867
- * const page = await client.illusts.search({ word: 'cat' })
868
- * if (page.isOk) {
869
- * console.log(page.value.illusts.length)
870
- * }
871
- * ```
872
- */
873
- search(params) {
874
- return PaginatedResultAsync.fromResultAsync(
875
- this.#http.get(
876
- "/v1/search/illust",
877
- buildParams({
878
- word: params.word,
879
- searchTarget: params.searchTarget ?? "partial_match_for_tags",
880
- sort: params.sort ?? "date_desc",
881
- filter: params.filter ?? "for_ios",
882
- duration: params.duration,
883
- startDate: params.startDate,
884
- endDate: params.endDate,
885
- searchAiType: params.searchAiType,
886
- offset: params.offset
887
- })
888
- ),
889
- this.#http,
890
- (page) => page.illusts
891
- );
892
- }
893
- /**
894
- * Fetches the illust ranking.
895
- * GET /v1/illust/ranking
896
- *
897
- * @param params - Request parameters
898
- */
899
- ranking(params = {}) {
900
- return PaginatedResultAsync.fromResultAsync(
901
- this.#http.get(
902
- "/v1/illust/ranking",
903
- buildParams({
904
- mode: params.mode ?? "day",
905
- filter: params.filter ?? "for_ios",
906
- date: params.date,
907
- offset: params.offset
908
- })
909
- ),
910
- this.#http,
911
- (page) => page.illusts
912
- );
913
- }
914
- /**
915
- * Fetches recommended illusts.
916
- * GET /v1/illust/recommended
917
- *
918
- * @param params - Request parameters
919
- */
920
- recommended(params = {}) {
921
- return PaginatedResultAsync.fromResultAsync(
922
- this.#http.get(
923
- "/v1/illust/recommended",
924
- buildParams({
925
- filter: params.filter ?? "for_ios",
926
- includeRankingLabel: true,
927
- includeRankingIllusts: true,
928
- includePrivacyPolicy: true,
929
- offset: params.offset,
930
- maxBookmarkIdForRecommend: params.maxBookmarkIdForRecommend,
931
- minBookmarkIdForRecentIllust: params.minBookmarkIdForRecentIllust
932
- })
933
- ),
934
- this.#http,
935
- (page) => page.illusts
936
- );
937
- }
938
- /**
939
- * Fetches an illust series.
940
- * GET /v1/illust/series
941
- *
942
- * @param params - Request parameters
943
- */
944
- series(params) {
945
- return PaginatedResultAsync.fromResultAsync(
946
- this.#http.get(
947
- "/v1/illust/series",
948
- buildParams({
949
- illustSeriesId: params.illustSeriesId,
950
- filter: params.filter ?? "for_ios"
951
- })
952
- ),
953
- this.#http,
954
- (page) => page.illusts
955
- );
956
- }
957
- /**
958
- * Adds an illust bookmark.
959
- * POST /v2/illust/bookmark/add
960
- *
961
- * @param params - Request parameters
962
- */
963
- bookmarkAdd(params) {
964
- const body = buildParams({
965
- illustId: params.illustId,
966
- restrict: params.restrict ?? "public",
967
- ...params.tags ? { tags: params.tags } : {}
968
- });
969
- return this.#http.post(
970
- "/v2/illust/bookmark/add",
971
- body.toString()
972
- );
973
- }
974
- /**
975
- * Removes an illust bookmark.
976
- * POST /v1/illust/bookmark/delete
977
- *
978
- * @param params - Request parameters
979
- */
980
- bookmarkDelete(params) {
981
- const body = buildParams({ illustId: String(params.illustId) });
982
- return this.#http.post(
983
- "/v1/illust/bookmark/delete",
984
- body.toString()
985
- );
986
- }
1006
+ #http;
1007
+ constructor(http) {
1008
+ this.#http = http;
1009
+ }
1010
+ /**
1011
+ * Fetches a single illust by ID.
1012
+ * GET /v1/illust/detail
1013
+ *
1014
+ * @param params - Request parameters
1015
+ *
1016
+ * @example
1017
+ * ```ts
1018
+ * const result = await client.illusts.detail({ illustId: 12345 })
1019
+ * if (result.isOk) {
1020
+ * console.log(result.value.illust.title)
1021
+ * } else {
1022
+ * console.error(result.error)
1023
+ * }
1024
+ * ```
1025
+ */
1026
+ detail(params) {
1027
+ return this.#http.get("/v1/illust/detail", buildParams({
1028
+ illustId: params.illustId,
1029
+ filter: params.filter ?? "for_ios"
1030
+ }));
1031
+ }
1032
+ /**
1033
+ * Fetches related illusts for a given illust.
1034
+ * GET /v2/illust/related
1035
+ *
1036
+ * @param params - Request parameters
1037
+ */
1038
+ related(params) {
1039
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v2/illust/related", buildParams({
1040
+ illustId: params.illustId,
1041
+ filter: params.filter ?? "for_ios",
1042
+ ...params.seedIllustIds ? { seedIllustIds: params.seedIllustIds } : {}
1043
+ })), this.#http, (page) => page.illusts);
1044
+ }
1045
+ /**
1046
+ * Searches for illusts.
1047
+ * GET /v1/search/illust
1048
+ *
1049
+ * @param params - Request parameters
1050
+ *
1051
+ * @example
1052
+ * ```ts
1053
+ * // Iterate all results across pages
1054
+ * for await (const illust of client.illusts.search({ word: 'cat' }).items()) {
1055
+ * console.log(illust.title)
1056
+ * }
1057
+ *
1058
+ * // Fetch only the first page
1059
+ * const page = await client.illusts.search({ word: 'cat' })
1060
+ * if (page.isOk) {
1061
+ * console.log(page.value.illusts.length)
1062
+ * }
1063
+ * ```
1064
+ */
1065
+ search(params) {
1066
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/search/illust", buildParams({
1067
+ word: params.word,
1068
+ searchTarget: params.searchTarget ?? "partial_match_for_tags",
1069
+ sort: params.sort ?? "date_desc",
1070
+ filter: params.filter ?? "for_ios",
1071
+ duration: params.duration,
1072
+ startDate: params.startDate,
1073
+ endDate: params.endDate,
1074
+ searchAiType: params.searchAiType,
1075
+ offset: params.offset
1076
+ })), this.#http, (page) => page.illusts);
1077
+ }
1078
+ /**
1079
+ * Fetches the illust ranking.
1080
+ * GET /v1/illust/ranking
1081
+ *
1082
+ * @param params - Request parameters
1083
+ */
1084
+ ranking(params = {}) {
1085
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/illust/ranking", buildParams({
1086
+ mode: params.mode ?? "day",
1087
+ filter: params.filter ?? "for_ios",
1088
+ date: params.date,
1089
+ offset: params.offset
1090
+ })), this.#http, (page) => page.illusts);
1091
+ }
1092
+ /**
1093
+ * Fetches recommended illusts.
1094
+ * GET /v1/illust/recommended
1095
+ *
1096
+ * @param params - Request parameters
1097
+ */
1098
+ recommended(params = {}) {
1099
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/illust/recommended", buildParams({
1100
+ filter: params.filter ?? "for_ios",
1101
+ includeRankingLabel: true,
1102
+ includeRankingIllusts: true,
1103
+ includePrivacyPolicy: true,
1104
+ offset: params.offset,
1105
+ maxBookmarkIdForRecommend: params.maxBookmarkIdForRecommend,
1106
+ minBookmarkIdForRecentIllust: params.minBookmarkIdForRecentIllust
1107
+ })), this.#http, (page) => page.illusts);
1108
+ }
1109
+ /**
1110
+ * Fetches an illust series.
1111
+ * GET /v1/illust/series
1112
+ *
1113
+ * @param params - Request parameters
1114
+ */
1115
+ series(params) {
1116
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/illust/series", buildParams({
1117
+ illustSeriesId: params.illustSeriesId,
1118
+ filter: params.filter ?? "for_ios"
1119
+ })), this.#http, (page) => page.illusts);
1120
+ }
1121
+ /**
1122
+ * Adds an illust bookmark.
1123
+ * POST /v2/illust/bookmark/add
1124
+ *
1125
+ * @param params - Request parameters
1126
+ */
1127
+ bookmarkAdd(params) {
1128
+ const body = buildParams({
1129
+ illustId: params.illustId,
1130
+ restrict: params.restrict ?? "public",
1131
+ ...params.tags ? { tags: params.tags } : {}
1132
+ });
1133
+ return this.#http.post("/v2/illust/bookmark/add", body.toString());
1134
+ }
1135
+ /**
1136
+ * Removes an illust bookmark.
1137
+ * POST /v1/illust/bookmark/delete
1138
+ *
1139
+ * @param params - Request parameters
1140
+ */
1141
+ bookmarkDelete(params) {
1142
+ const body = buildParams({ illustId: String(params.illustId) });
1143
+ return this.#http.post("/v1/illust/bookmark/delete", body.toString());
1144
+ }
987
1145
  };
988
-
989
- // src/resources/novels.ts
1146
+ //#endregion
1147
+ //#region src/resources/novels.ts
1148
+ /** Methods for the novel API namespace. */
990
1149
  var NovelResource = class {
991
- #http;
992
- constructor(http) {
993
- this.#http = http;
994
- }
995
- /**
996
- * Fetches a single novel by ID.
997
- * GET /v2/novel/detail
998
- *
999
- * @param params - Request parameters
1000
- *
1001
- * @example
1002
- * ```ts
1003
- * const result = await client.novels.detail({ novelId: 67890 })
1004
- * if (result.isOk) {
1005
- * console.log(result.value.novel.title)
1006
- * } else {
1007
- * console.error(result.error)
1008
- * }
1009
- * ```
1010
- */
1011
- detail(params) {
1012
- return this.#http.get(
1013
- "/v2/novel/detail",
1014
- buildParams({ novelId: params.novelId })
1015
- );
1016
- }
1017
- /**
1018
- * Fetches the WebView HTML for a novel.
1019
- * GET /webview/v2/novel
1020
- *
1021
- * Returns the raw HTML page that the pixiv app renders in a WebView.
1022
- * To extract the plain text, parse the returned HTML (e.g. strip tags).
1023
- *
1024
- * @param params - Request parameters
1025
- */
1026
- text(params) {
1027
- return this.#http.get(
1028
- "/webview/v2/novel",
1029
- // The webview endpoint uses the query parameter 'id', not 'novel_id'
1030
- buildParams({ id: params.novelId })
1031
- );
1032
- }
1033
- /**
1034
- * Fetches related novels for a given novel.
1035
- * GET /v1/novel/related
1036
- *
1037
- * @param params - Request parameters
1038
- */
1039
- related(params) {
1040
- return PaginatedResultAsync.fromResultAsync(
1041
- this.#http.get(
1042
- "/v1/novel/related",
1043
- buildParams({ novelId: params.novelId })
1044
- ),
1045
- this.#http,
1046
- (page) => page.novels
1047
- );
1048
- }
1049
- /**
1050
- * Searches for novels.
1051
- * GET /v1/search/novel
1052
- *
1053
- * @param params - Request parameters
1054
- *
1055
- * @example
1056
- * ```ts
1057
- * // Iterate all results across pages
1058
- * for await (const novel of client.novels.search({ word: 'fantasy' }).items()) {
1059
- * console.log(novel.title)
1060
- * }
1061
- *
1062
- * // Fetch only the first page
1063
- * const page = await client.novels.search({ word: 'fantasy' })
1064
- * if (page.isOk) {
1065
- * console.log(page.value.novels.length)
1066
- * }
1067
- * ```
1068
- */
1069
- search(params) {
1070
- return PaginatedResultAsync.fromResultAsync(
1071
- this.#http.get(
1072
- "/v1/search/novel",
1073
- buildParams({
1074
- word: params.word,
1075
- searchTarget: params.searchTarget ?? "partial_match_for_tags",
1076
- sort: params.sort ?? "date_desc",
1077
- filter: params.filter ?? "for_ios",
1078
- duration: params.duration,
1079
- startDate: params.startDate,
1080
- endDate: params.endDate,
1081
- searchAiType: params.searchAiType,
1082
- offset: params.offset
1083
- })
1084
- ),
1085
- this.#http,
1086
- (page) => page.novels
1087
- );
1088
- }
1089
- /**
1090
- * Fetches the novel ranking.
1091
- * GET /v1/novel/ranking
1092
- *
1093
- * @param params - Request parameters
1094
- */
1095
- ranking(params = {}) {
1096
- return PaginatedResultAsync.fromResultAsync(
1097
- this.#http.get(
1098
- "/v1/novel/ranking",
1099
- buildParams({
1100
- mode: params.mode ?? "day",
1101
- filter: params.filter ?? "for_ios",
1102
- date: params.date,
1103
- offset: params.offset
1104
- })
1105
- ),
1106
- this.#http,
1107
- (page) => page.novels
1108
- );
1109
- }
1110
- /**
1111
- * Fetches recommended novels.
1112
- * GET /v1/novel/recommended
1113
- *
1114
- * @param params - Request parameters
1115
- */
1116
- recommended(params = {}) {
1117
- return PaginatedResultAsync.fromResultAsync(
1118
- this.#http.get(
1119
- "/v1/novel/recommended",
1120
- buildParams({
1121
- filter: params.filter ?? "for_ios",
1122
- includeRankingNovels: true,
1123
- includePrivacyPolicy: true,
1124
- offset: params.offset,
1125
- maxBookmarkIdForRecommend: params.maxBookmarkIdForRecommend
1126
- })
1127
- ),
1128
- this.#http,
1129
- (page) => page.novels
1130
- );
1131
- }
1132
- /**
1133
- * Fetches a novel series.
1134
- * GET /v2/novel/series
1135
- *
1136
- * @param params - Request parameters
1137
- */
1138
- series(params) {
1139
- return PaginatedResultAsync.fromResultAsync(
1140
- this.#http.get(
1141
- "/v2/novel/series",
1142
- buildParams({ seriesId: params.seriesId, lastOrder: params.lastOrder })
1143
- ),
1144
- this.#http,
1145
- (page) => page.novels
1146
- );
1147
- }
1148
- /**
1149
- * Adds a novel bookmark.
1150
- * POST /v2/novel/bookmark/add
1151
- *
1152
- * @param params - Request parameters
1153
- */
1154
- bookmarkAdd(params) {
1155
- const body = buildParams({
1156
- novelId: params.novelId,
1157
- restrict: params.restrict ?? "public",
1158
- ...params.tags ? { tags: params.tags } : {}
1159
- });
1160
- return this.#http.post(
1161
- "/v2/novel/bookmark/add",
1162
- body.toString()
1163
- );
1164
- }
1165
- /**
1166
- * Removes a novel bookmark.
1167
- * POST /v1/novel/bookmark/delete
1168
- *
1169
- * @param params - Request parameters
1170
- */
1171
- bookmarkDelete(params) {
1172
- const body = buildParams({ novelId: String(params.novelId) });
1173
- return this.#http.post(
1174
- "/v1/novel/bookmark/delete",
1175
- body.toString()
1176
- );
1177
- }
1150
+ #http;
1151
+ constructor(http) {
1152
+ this.#http = http;
1153
+ }
1154
+ /**
1155
+ * Fetches a single novel by ID.
1156
+ * GET /v2/novel/detail
1157
+ *
1158
+ * @param params - Request parameters
1159
+ *
1160
+ * @example
1161
+ * ```ts
1162
+ * const result = await client.novels.detail({ novelId: 67890 })
1163
+ * if (result.isOk) {
1164
+ * console.log(result.value.novel.title)
1165
+ * } else {
1166
+ * console.error(result.error)
1167
+ * }
1168
+ * ```
1169
+ */
1170
+ detail(params) {
1171
+ return this.#http.get("/v2/novel/detail", buildParams({ novelId: params.novelId }));
1172
+ }
1173
+ /**
1174
+ * Fetches the WebView HTML for a novel.
1175
+ * GET /webview/v2/novel
1176
+ *
1177
+ * Returns the raw HTML page that the pixiv app renders in a WebView.
1178
+ * To extract the plain text, parse the returned HTML (e.g. strip tags).
1179
+ *
1180
+ * @param params - Request parameters
1181
+ */
1182
+ text(params) {
1183
+ return this.#http.get("/webview/v2/novel", buildParams({ id: params.novelId }));
1184
+ }
1185
+ /**
1186
+ * Fetches related novels for a given novel.
1187
+ * GET /v1/novel/related
1188
+ *
1189
+ * @param params - Request parameters
1190
+ */
1191
+ related(params) {
1192
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/novel/related", buildParams({ novelId: params.novelId })), this.#http, (page) => page.novels);
1193
+ }
1194
+ /**
1195
+ * Searches for novels.
1196
+ * GET /v1/search/novel
1197
+ *
1198
+ * @param params - Request parameters
1199
+ *
1200
+ * @example
1201
+ * ```ts
1202
+ * // Iterate all results across pages
1203
+ * for await (const novel of client.novels.search({ word: 'fantasy' }).items()) {
1204
+ * console.log(novel.title)
1205
+ * }
1206
+ *
1207
+ * // Fetch only the first page
1208
+ * const page = await client.novels.search({ word: 'fantasy' })
1209
+ * if (page.isOk) {
1210
+ * console.log(page.value.novels.length)
1211
+ * }
1212
+ * ```
1213
+ */
1214
+ search(params) {
1215
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/search/novel", buildParams({
1216
+ word: params.word,
1217
+ searchTarget: params.searchTarget ?? "partial_match_for_tags",
1218
+ sort: params.sort ?? "date_desc",
1219
+ filter: params.filter ?? "for_ios",
1220
+ duration: params.duration,
1221
+ startDate: params.startDate,
1222
+ endDate: params.endDate,
1223
+ searchAiType: params.searchAiType,
1224
+ offset: params.offset
1225
+ })), this.#http, (page) => page.novels);
1226
+ }
1227
+ /**
1228
+ * Fetches the novel ranking.
1229
+ * GET /v1/novel/ranking
1230
+ *
1231
+ * @param params - Request parameters
1232
+ */
1233
+ ranking(params = {}) {
1234
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/novel/ranking", buildParams({
1235
+ mode: params.mode ?? "day",
1236
+ filter: params.filter ?? "for_ios",
1237
+ date: params.date,
1238
+ offset: params.offset
1239
+ })), this.#http, (page) => page.novels);
1240
+ }
1241
+ /**
1242
+ * Fetches recommended novels.
1243
+ * GET /v1/novel/recommended
1244
+ *
1245
+ * @param params - Request parameters
1246
+ */
1247
+ recommended(params = {}) {
1248
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/novel/recommended", buildParams({
1249
+ filter: params.filter ?? "for_ios",
1250
+ includeRankingNovels: true,
1251
+ includePrivacyPolicy: true,
1252
+ offset: params.offset,
1253
+ maxBookmarkIdForRecommend: params.maxBookmarkIdForRecommend
1254
+ })), this.#http, (page) => page.novels);
1255
+ }
1256
+ /**
1257
+ * Fetches a novel series.
1258
+ * GET /v2/novel/series
1259
+ *
1260
+ * @param params - Request parameters
1261
+ */
1262
+ series(params) {
1263
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v2/novel/series", buildParams({
1264
+ seriesId: params.seriesId,
1265
+ lastOrder: params.lastOrder
1266
+ })), this.#http, (page) => page.novels);
1267
+ }
1268
+ /**
1269
+ * Adds a novel bookmark.
1270
+ * POST /v2/novel/bookmark/add
1271
+ *
1272
+ * @param params - Request parameters
1273
+ */
1274
+ bookmarkAdd(params) {
1275
+ const body = buildParams({
1276
+ novelId: params.novelId,
1277
+ restrict: params.restrict ?? "public",
1278
+ ...params.tags ? { tags: params.tags } : {}
1279
+ });
1280
+ return this.#http.post("/v2/novel/bookmark/add", body.toString());
1281
+ }
1282
+ /**
1283
+ * Removes a novel bookmark.
1284
+ * POST /v1/novel/bookmark/delete
1285
+ *
1286
+ * @param params - Request parameters
1287
+ */
1288
+ bookmarkDelete(params) {
1289
+ const body = buildParams({ novelId: String(params.novelId) });
1290
+ return this.#http.post("/v1/novel/bookmark/delete", body.toString());
1291
+ }
1178
1292
  };
1179
-
1180
- // src/resources/users.ts
1293
+ //#endregion
1294
+ //#region src/resources/users.ts
1295
+ /** Methods for the user bookmarks sub-namespace. */
1181
1296
  var UserBookmarksResource = class {
1182
- #http;
1183
- constructor(http) {
1184
- this.#http = http;
1185
- }
1186
- /**
1187
- * Fetches a user's bookmarked illusts.
1188
- * GET /v1/user/bookmarks/illust
1189
- *
1190
- * @param params - Request parameters
1191
- *
1192
- * @example
1193
- * ```ts
1194
- * // Iterate all bookmarked illusts across pages
1195
- * for await (const illust of client.users.bookmarks.illusts({ userId: client.userId }).items()) {
1196
- * console.log(illust.title)
1197
- * }
1198
- *
1199
- * // Resume from a saved cursor
1200
- * import { parseNextUrl } from '@book000/pixivts'
1201
- * const page = await client.users.bookmarks.illusts({ userId: client.userId })
1202
- * if (page.isOk && page.value.nextUrl) {
1203
- * const cursor = parseNextUrl(page.value.nextUrl)
1204
- * const next = await client.users.bookmarks.illusts({
1205
- * userId: client.userId,
1206
- * maxBookmarkId: cursor.maxBookmarkId,
1207
- * })
1208
- * }
1209
- * ```
1210
- */
1211
- illusts(params) {
1212
- return PaginatedResultAsync.fromResultAsync(
1213
- this.#http.get(
1214
- "/v1/user/bookmarks/illust",
1215
- buildParams({
1216
- userId: params.userId,
1217
- restrict: params.restrict ?? "public",
1218
- filter: params.filter ?? "for_ios",
1219
- tag: params.tag,
1220
- maxBookmarkId: params.maxBookmarkId,
1221
- offset: params.offset
1222
- })
1223
- ),
1224
- this.#http,
1225
- (page) => page.illusts
1226
- );
1227
- }
1228
- /**
1229
- * Fetches a user's bookmarked novels.
1230
- * GET /v1/user/bookmarks/novel
1231
- *
1232
- * @param params - Request parameters
1233
- *
1234
- * @example
1235
- * ```ts
1236
- * // Iterate all bookmarked novels across pages
1237
- * for await (const novel of client.users.bookmarks.novels({ userId: client.userId }).items()) {
1238
- * console.log(novel.title)
1239
- * }
1240
- * ```
1241
- */
1242
- novels(params) {
1243
- return PaginatedResultAsync.fromResultAsync(
1244
- this.#http.get(
1245
- "/v1/user/bookmarks/novel",
1246
- buildParams({
1247
- userId: params.userId,
1248
- restrict: params.restrict ?? "public",
1249
- filter: params.filter ?? "for_ios",
1250
- tag: params.tag,
1251
- maxBookmarkId: params.maxBookmarkId,
1252
- offset: params.offset
1253
- })
1254
- ),
1255
- this.#http,
1256
- (page) => page.novels
1257
- );
1258
- }
1297
+ #http;
1298
+ constructor(http) {
1299
+ this.#http = http;
1300
+ }
1301
+ /**
1302
+ * Fetches a user's bookmarked illusts.
1303
+ * GET /v1/user/bookmarks/illust
1304
+ *
1305
+ * @param params - Request parameters
1306
+ *
1307
+ * @example
1308
+ * ```ts
1309
+ * // Iterate all bookmarked illusts across pages
1310
+ * for await (const illust of client.users.bookmarks.illusts({ userId: client.userId }).items()) {
1311
+ * console.log(illust.title)
1312
+ * }
1313
+ *
1314
+ * // Resume from a saved cursor
1315
+ * import { parseNextUrl } from '@book000/pixivts'
1316
+ * const page = await client.users.bookmarks.illusts({ userId: client.userId })
1317
+ * if (page.isOk && page.value.nextUrl) {
1318
+ * const cursor = parseNextUrl(page.value.nextUrl)
1319
+ * const next = await client.users.bookmarks.illusts({
1320
+ * userId: client.userId,
1321
+ * maxBookmarkId: cursor.maxBookmarkId,
1322
+ * })
1323
+ * }
1324
+ * ```
1325
+ */
1326
+ illusts(params) {
1327
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/user/bookmarks/illust", buildParams({
1328
+ userId: params.userId,
1329
+ restrict: params.restrict ?? "public",
1330
+ filter: params.filter ?? "for_ios",
1331
+ tag: params.tag,
1332
+ maxBookmarkId: params.maxBookmarkId,
1333
+ offset: params.offset
1334
+ })), this.#http, (page) => page.illusts);
1335
+ }
1336
+ /**
1337
+ * Fetches a user's bookmarked novels.
1338
+ * GET /v1/user/bookmarks/novel
1339
+ *
1340
+ * @param params - Request parameters
1341
+ *
1342
+ * @example
1343
+ * ```ts
1344
+ * // Iterate all bookmarked novels across pages
1345
+ * for await (const novel of client.users.bookmarks.novels({ userId: client.userId }).items()) {
1346
+ * console.log(novel.title)
1347
+ * }
1348
+ * ```
1349
+ */
1350
+ novels(params) {
1351
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/user/bookmarks/novel", buildParams({
1352
+ userId: params.userId,
1353
+ restrict: params.restrict ?? "public",
1354
+ filter: params.filter ?? "for_ios",
1355
+ tag: params.tag,
1356
+ maxBookmarkId: params.maxBookmarkId,
1357
+ offset: params.offset
1358
+ })), this.#http, (page) => page.novels);
1359
+ }
1259
1360
  };
1361
+ /** Methods for the user API namespace. */
1260
1362
  var UserResource = class {
1261
- /** User bookmarks sub-namespace. */
1262
- bookmarks;
1263
- #http;
1264
- constructor(http) {
1265
- this.#http = http;
1266
- this.bookmarks = new UserBookmarksResource(http);
1267
- }
1268
- /**
1269
- * Fetches detailed profile information for a user.
1270
- * GET /v1/user/detail
1271
- *
1272
- * @param params - Request parameters
1273
- */
1274
- detail(params) {
1275
- return this.#http.get(
1276
- "/v1/user/detail",
1277
- buildParams({ userId: params.userId, filter: params.filter ?? "for_ios" })
1278
- );
1279
- }
1280
- /**
1281
- * Fetches illusts posted by a user.
1282
- * GET /v1/user/illusts
1283
- *
1284
- * @param params - Request parameters
1285
- */
1286
- illusts(params) {
1287
- return PaginatedResultAsync.fromResultAsync(
1288
- this.#http.get(
1289
- "/v1/user/illusts",
1290
- buildParams({
1291
- userId: params.userId,
1292
- type: params.type,
1293
- filter: params.filter ?? "for_ios",
1294
- offset: params.offset
1295
- })
1296
- ),
1297
- this.#http,
1298
- (page) => page.illusts
1299
- );
1300
- }
1301
- /**
1302
- * Fetches novels posted by a user.
1303
- * GET /v1/user/novels
1304
- *
1305
- * @param params - Request parameters
1306
- */
1307
- novels(params) {
1308
- return PaginatedResultAsync.fromResultAsync(
1309
- this.#http.get(
1310
- "/v1/user/novels",
1311
- buildParams({
1312
- userId: params.userId,
1313
- filter: params.filter ?? "for_ios",
1314
- offset: params.offset
1315
- })
1316
- ),
1317
- this.#http,
1318
- (page) => page.novels
1319
- );
1320
- }
1321
- /**
1322
- * Fetches the list of users that a user is following.
1323
- * GET /v1/user/following
1324
- *
1325
- * @param params - Request parameters
1326
- */
1327
- following(params) {
1328
- return PaginatedResultAsync.fromResultAsync(
1329
- this.#http.get(
1330
- "/v1/user/following",
1331
- buildParams({
1332
- userId: params.userId,
1333
- restrict: params.restrict ?? "public",
1334
- offset: params.offset
1335
- })
1336
- ),
1337
- this.#http,
1338
- (page) => page.userPreviews
1339
- );
1340
- }
1341
- /**
1342
- * Follows a user.
1343
- * POST /v1/user/follow/add
1344
- *
1345
- * @param params - Request parameters
1346
- */
1347
- followAdd(params) {
1348
- const body = buildParams({
1349
- userId: params.userId,
1350
- restrict: params.restrict ?? "public"
1351
- });
1352
- return this.#http.post(
1353
- "/v1/user/follow/add",
1354
- body.toString()
1355
- );
1356
- }
1357
- /**
1358
- * Unfollows a user.
1359
- * POST /v1/user/follow/delete
1360
- *
1361
- * @param params - Request parameters
1362
- */
1363
- followDelete(params) {
1364
- const body = buildParams({ userId: String(params.userId) });
1365
- return this.#http.post(
1366
- "/v1/user/follow/delete",
1367
- body.toString()
1368
- );
1369
- }
1363
+ /** User bookmarks sub-namespace. */
1364
+ bookmarks;
1365
+ #http;
1366
+ constructor(http) {
1367
+ this.#http = http;
1368
+ this.bookmarks = new UserBookmarksResource(http);
1369
+ }
1370
+ /**
1371
+ * Fetches detailed profile information for a user.
1372
+ * GET /v1/user/detail
1373
+ *
1374
+ * @param params - Request parameters
1375
+ */
1376
+ detail(params) {
1377
+ return this.#http.get("/v1/user/detail", buildParams({
1378
+ userId: params.userId,
1379
+ filter: params.filter ?? "for_ios"
1380
+ }));
1381
+ }
1382
+ /**
1383
+ * Fetches illusts posted by a user.
1384
+ * GET /v1/user/illusts
1385
+ *
1386
+ * @param params - Request parameters
1387
+ */
1388
+ illusts(params) {
1389
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/user/illusts", buildParams({
1390
+ userId: params.userId,
1391
+ type: params.type,
1392
+ filter: params.filter ?? "for_ios",
1393
+ offset: params.offset
1394
+ })), this.#http, (page) => page.illusts);
1395
+ }
1396
+ /**
1397
+ * Fetches novels posted by a user.
1398
+ * GET /v1/user/novels
1399
+ *
1400
+ * @param params - Request parameters
1401
+ */
1402
+ novels(params) {
1403
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/user/novels", buildParams({
1404
+ userId: params.userId,
1405
+ filter: params.filter ?? "for_ios",
1406
+ offset: params.offset
1407
+ })), this.#http, (page) => page.novels);
1408
+ }
1409
+ /**
1410
+ * Fetches the list of users that a user is following.
1411
+ * GET /v1/user/following
1412
+ *
1413
+ * @param params - Request parameters
1414
+ */
1415
+ following(params) {
1416
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/user/following", buildParams({
1417
+ userId: params.userId,
1418
+ restrict: params.restrict ?? "public",
1419
+ offset: params.offset
1420
+ })), this.#http, (page) => page.userPreviews);
1421
+ }
1422
+ /**
1423
+ * Follows a user.
1424
+ * POST /v1/user/follow/add
1425
+ *
1426
+ * @param params - Request parameters
1427
+ */
1428
+ followAdd(params) {
1429
+ const body = buildParams({
1430
+ userId: params.userId,
1431
+ restrict: params.restrict ?? "public"
1432
+ });
1433
+ return this.#http.post("/v1/user/follow/add", body.toString());
1434
+ }
1435
+ /**
1436
+ * Unfollows a user.
1437
+ * POST /v1/user/follow/delete
1438
+ *
1439
+ * @param params - Request parameters
1440
+ */
1441
+ followDelete(params) {
1442
+ const body = buildParams({ userId: String(params.userId) });
1443
+ return this.#http.post("/v1/user/follow/delete", body.toString());
1444
+ }
1370
1445
  };
1371
-
1372
- // src/resources/manga.ts
1446
+ //#endregion
1447
+ //#region src/resources/manga.ts
1448
+ /** Methods for the manga API namespace. */
1373
1449
  var MangaResource = class {
1374
- #http;
1375
- constructor(http) {
1376
- this.#http = http;
1377
- }
1378
- /**
1379
- * Fetches recommended manga.
1380
- * GET /v1/manga/recommended
1381
- *
1382
- * @param params - Request parameters
1383
- */
1384
- recommended(params = {}) {
1385
- return PaginatedResultAsync.fromResultAsync(
1386
- this.#http.get(
1387
- "/v1/manga/recommended",
1388
- buildParams({
1389
- filter: params.filter ?? "for_ios",
1390
- includeRankingIllusts: true,
1391
- includePrivacyPolicy: true,
1392
- offset: params.offset
1393
- })
1394
- ),
1395
- this.#http,
1396
- (page) => page.illusts
1397
- );
1398
- }
1450
+ #http;
1451
+ constructor(http) {
1452
+ this.#http = http;
1453
+ }
1454
+ /**
1455
+ * Fetches recommended manga.
1456
+ * GET /v1/manga/recommended
1457
+ *
1458
+ * @param params - Request parameters
1459
+ */
1460
+ recommended(params = {}) {
1461
+ return PaginatedResultAsync.fromResultAsync(this.#http.get("/v1/manga/recommended", buildParams({
1462
+ filter: params.filter ?? "for_ios",
1463
+ includeRankingIllusts: true,
1464
+ includePrivacyPolicy: true,
1465
+ offset: params.offset
1466
+ })), this.#http, (page) => page.illusts);
1467
+ }
1399
1468
  };
1400
-
1401
- // src/resources/ugoira.ts
1469
+ //#endregion
1470
+ //#region src/resources/ugoira.ts
1471
+ /** Methods for the ugoira API namespace. */
1402
1472
  var UgoiraResource = class {
1403
- #http;
1404
- constructor(http) {
1405
- this.#http = http;
1406
- }
1407
- /**
1408
- * Fetches ugoira metadata (ZIP URL and per-frame timings).
1409
- * GET /v1/ugoira/metadata
1410
- *
1411
- * @param params - Request parameters
1412
- */
1413
- metadata(params) {
1414
- return this.#http.get(
1415
- "/v1/ugoira/metadata",
1416
- buildParams({ illustId: params.illustId })
1417
- );
1418
- }
1473
+ #http;
1474
+ constructor(http) {
1475
+ this.#http = http;
1476
+ }
1477
+ /**
1478
+ * Fetches ugoira metadata (ZIP URL and per-frame timings).
1479
+ * GET /v1/ugoira/metadata
1480
+ *
1481
+ * @param params - Request parameters
1482
+ */
1483
+ metadata(params) {
1484
+ return this.#http.get("/v1/ugoira/metadata", buildParams({ illustId: params.illustId }));
1485
+ }
1419
1486
  };
1420
-
1421
- // src/resources/images.ts
1487
+ //#endregion
1488
+ //#region src/resources/images.ts
1489
+ /** Methods for fetching pixiv images. */
1422
1490
  var ImageResource = class {
1423
- #http;
1424
- constructor(http) {
1425
- this.#http = http;
1426
- }
1427
- /**
1428
- * Fetches a pixiv image.
1429
- *
1430
- * Uses a browser User-Agent and Referer (required for pixiv CDN).
1431
- * No Authorization header is sent.
1432
- *
1433
- * @param imageUrl - Full CDN image URL
1434
- */
1435
- fetch(imageUrl) {
1436
- return this.#http.fetchImage(imageUrl);
1437
- }
1491
+ #http;
1492
+ constructor(http) {
1493
+ this.#http = http;
1494
+ }
1495
+ /**
1496
+ * Fetches a pixiv image.
1497
+ *
1498
+ * Uses a browser User-Agent and Referer (required for pixiv CDN).
1499
+ * No Authorization header is sent.
1500
+ *
1501
+ * @param imageUrl - Full CDN image URL
1502
+ */
1503
+ fetch(imageUrl) {
1504
+ return this.#http.fetchImage(imageUrl);
1505
+ }
1438
1506
  };
1439
-
1440
- // src/client.ts
1441
- var PixivClient = class _PixivClient {
1442
- /** Illust API namespace. */
1443
- illusts;
1444
- /** Novel API namespace. */
1445
- novels;
1446
- /** User API namespace. */
1447
- users;
1448
- /** Manga API namespace. */
1449
- manga;
1450
- /** Ugoira API namespace. */
1451
- ugoira;
1452
- /** Image fetch helpers. */
1453
- images;
1454
- #auth;
1455
- constructor(auth, http) {
1456
- this.#auth = auth;
1457
- this.illusts = new IllustResource(http);
1458
- this.novels = new NovelResource(http);
1459
- this.users = new UserResource(http);
1460
- this.manga = new MangaResource(http);
1461
- this.ugoira = new UgoiraResource(http);
1462
- this.images = new ImageResource(http);
1463
- }
1464
- /**
1465
- * Numeric user ID of the authenticated account.
1466
- *
1467
- * Available immediately after {@link PixivClient.of} resolves.
1468
- * The pixiv OAuth endpoint returns the ID as a string; this getter
1469
- * normalises it to `number` for consistency with resource method params
1470
- * (e.g. `UserBookmarksIllustParams.userId`).
1471
- *
1472
- * @example
1473
- * ```ts
1474
- * const client = await PixivClient.of(refreshToken)
1475
- * const bookmarks = await client.users.bookmarks.illusts({ userId: client.userId })
1476
- * ```
1477
- */
1478
- get userId() {
1479
- const id = Number(this.#auth.userId);
1480
- if (Number.isNaN(id)) {
1481
- throw new TypeError(`Invalid userId: "${this.#auth.userId}"`);
1482
- }
1483
- return id;
1484
- }
1485
- /**
1486
- * Returns the current OAuth access token.
1487
- *
1488
- * The access token is short-lived and changes after each call to
1489
- * {@link PixivClient.of} and after each automatic token refresh triggered
1490
- * by a 401 response.
1491
- *
1492
- * @returns The current bearer access token string
1493
- */
1494
- getAccessToken() {
1495
- return this.#auth.accessToken;
1496
- }
1497
- /**
1498
- * Returns the current OAuth refresh token.
1499
- *
1500
- * The refresh token is long-lived and is used to obtain new access tokens.
1501
- * It may rotate after a successful token refresh.
1502
- *
1503
- * @returns The current refresh token string
1504
- */
1505
- getRefreshToken() {
1506
- return this.#auth.refreshToken;
1507
- }
1508
- /**
1509
- * Creates a PixivClient by refreshing the given token.
1510
- *
1511
- * @param refreshToken - Pixiv refresh token
1512
- * @param options - Optional retry and response interceptor configuration
1513
- * @returns A fully initialised {@link PixivClient}
1514
- */
1515
- static async of(refreshToken, options) {
1516
- const auth = await AuthManager.login(refreshToken);
1517
- const http = new HttpClient(auth, options);
1518
- return new _PixivClient(auth, http);
1519
- }
1507
+ //#endregion
1508
+ //#region src/client.ts
1509
+ /**
1510
+ * PixivClient main entry point for the @book000/pixivts library.
1511
+ *
1512
+ * @example
1513
+ * ```ts
1514
+ * const client = await PixivClient.of(process.env.PIXIV_REFRESH_TOKEN!)
1515
+ * const result = await client.illusts.detail({ illustId: 12345 })
1516
+ * if (result.isOk) console.log(result.value.illust.title)
1517
+ * ```
1518
+ */
1519
+ /**
1520
+ * Main client for the pixiv API.
1521
+ *
1522
+ * Create an instance via {@link PixivClient.of} — the constructor is private
1523
+ * because initialisation requires an async token refresh.
1524
+ */
1525
+ var PixivClient = class PixivClient {
1526
+ /** Illust API namespace. */
1527
+ illusts;
1528
+ /** Novel API namespace. */
1529
+ novels;
1530
+ /** User API namespace. */
1531
+ users;
1532
+ /** Manga API namespace. */
1533
+ manga;
1534
+ /** Ugoira API namespace. */
1535
+ ugoira;
1536
+ /** Image fetch helpers. */
1537
+ images;
1538
+ #auth;
1539
+ constructor(auth, http) {
1540
+ this.#auth = auth;
1541
+ this.illusts = new IllustResource(http);
1542
+ this.novels = new NovelResource(http);
1543
+ this.users = new UserResource(http);
1544
+ this.manga = new MangaResource(http);
1545
+ this.ugoira = new UgoiraResource(http);
1546
+ this.images = new ImageResource(http);
1547
+ }
1548
+ /**
1549
+ * Numeric user ID of the authenticated account.
1550
+ *
1551
+ * Available immediately after {@link PixivClient.of} resolves.
1552
+ * The pixiv OAuth endpoint returns the ID as a string; this getter
1553
+ * normalises it to `number` for consistency with resource method params
1554
+ * (e.g. `UserBookmarksIllustParams.userId`).
1555
+ *
1556
+ * @example
1557
+ * ```ts
1558
+ * const client = await PixivClient.of(refreshToken)
1559
+ * const bookmarks = await client.users.bookmarks.illusts({ userId: client.userId })
1560
+ * ```
1561
+ */
1562
+ get userId() {
1563
+ const id = Number(this.#auth.userId);
1564
+ if (Number.isNaN(id)) throw new TypeError(`Invalid userId: "${this.#auth.userId}"`);
1565
+ return id;
1566
+ }
1567
+ /**
1568
+ * Returns the current OAuth access token.
1569
+ *
1570
+ * The access token is short-lived and changes after each call to
1571
+ * {@link PixivClient.of} and after each automatic token refresh triggered
1572
+ * by a 401 response.
1573
+ *
1574
+ * @returns The current bearer access token string
1575
+ */
1576
+ getAccessToken() {
1577
+ return this.#auth.accessToken;
1578
+ }
1579
+ /**
1580
+ * Returns the current OAuth refresh token.
1581
+ *
1582
+ * The refresh token is long-lived and is used to obtain new access tokens.
1583
+ * It may rotate after a successful token refresh.
1584
+ *
1585
+ * @returns The current refresh token string
1586
+ */
1587
+ getRefreshToken() {
1588
+ return this.#auth.refreshToken;
1589
+ }
1590
+ /**
1591
+ * Creates a PixivClient by refreshing the given token.
1592
+ *
1593
+ * @param refreshToken - Pixiv refresh token
1594
+ * @param options - Optional retry and response interceptor configuration
1595
+ * @returns A fully initialised {@link PixivClient}
1596
+ */
1597
+ static async of(refreshToken, options) {
1598
+ const auth = await AuthManager.login(refreshToken);
1599
+ return new PixivClient(auth, new HttpClient(auth, options));
1600
+ }
1520
1601
  };
1521
-
1602
+ //#endregion
1522
1603
  exports.BookmarkRestrict = BookmarkRestrict;
1523
1604
  exports.FollowRestrict = FollowRestrict;
1524
1605
  exports.NovelRankingMode = NovelRankingMode;