ani-client 1.4.4 → 1.5.1
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/README.md +3 -3
- package/dist/index.d.mts +221 -354
- package/dist/index.d.ts +221 -354
- package/dist/index.js +731 -653
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +728 -654
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -7
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,137 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/utils/markdown.ts
|
|
2
|
+
function parseAniListMarkdown(text) {
|
|
3
|
+
if (!text) return "";
|
|
4
|
+
let html = text;
|
|
5
|
+
html = html.replace(/~!(.*?)!~/gs, '<span class="anilist-spoiler">$1</span>');
|
|
6
|
+
html = html.replace(/~~~(.*?)~~~/gs, '<div class="anilist-center">$1</div>');
|
|
7
|
+
html = html.replace(/img(\d+)\((.*?)\)/gi, '<img src="$2" width="$1" alt="" class="anilist-image" />');
|
|
8
|
+
html = html.replace(/img\((.*?)\)/gi, '<img src="$1" alt="" class="anilist-image" />');
|
|
9
|
+
html = html.replace(
|
|
10
|
+
/youtube\((.*?)\)/gi,
|
|
11
|
+
'<iframe src="https://www.youtube.com/embed/$1" frameborder="0" allowfullscreen class="anilist-youtube"></iframe>'
|
|
12
|
+
);
|
|
13
|
+
html = html.replace(/webm\((.*?)\)/gi, '<video src="$1" controls class="anilist-webm"></video>');
|
|
14
|
+
html = html.replace(/__(.*?)__/g, "<strong>$1</strong>");
|
|
15
|
+
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
|
16
|
+
html = html.replace(/_(.*?)_/g, "<em>$1</em>");
|
|
17
|
+
html = html.replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/g, "<em>$1</em>");
|
|
18
|
+
html = html.replace(/~~(.*?)~~/g, "<del>$1</del>");
|
|
19
|
+
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
20
|
+
html = html.replace(/\r\n/g, "\n");
|
|
21
|
+
const paragraphs = html.split(/\n{2,}/);
|
|
22
|
+
html = paragraphs.map((p) => {
|
|
23
|
+
const withBr = p.replace(/\n/g, "<br />");
|
|
24
|
+
if (withBr.match(/^(<div|<iframe|<video|<img)/)) {
|
|
25
|
+
return withBr;
|
|
26
|
+
}
|
|
27
|
+
return `<p>${withBr}</p>`;
|
|
28
|
+
}).join("\n");
|
|
29
|
+
return html;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/utils/index.ts
|
|
33
|
+
function normalizeQuery(query) {
|
|
34
|
+
return query.replace(/\s+/g, " ").trim();
|
|
35
|
+
}
|
|
36
|
+
function clampPerPage(value) {
|
|
37
|
+
return Math.min(Math.max(value, 1), 50);
|
|
38
|
+
}
|
|
39
|
+
function chunk(arr, size) {
|
|
40
|
+
const chunks = [];
|
|
41
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
42
|
+
chunks.push(arr.slice(i, i + size));
|
|
43
|
+
}
|
|
44
|
+
return chunks;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/cache/index.ts
|
|
48
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
49
|
+
var MemoryCache = class {
|
|
50
|
+
constructor(options = {}) {
|
|
51
|
+
this.store = /* @__PURE__ */ new Map();
|
|
52
|
+
this.ttl = options.ttl ?? ONE_DAY_MS;
|
|
53
|
+
this.maxSize = options.maxSize ?? 500;
|
|
54
|
+
this.enabled = options.enabled ?? true;
|
|
55
|
+
}
|
|
56
|
+
/** Build a deterministic cache key from a query + variables pair. */
|
|
57
|
+
static key(query, variables) {
|
|
58
|
+
const normalized = normalizeQuery(query);
|
|
59
|
+
return `${normalized}|${JSON.stringify(variables, Object.keys(variables).sort())}`;
|
|
60
|
+
}
|
|
61
|
+
/** Retrieve a cached value, or `undefined` if missing / expired. */
|
|
62
|
+
get(key) {
|
|
63
|
+
if (!this.enabled) return void 0;
|
|
64
|
+
const entry = this.store.get(key);
|
|
65
|
+
if (!entry) return void 0;
|
|
66
|
+
if (Date.now() > entry.expiresAt) {
|
|
67
|
+
this.store.delete(key);
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
this.store.delete(key);
|
|
71
|
+
this.store.set(key, entry);
|
|
72
|
+
return entry.data;
|
|
73
|
+
}
|
|
74
|
+
/** Store a value in the cache. */
|
|
75
|
+
set(key, data) {
|
|
76
|
+
if (!this.enabled) return;
|
|
77
|
+
this.store.delete(key);
|
|
78
|
+
if (this.maxSize > 0 && this.store.size >= this.maxSize) {
|
|
79
|
+
const firstKey = this.store.keys().next().value;
|
|
80
|
+
if (firstKey !== void 0) this.store.delete(firstKey);
|
|
81
|
+
}
|
|
82
|
+
this.store.set(key, { data, expiresAt: Date.now() + this.ttl });
|
|
83
|
+
}
|
|
84
|
+
/** Remove a specific entry. */
|
|
85
|
+
delete(key) {
|
|
86
|
+
return this.store.delete(key);
|
|
87
|
+
}
|
|
88
|
+
/** Clear the entire cache. */
|
|
89
|
+
clear() {
|
|
90
|
+
this.store.clear();
|
|
91
|
+
}
|
|
92
|
+
/** Number of entries currently stored. */
|
|
93
|
+
get size() {
|
|
94
|
+
return this.store.size;
|
|
95
|
+
}
|
|
96
|
+
/** Return all cache keys. */
|
|
97
|
+
keys() {
|
|
98
|
+
return [...this.store.keys()];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Remove all entries whose key matches the given pattern.
|
|
102
|
+
*
|
|
103
|
+
* - **String**: treated as a substring match (e.g. `"Media"` removes all keys containing `"Media"`).
|
|
104
|
+
* - **RegExp**: tested against each key directly.
|
|
105
|
+
*
|
|
106
|
+
* @param pattern — A string (substring match) or RegExp.
|
|
107
|
+
* @returns Number of entries removed.
|
|
108
|
+
*/
|
|
109
|
+
invalidate(pattern) {
|
|
110
|
+
const test = typeof pattern === "string" ? (key) => key.includes(pattern) : (key) => pattern.test(key);
|
|
111
|
+
const toDelete = [];
|
|
112
|
+
for (const key of this.store.keys()) {
|
|
113
|
+
if (test(key)) toDelete.push(key);
|
|
114
|
+
}
|
|
115
|
+
for (const key of toDelete) this.store.delete(key);
|
|
116
|
+
return toDelete.length;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/errors/index.ts
|
|
121
|
+
var AniListError = class _AniListError extends Error {
|
|
122
|
+
constructor(message, status, errors = []) {
|
|
123
|
+
super(message);
|
|
124
|
+
this.name = "AniListError";
|
|
125
|
+
this.status = status;
|
|
126
|
+
this.errors = errors;
|
|
127
|
+
Object.setPrototypeOf(this, _AniListError.prototype);
|
|
128
|
+
if (Error.captureStackTrace) {
|
|
129
|
+
Error.captureStackTrace(this, _AniListError);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/queries/fragments.ts
|
|
2
135
|
var MEDIA_FIELDS_BASE = `
|
|
3
136
|
id
|
|
4
137
|
idMal
|
|
@@ -33,6 +166,13 @@ var MEDIA_FIELDS_BASE = `
|
|
|
33
166
|
studios { nodes { id name isAnimationStudio siteUrl } }
|
|
34
167
|
isAdult
|
|
35
168
|
siteUrl
|
|
169
|
+
nextAiringEpisode {
|
|
170
|
+
id
|
|
171
|
+
airingAt
|
|
172
|
+
episode
|
|
173
|
+
mediaId
|
|
174
|
+
timeUntilAiring
|
|
175
|
+
}
|
|
36
176
|
`;
|
|
37
177
|
var RELATIONS_FIELDS = `
|
|
38
178
|
relations {
|
|
@@ -184,6 +324,45 @@ var USER_FIELDS = `
|
|
|
184
324
|
manga { count meanScore minutesWatched episodesWatched chaptersRead volumesRead }
|
|
185
325
|
}
|
|
186
326
|
`;
|
|
327
|
+
var MEDIA_LIST_FIELDS = `
|
|
328
|
+
id
|
|
329
|
+
mediaId
|
|
330
|
+
status
|
|
331
|
+
score(format: POINT_100)
|
|
332
|
+
progress
|
|
333
|
+
progressVolumes
|
|
334
|
+
repeat
|
|
335
|
+
priority
|
|
336
|
+
private
|
|
337
|
+
notes
|
|
338
|
+
startedAt { year month day }
|
|
339
|
+
completedAt { year month day }
|
|
340
|
+
updatedAt
|
|
341
|
+
createdAt
|
|
342
|
+
media {
|
|
343
|
+
${MEDIA_FIELDS_BASE}
|
|
344
|
+
}
|
|
345
|
+
`;
|
|
346
|
+
var STUDIO_FIELDS = `
|
|
347
|
+
id
|
|
348
|
+
name
|
|
349
|
+
isAnimationStudio
|
|
350
|
+
siteUrl
|
|
351
|
+
favourites
|
|
352
|
+
media(page: 1, perPage: 25, sort: POPULARITY_DESC) {
|
|
353
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
354
|
+
nodes {
|
|
355
|
+
id
|
|
356
|
+
title { romaji english native userPreferred }
|
|
357
|
+
type
|
|
358
|
+
format
|
|
359
|
+
coverImage { large medium }
|
|
360
|
+
siteUrl
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
`;
|
|
364
|
+
|
|
365
|
+
// src/queries/media.ts
|
|
187
366
|
var QUERY_MEDIA_BY_ID = `
|
|
188
367
|
query ($id: Int!) {
|
|
189
368
|
Media(id: $id) {
|
|
@@ -200,6 +379,10 @@ query (
|
|
|
200
379
|
$seasonYear: Int,
|
|
201
380
|
$genre: String,
|
|
202
381
|
$tag: String,
|
|
382
|
+
$genre_in: [String],
|
|
383
|
+
$tag_in: [String],
|
|
384
|
+
$genre_not_in: [String],
|
|
385
|
+
$tag_not_in: [String],
|
|
203
386
|
$isAdult: Boolean,
|
|
204
387
|
$sort: [MediaSort],
|
|
205
388
|
$page: Int,
|
|
@@ -216,6 +399,10 @@ query (
|
|
|
216
399
|
seasonYear: $seasonYear,
|
|
217
400
|
genre: $genre,
|
|
218
401
|
tag: $tag,
|
|
402
|
+
genre_in: $genre_in,
|
|
403
|
+
tag_in: $tag_in,
|
|
404
|
+
genre_not_in: $genre_not_in,
|
|
405
|
+
tag_not_in: $tag_not_in,
|
|
219
406
|
isAdult: $isAdult,
|
|
220
407
|
sort: $sort
|
|
221
408
|
) {
|
|
@@ -232,70 +419,6 @@ query ($type: MediaType, $page: Int, $perPage: Int) {
|
|
|
232
419
|
}
|
|
233
420
|
}
|
|
234
421
|
}`;
|
|
235
|
-
var QUERY_CHARACTER_BY_ID = `
|
|
236
|
-
query ($id: Int!) {
|
|
237
|
-
Character(id: $id) {
|
|
238
|
-
${CHARACTER_FIELDS}
|
|
239
|
-
}
|
|
240
|
-
}`;
|
|
241
|
-
var QUERY_CHARACTER_BY_ID_WITH_VA = `
|
|
242
|
-
query ($id: Int!) {
|
|
243
|
-
Character(id: $id) {
|
|
244
|
-
${CHARACTER_FIELDS_WITH_VA}
|
|
245
|
-
}
|
|
246
|
-
}`;
|
|
247
|
-
var QUERY_CHARACTER_SEARCH = `
|
|
248
|
-
query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
|
|
249
|
-
Page(page: $page, perPage: $perPage) {
|
|
250
|
-
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
251
|
-
characters(search: $search, sort: $sort) {
|
|
252
|
-
${CHARACTER_FIELDS}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}`;
|
|
256
|
-
var QUERY_CHARACTER_SEARCH_WITH_VA = `
|
|
257
|
-
query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
|
|
258
|
-
Page(page: $page, perPage: $perPage) {
|
|
259
|
-
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
260
|
-
characters(search: $search, sort: $sort) {
|
|
261
|
-
${CHARACTER_FIELDS_WITH_VA}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}`;
|
|
265
|
-
var QUERY_STAFF_BY_ID = `
|
|
266
|
-
query ($id: Int!) {
|
|
267
|
-
Staff(id: $id) {
|
|
268
|
-
${STAFF_FIELDS}
|
|
269
|
-
}
|
|
270
|
-
}`;
|
|
271
|
-
var QUERY_STAFF_BY_ID_WITH_MEDIA = `
|
|
272
|
-
query ($id: Int!, $perPage: Int) {
|
|
273
|
-
Staff(id: $id) {
|
|
274
|
-
${STAFF_FIELDS}
|
|
275
|
-
${STAFF_MEDIA_FIELDS}
|
|
276
|
-
}
|
|
277
|
-
}`;
|
|
278
|
-
var QUERY_STAFF_SEARCH = `
|
|
279
|
-
query ($search: String, $sort: [StaffSort], $page: Int, $perPage: Int) {
|
|
280
|
-
Page(page: $page, perPage: $perPage) {
|
|
281
|
-
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
282
|
-
staff(search: $search, sort: $sort) {
|
|
283
|
-
${STAFF_FIELDS}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}`;
|
|
287
|
-
var QUERY_USER_BY_ID = `
|
|
288
|
-
query ($id: Int!) {
|
|
289
|
-
User(id: $id) {
|
|
290
|
-
${USER_FIELDS}
|
|
291
|
-
}
|
|
292
|
-
}`;
|
|
293
|
-
var QUERY_USER_BY_NAME = `
|
|
294
|
-
query ($name: String!) {
|
|
295
|
-
User(name: $name) {
|
|
296
|
-
${USER_FIELDS}
|
|
297
|
-
}
|
|
298
|
-
}`;
|
|
299
422
|
var QUERY_AIRING_SCHEDULE = `
|
|
300
423
|
query ($airingAt_greater: Int, $airingAt_lesser: Int, $sort: [AiringSort], $page: Int, $perPage: Int) {
|
|
301
424
|
Page(page: $page, perPage: $perPage) {
|
|
@@ -339,25 +462,6 @@ query ($season: MediaSeason!, $seasonYear: Int!, $type: MediaType, $sort: [Media
|
|
|
339
462
|
}
|
|
340
463
|
}
|
|
341
464
|
}`;
|
|
342
|
-
var MEDIA_LIST_FIELDS = `
|
|
343
|
-
id
|
|
344
|
-
mediaId
|
|
345
|
-
status
|
|
346
|
-
score(format: POINT_100)
|
|
347
|
-
progress
|
|
348
|
-
progressVolumes
|
|
349
|
-
repeat
|
|
350
|
-
priority
|
|
351
|
-
private
|
|
352
|
-
notes
|
|
353
|
-
startedAt { year month day }
|
|
354
|
-
completedAt { year month day }
|
|
355
|
-
updatedAt
|
|
356
|
-
createdAt
|
|
357
|
-
media {
|
|
358
|
-
${MEDIA_FIELDS_BASE}
|
|
359
|
-
}
|
|
360
|
-
`;
|
|
361
465
|
var QUERY_RECOMMENDATIONS = `
|
|
362
466
|
query ($mediaId: Int!, $page: Int, $perPage: Int, $sort: [RecommendationSort]) {
|
|
363
467
|
Media(id: $mediaId) {
|
|
@@ -392,6 +496,85 @@ query ($mediaId: Int!, $page: Int, $perPage: Int, $sort: [RecommendationSort]) {
|
|
|
392
496
|
}
|
|
393
497
|
}
|
|
394
498
|
}`;
|
|
499
|
+
|
|
500
|
+
// src/queries/character.ts
|
|
501
|
+
var QUERY_CHARACTER_BY_ID = `
|
|
502
|
+
query ($id: Int!) {
|
|
503
|
+
Character(id: $id) {
|
|
504
|
+
${CHARACTER_FIELDS}
|
|
505
|
+
}
|
|
506
|
+
}`;
|
|
507
|
+
var QUERY_CHARACTER_BY_ID_WITH_VA = `
|
|
508
|
+
query ($id: Int!) {
|
|
509
|
+
Character(id: $id) {
|
|
510
|
+
${CHARACTER_FIELDS_WITH_VA}
|
|
511
|
+
}
|
|
512
|
+
}`;
|
|
513
|
+
var QUERY_CHARACTER_SEARCH = `
|
|
514
|
+
query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
|
|
515
|
+
Page(page: $page, perPage: $perPage) {
|
|
516
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
517
|
+
characters(search: $search, sort: $sort) {
|
|
518
|
+
${CHARACTER_FIELDS}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}`;
|
|
522
|
+
var QUERY_CHARACTER_SEARCH_WITH_VA = `
|
|
523
|
+
query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
|
|
524
|
+
Page(page: $page, perPage: $perPage) {
|
|
525
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
526
|
+
characters(search: $search, sort: $sort) {
|
|
527
|
+
${CHARACTER_FIELDS_WITH_VA}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}`;
|
|
531
|
+
|
|
532
|
+
// src/queries/staff.ts
|
|
533
|
+
var QUERY_STAFF_BY_ID = `
|
|
534
|
+
query ($id: Int!) {
|
|
535
|
+
Staff(id: $id) {
|
|
536
|
+
${STAFF_FIELDS}
|
|
537
|
+
}
|
|
538
|
+
}`;
|
|
539
|
+
var QUERY_STAFF_BY_ID_WITH_MEDIA = `
|
|
540
|
+
query ($id: Int!, $perPage: Int) {
|
|
541
|
+
Staff(id: $id) {
|
|
542
|
+
${STAFF_FIELDS}
|
|
543
|
+
${STAFF_MEDIA_FIELDS}
|
|
544
|
+
}
|
|
545
|
+
}`;
|
|
546
|
+
var QUERY_STAFF_SEARCH = `
|
|
547
|
+
query ($search: String, $sort: [StaffSort], $page: Int, $perPage: Int) {
|
|
548
|
+
Page(page: $page, perPage: $perPage) {
|
|
549
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
550
|
+
staff(search: $search, sort: $sort) {
|
|
551
|
+
${STAFF_FIELDS}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}`;
|
|
555
|
+
|
|
556
|
+
// src/queries/user.ts
|
|
557
|
+
var QUERY_USER_BY_ID = `
|
|
558
|
+
query ($id: Int!) {
|
|
559
|
+
User(id: $id) {
|
|
560
|
+
${USER_FIELDS}
|
|
561
|
+
}
|
|
562
|
+
}`;
|
|
563
|
+
var QUERY_USER_BY_NAME = `
|
|
564
|
+
query ($name: String!) {
|
|
565
|
+
User(name: $name) {
|
|
566
|
+
${USER_FIELDS}
|
|
567
|
+
}
|
|
568
|
+
}`;
|
|
569
|
+
var QUERY_USER_SEARCH = `
|
|
570
|
+
query ($search: String, $sort: [UserSort], $page: Int, $perPage: Int) {
|
|
571
|
+
Page(page: $page, perPage: $perPage) {
|
|
572
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
573
|
+
users(search: $search, sort: $sort) {
|
|
574
|
+
${USER_FIELDS}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}`;
|
|
395
578
|
var QUERY_USER_MEDIA_LIST = `
|
|
396
579
|
query ($userId: Int, $userName: String, $type: MediaType!, $status: MediaListStatus, $sort: [MediaListSort], $page: Int, $perPage: Int) {
|
|
397
580
|
Page(page: $page, perPage: $perPage) {
|
|
@@ -401,24 +584,8 @@ query ($userId: Int, $userName: String, $type: MediaType!, $status: MediaListSta
|
|
|
401
584
|
}
|
|
402
585
|
}
|
|
403
586
|
}`;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
name
|
|
407
|
-
isAnimationStudio
|
|
408
|
-
siteUrl
|
|
409
|
-
favourites
|
|
410
|
-
media(page: 1, perPage: 25, sort: POPULARITY_DESC) {
|
|
411
|
-
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
412
|
-
nodes {
|
|
413
|
-
id
|
|
414
|
-
title { romaji english native userPreferred }
|
|
415
|
-
type
|
|
416
|
-
format
|
|
417
|
-
coverImage { large medium }
|
|
418
|
-
siteUrl
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
`;
|
|
587
|
+
|
|
588
|
+
// src/queries/studio.ts
|
|
422
589
|
var QUERY_STUDIO_BY_ID = `
|
|
423
590
|
query ($id: Int!) {
|
|
424
591
|
Studio(id: $id) {
|
|
@@ -434,6 +601,8 @@ query ($search: String, $page: Int, $perPage: Int) {
|
|
|
434
601
|
}
|
|
435
602
|
}
|
|
436
603
|
}`;
|
|
604
|
+
|
|
605
|
+
// src/queries/metadata.ts
|
|
437
606
|
var QUERY_GENRES = `
|
|
438
607
|
query {
|
|
439
608
|
GenreCollection
|
|
@@ -448,6 +617,8 @@ query {
|
|
|
448
617
|
isAdult
|
|
449
618
|
}
|
|
450
619
|
}`;
|
|
620
|
+
|
|
621
|
+
// src/queries/builders.ts
|
|
451
622
|
function buildMediaByIdQuery(include) {
|
|
452
623
|
if (!include) return QUERY_MEDIA_BY_ID;
|
|
453
624
|
const extra = [];
|
|
@@ -550,95 +721,70 @@ var buildBatchMediaQuery = (ids) => buildBatchQuery(ids, "Media", MEDIA_FIELDS_B
|
|
|
550
721
|
var buildBatchCharacterQuery = (ids) => buildBatchQuery(ids, "Character", CHARACTER_FIELDS, "c");
|
|
551
722
|
var buildBatchStaffQuery = (ids) => buildBatchQuery(ids, "Staff", STAFF_FIELDS, "s");
|
|
552
723
|
|
|
553
|
-
// src/
|
|
554
|
-
var
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
return void 0;
|
|
575
|
-
}
|
|
576
|
-
this.store.delete(key);
|
|
577
|
-
this.store.set(key, entry);
|
|
578
|
-
return entry.data;
|
|
579
|
-
}
|
|
580
|
-
/** Store a value in the cache. */
|
|
581
|
-
set(key, data) {
|
|
582
|
-
if (!this.enabled) return;
|
|
583
|
-
this.store.delete(key);
|
|
584
|
-
if (this.maxSize > 0 && this.store.size >= this.maxSize) {
|
|
585
|
-
const firstKey = this.store.keys().next().value;
|
|
586
|
-
if (firstKey !== void 0) this.store.delete(firstKey);
|
|
587
|
-
}
|
|
588
|
-
this.store.set(key, { data, expiresAt: Date.now() + this.ttl });
|
|
724
|
+
// src/queries/thread.ts
|
|
725
|
+
var THREAD_FIELDS = `
|
|
726
|
+
id
|
|
727
|
+
title
|
|
728
|
+
body(asHtml: false)
|
|
729
|
+
userId
|
|
730
|
+
replyUserId
|
|
731
|
+
replyCommentId
|
|
732
|
+
replyCount
|
|
733
|
+
viewCount
|
|
734
|
+
isLocked
|
|
735
|
+
isSticky
|
|
736
|
+
isSubscribed
|
|
737
|
+
repliedAt
|
|
738
|
+
createdAt
|
|
739
|
+
updatedAt
|
|
740
|
+
siteUrl
|
|
741
|
+
user {
|
|
742
|
+
id
|
|
743
|
+
name
|
|
744
|
+
avatar { large medium }
|
|
589
745
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
746
|
+
replyUser {
|
|
747
|
+
id
|
|
748
|
+
name
|
|
749
|
+
avatar { large medium }
|
|
593
750
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
751
|
+
categories {
|
|
752
|
+
id
|
|
753
|
+
name
|
|
597
754
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
755
|
+
mediaCategories {
|
|
756
|
+
id
|
|
757
|
+
title { romaji english native userPreferred }
|
|
758
|
+
type
|
|
759
|
+
coverImage { large medium }
|
|
760
|
+
siteUrl
|
|
601
761
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
762
|
+
likes {
|
|
763
|
+
id
|
|
764
|
+
name
|
|
605
765
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
*/
|
|
612
|
-
invalidate(pattern) {
|
|
613
|
-
const regex = typeof pattern === "string" ? new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) : pattern;
|
|
614
|
-
const toDelete = [];
|
|
615
|
-
for (const key of this.store.keys()) {
|
|
616
|
-
if (regex.test(key)) toDelete.push(key);
|
|
617
|
-
}
|
|
618
|
-
for (const key of toDelete) this.store.delete(key);
|
|
619
|
-
return toDelete.length;
|
|
766
|
+
`;
|
|
767
|
+
var QUERY_THREAD_BY_ID = `
|
|
768
|
+
query ($id: Int!) {
|
|
769
|
+
Thread(id: $id) {
|
|
770
|
+
${THREAD_FIELDS}
|
|
620
771
|
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
this.status = status;
|
|
629
|
-
this.errors = errors;
|
|
630
|
-
Object.setPrototypeOf(this, _AniListError.prototype);
|
|
631
|
-
if (Error.captureStackTrace) {
|
|
632
|
-
Error.captureStackTrace(this, _AniListError);
|
|
772
|
+
}`;
|
|
773
|
+
var QUERY_THREAD_SEARCH = `
|
|
774
|
+
query ($search: String, $mediaCategoryId: Int, $categoryId: Int, $sort: [ThreadSort], $page: Int, $perPage: Int) {
|
|
775
|
+
Page(page: $page, perPage: $perPage) {
|
|
776
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
777
|
+
threads(search: $search, mediaCategoryId: $mediaCategoryId, categoryId: $categoryId, sort: $sort) {
|
|
778
|
+
${THREAD_FIELDS}
|
|
633
779
|
}
|
|
634
780
|
}
|
|
635
|
-
}
|
|
781
|
+
}`;
|
|
636
782
|
|
|
637
783
|
// src/rate-limiter/index.ts
|
|
638
784
|
var RateLimiter = class {
|
|
639
785
|
constructor(options = {}) {
|
|
640
|
-
|
|
641
|
-
this.
|
|
786
|
+
this.head = 0;
|
|
787
|
+
this.count = 0;
|
|
642
788
|
this.maxRequests = options.maxRequests ?? 85;
|
|
643
789
|
this.windowMs = options.windowMs ?? 6e4;
|
|
644
790
|
this.maxRetries = options.maxRetries ?? 3;
|
|
@@ -646,24 +792,34 @@ var RateLimiter = class {
|
|
|
646
792
|
this.enabled = options.enabled ?? true;
|
|
647
793
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
648
794
|
this.retryOnNetworkError = options.retryOnNetworkError ?? true;
|
|
795
|
+
this.timestamps = new Array(this.maxRequests).fill(0);
|
|
649
796
|
}
|
|
650
797
|
/**
|
|
651
798
|
* Wait until it's safe to make a request (respects rate limit window).
|
|
652
799
|
*/
|
|
653
800
|
async acquire() {
|
|
654
801
|
if (!this.enabled) return;
|
|
802
|
+
if (this.count >= this.maxRequests) {
|
|
803
|
+
const oldest = this.timestamps[this.head];
|
|
804
|
+
const now2 = Date.now();
|
|
805
|
+
const elapsed = now2 - oldest;
|
|
806
|
+
if (elapsed < this.windowMs) {
|
|
807
|
+
const waitMs = this.windowMs - elapsed + 50;
|
|
808
|
+
await this.sleep(waitMs);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
655
811
|
const now = Date.now();
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
812
|
+
if (this.count < this.maxRequests) {
|
|
813
|
+
this.timestamps[(this.head + this.count) % this.maxRequests] = now;
|
|
814
|
+
this.count++;
|
|
815
|
+
} else {
|
|
816
|
+
this.timestamps[this.head] = now;
|
|
817
|
+
this.head = (this.head + 1) % this.maxRequests;
|
|
662
818
|
}
|
|
663
|
-
this.timestamps.push(Date.now());
|
|
664
819
|
}
|
|
665
820
|
/**
|
|
666
821
|
* Execute a fetch with automatic retry on 429 responses and network errors.
|
|
822
|
+
* Uses exponential backoff with jitter for retry delays.
|
|
667
823
|
*/
|
|
668
824
|
async fetchWithRetry(url, init, hooks) {
|
|
669
825
|
await this.acquire();
|
|
@@ -676,7 +832,7 @@ var RateLimiter = class {
|
|
|
676
832
|
lastResponse = res;
|
|
677
833
|
if (attempt === this.maxRetries) break;
|
|
678
834
|
const retryAfter = res.headers.get("Retry-After");
|
|
679
|
-
const delayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1e3 : this.
|
|
835
|
+
const delayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1e3 : this.exponentialDelay(attempt);
|
|
680
836
|
hooks?.onRateLimit?.(delayMs);
|
|
681
837
|
hooks?.onRetry?.(attempt + 1, "HTTP 429", delayMs);
|
|
682
838
|
await this.sleep(delayMs);
|
|
@@ -684,7 +840,7 @@ var RateLimiter = class {
|
|
|
684
840
|
} catch (err) {
|
|
685
841
|
lastError = err;
|
|
686
842
|
if (this.retryOnNetworkError && isNetworkError(err) && attempt < this.maxRetries) {
|
|
687
|
-
const delayMs = this.
|
|
843
|
+
const delayMs = this.exponentialDelay(attempt);
|
|
688
844
|
hooks?.onRetry?.(attempt + 1, `Network error: ${err.message}`, delayMs);
|
|
689
845
|
await this.sleep(delayMs);
|
|
690
846
|
await this.acquire();
|
|
@@ -696,6 +852,12 @@ var RateLimiter = class {
|
|
|
696
852
|
if (lastResponse) return lastResponse;
|
|
697
853
|
throw lastError;
|
|
698
854
|
}
|
|
855
|
+
/** @internal — Exponential backoff with jitter, capped at 30s */
|
|
856
|
+
exponentialDelay(attempt) {
|
|
857
|
+
const base = this.retryDelayMs * 2 ** attempt;
|
|
858
|
+
const jitter = Math.random() * 1e3;
|
|
859
|
+
return Math.min(base + jitter, 3e4);
|
|
860
|
+
}
|
|
699
861
|
/** @internal */
|
|
700
862
|
async fetchWithTimeout(url, init) {
|
|
701
863
|
if (this.timeoutMs <= 0) return fetch(url, init);
|
|
@@ -729,12 +891,42 @@ function isNetworkError(err) {
|
|
|
729
891
|
return false;
|
|
730
892
|
}
|
|
731
893
|
|
|
894
|
+
// src/client/character.ts
|
|
895
|
+
async function getCharacter(client, id, include) {
|
|
896
|
+
const query = include?.voiceActors ? QUERY_CHARACTER_BY_ID_WITH_VA : QUERY_CHARACTER_BY_ID;
|
|
897
|
+
const data = await client.request(query, { id });
|
|
898
|
+
return data.Character;
|
|
899
|
+
}
|
|
900
|
+
async function searchCharacters(client, options = {}) {
|
|
901
|
+
const { query: search, page = 1, perPage = 20, sort, voiceActors } = options;
|
|
902
|
+
const gqlQuery = voiceActors ? QUERY_CHARACTER_SEARCH_WITH_VA : QUERY_CHARACTER_SEARCH;
|
|
903
|
+
return client.pagedRequest(gqlQuery, { search, sort, page, perPage: clampPerPage(perPage) }, "characters");
|
|
904
|
+
}
|
|
905
|
+
|
|
732
906
|
// src/types/media.ts
|
|
733
907
|
var MediaType = /* @__PURE__ */ ((MediaType2) => {
|
|
734
908
|
MediaType2["ANIME"] = "ANIME";
|
|
735
909
|
MediaType2["MANGA"] = "MANGA";
|
|
736
910
|
return MediaType2;
|
|
737
911
|
})(MediaType || {});
|
|
912
|
+
var MediaSource = /* @__PURE__ */ ((MediaSource2) => {
|
|
913
|
+
MediaSource2["ORIGINAL"] = "ORIGINAL";
|
|
914
|
+
MediaSource2["MANGA"] = "MANGA";
|
|
915
|
+
MediaSource2["LIGHT_NOVEL"] = "LIGHT_NOVEL";
|
|
916
|
+
MediaSource2["VISUAL_NOVEL"] = "VISUAL_NOVEL";
|
|
917
|
+
MediaSource2["VIDEO_GAME"] = "VIDEO_GAME";
|
|
918
|
+
MediaSource2["OTHER"] = "OTHER";
|
|
919
|
+
MediaSource2["NOVEL"] = "NOVEL";
|
|
920
|
+
MediaSource2["DOUJINSHI"] = "DOUJINSHI";
|
|
921
|
+
MediaSource2["ANIME"] = "ANIME";
|
|
922
|
+
MediaSource2["WEB_NOVEL"] = "WEB_NOVEL";
|
|
923
|
+
MediaSource2["LIVE_ACTION"] = "LIVE_ACTION";
|
|
924
|
+
MediaSource2["GAME"] = "GAME";
|
|
925
|
+
MediaSource2["COMIC"] = "COMIC";
|
|
926
|
+
MediaSource2["MULTIMEDIA_PROJECT"] = "MULTIMEDIA_PROJECT";
|
|
927
|
+
MediaSource2["PICTURE_BOOK"] = "PICTURE_BOOK";
|
|
928
|
+
return MediaSource2;
|
|
929
|
+
})(MediaSource || {});
|
|
738
930
|
var MediaFormat = /* @__PURE__ */ ((MediaFormat2) => {
|
|
739
931
|
MediaFormat2["TV"] = "TV";
|
|
740
932
|
MediaFormat2["TV_SHORT"] = "TV_SHORT";
|
|
@@ -867,6 +1059,20 @@ var StaffSort = /* @__PURE__ */ ((StaffSort2) => {
|
|
|
867
1059
|
return StaffSort2;
|
|
868
1060
|
})(StaffSort || {});
|
|
869
1061
|
|
|
1062
|
+
// src/types/user.ts
|
|
1063
|
+
var UserSort = /* @__PURE__ */ ((UserSort2) => {
|
|
1064
|
+
UserSort2["ID"] = "ID";
|
|
1065
|
+
UserSort2["ID_DESC"] = "ID_DESC";
|
|
1066
|
+
UserSort2["USERNAME"] = "USERNAME";
|
|
1067
|
+
UserSort2["USERNAME_DESC"] = "USERNAME_DESC";
|
|
1068
|
+
UserSort2["WATCHED_TIME"] = "WATCHED_TIME";
|
|
1069
|
+
UserSort2["WATCHED_TIME_DESC"] = "WATCHED_TIME_DESC";
|
|
1070
|
+
UserSort2["CHAPTERS_READ"] = "CHAPTERS_READ";
|
|
1071
|
+
UserSort2["CHAPTERS_READ_DESC"] = "CHAPTERS_READ_DESC";
|
|
1072
|
+
UserSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
|
|
1073
|
+
return UserSort2;
|
|
1074
|
+
})(UserSort || {});
|
|
1075
|
+
|
|
870
1076
|
// src/types/lists.ts
|
|
871
1077
|
var MediaListStatus = /* @__PURE__ */ ((MediaListStatus2) => {
|
|
872
1078
|
MediaListStatus2["CURRENT"] = "CURRENT";
|
|
@@ -911,16 +1117,245 @@ var MediaListSort = /* @__PURE__ */ ((MediaListSort2) => {
|
|
|
911
1117
|
return MediaListSort2;
|
|
912
1118
|
})(MediaListSort || {});
|
|
913
1119
|
|
|
914
|
-
// src/
|
|
915
|
-
|
|
916
|
-
|
|
1120
|
+
// src/types/thread.ts
|
|
1121
|
+
var ThreadSort = /* @__PURE__ */ ((ThreadSort2) => {
|
|
1122
|
+
ThreadSort2["ID"] = "ID";
|
|
1123
|
+
ThreadSort2["ID_DESC"] = "ID_DESC";
|
|
1124
|
+
ThreadSort2["TITLE"] = "TITLE";
|
|
1125
|
+
ThreadSort2["TITLE_DESC"] = "TITLE_DESC";
|
|
1126
|
+
ThreadSort2["CREATED_AT"] = "CREATED_AT";
|
|
1127
|
+
ThreadSort2["CREATED_AT_DESC"] = "CREATED_AT_DESC";
|
|
1128
|
+
ThreadSort2["UPDATED_AT"] = "UPDATED_AT";
|
|
1129
|
+
ThreadSort2["UPDATED_AT_DESC"] = "UPDATED_AT_DESC";
|
|
1130
|
+
ThreadSort2["REPLIED_AT"] = "REPLIED_AT";
|
|
1131
|
+
ThreadSort2["REPLIED_AT_DESC"] = "REPLIED_AT_DESC";
|
|
1132
|
+
ThreadSort2["REPLY_COUNT"] = "REPLY_COUNT";
|
|
1133
|
+
ThreadSort2["REPLY_COUNT_DESC"] = "REPLY_COUNT_DESC";
|
|
1134
|
+
ThreadSort2["VIEW_COUNT"] = "VIEW_COUNT";
|
|
1135
|
+
ThreadSort2["VIEW_COUNT_DESC"] = "VIEW_COUNT_DESC";
|
|
1136
|
+
ThreadSort2["IS_STICKY"] = "IS_STICKY";
|
|
1137
|
+
ThreadSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
|
|
1138
|
+
return ThreadSort2;
|
|
1139
|
+
})(ThreadSort || {});
|
|
1140
|
+
|
|
1141
|
+
// src/client/media.ts
|
|
1142
|
+
async function getMedia(client, id, include) {
|
|
1143
|
+
const query = buildMediaByIdQuery(include);
|
|
1144
|
+
const data = await client.request(query, { id });
|
|
1145
|
+
return data.Media;
|
|
917
1146
|
}
|
|
918
|
-
function
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1147
|
+
async function searchMedia(client, options = {}) {
|
|
1148
|
+
const { query: search, page = 1, perPage = 20, genres, tags, genresExclude, tagsExclude, ...filters } = options;
|
|
1149
|
+
return client.pagedRequest(
|
|
1150
|
+
QUERY_MEDIA_SEARCH,
|
|
1151
|
+
{
|
|
1152
|
+
search,
|
|
1153
|
+
...filters,
|
|
1154
|
+
genre_in: genres,
|
|
1155
|
+
tag_in: tags,
|
|
1156
|
+
genre_not_in: genresExclude,
|
|
1157
|
+
tag_not_in: tagsExclude,
|
|
1158
|
+
page,
|
|
1159
|
+
perPage: clampPerPage(perPage)
|
|
1160
|
+
},
|
|
1161
|
+
"media"
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
async function getTrending(client, type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1165
|
+
return client.pagedRequest(QUERY_TRENDING, { type, page, perPage: clampPerPage(perPage) }, "media");
|
|
1166
|
+
}
|
|
1167
|
+
async function getPopular(client, type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1168
|
+
return searchMedia(client, { type, sort: ["POPULARITY_DESC" /* POPULARITY_DESC */], page, perPage });
|
|
1169
|
+
}
|
|
1170
|
+
async function getTopRated(client, type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1171
|
+
return searchMedia(client, { type, sort: ["SCORE_DESC" /* SCORE_DESC */], page, perPage });
|
|
1172
|
+
}
|
|
1173
|
+
async function getAiredEpisodes(client, options = {}) {
|
|
1174
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1175
|
+
return client.pagedRequest(
|
|
1176
|
+
QUERY_AIRING_SCHEDULE,
|
|
1177
|
+
{
|
|
1178
|
+
airingAt_greater: options.airingAtGreater ?? now - 24 * 3600,
|
|
1179
|
+
airingAt_lesser: options.airingAtLesser ?? now,
|
|
1180
|
+
sort: options.sort,
|
|
1181
|
+
page: options.page ?? 1,
|
|
1182
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1183
|
+
},
|
|
1184
|
+
"airingSchedules"
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
async function getAiredChapters(client, options = {}) {
|
|
1188
|
+
return client.pagedRequest(
|
|
1189
|
+
QUERY_RECENT_CHAPTERS,
|
|
1190
|
+
{
|
|
1191
|
+
page: options.page ?? 1,
|
|
1192
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1193
|
+
},
|
|
1194
|
+
"media"
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
async function getPlanning(client, options = {}) {
|
|
1198
|
+
return client.pagedRequest(
|
|
1199
|
+
QUERY_PLANNING,
|
|
1200
|
+
{
|
|
1201
|
+
type: options.type,
|
|
1202
|
+
sort: options.sort ?? ["POPULARITY_DESC" /* POPULARITY_DESC */],
|
|
1203
|
+
page: options.page ?? 1,
|
|
1204
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1205
|
+
},
|
|
1206
|
+
"media"
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
async function getRecommendations(client, mediaId, options = {}) {
|
|
1210
|
+
const data = await client.request(QUERY_RECOMMENDATIONS, {
|
|
1211
|
+
mediaId,
|
|
1212
|
+
page: options.page ?? 1,
|
|
1213
|
+
perPage: clampPerPage(options.perPage ?? 20),
|
|
1214
|
+
sort: options.sort
|
|
1215
|
+
});
|
|
1216
|
+
return {
|
|
1217
|
+
pageInfo: data.Media.recommendations.pageInfo,
|
|
1218
|
+
results: data.Media.recommendations.nodes
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
async function getMediaBySeason(client, options) {
|
|
1222
|
+
return client.pagedRequest(
|
|
1223
|
+
QUERY_MEDIA_BY_SEASON,
|
|
1224
|
+
{
|
|
1225
|
+
season: options.season,
|
|
1226
|
+
seasonYear: options.seasonYear,
|
|
1227
|
+
type: options.type,
|
|
1228
|
+
sort: options.sort,
|
|
1229
|
+
page: options.page ?? 1,
|
|
1230
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1231
|
+
},
|
|
1232
|
+
"media"
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
async function getWeeklySchedule(client, date = /* @__PURE__ */ new Date()) {
|
|
1236
|
+
const schedule = {
|
|
1237
|
+
Monday: [],
|
|
1238
|
+
Tuesday: [],
|
|
1239
|
+
Wednesday: [],
|
|
1240
|
+
Thursday: [],
|
|
1241
|
+
Friday: [],
|
|
1242
|
+
Saturday: [],
|
|
1243
|
+
Sunday: []
|
|
1244
|
+
};
|
|
1245
|
+
const startOfWeek = new Date(date);
|
|
1246
|
+
const day = startOfWeek.getDay();
|
|
1247
|
+
const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1);
|
|
1248
|
+
startOfWeek.setDate(diff);
|
|
1249
|
+
startOfWeek.setHours(0, 0, 0, 0);
|
|
1250
|
+
const endOfWeek = new Date(startOfWeek);
|
|
1251
|
+
endOfWeek.setDate(startOfWeek.getDate() + 6);
|
|
1252
|
+
endOfWeek.setHours(23, 59, 59, 999);
|
|
1253
|
+
const startTimestamp = Math.floor(startOfWeek.getTime() / 1e3);
|
|
1254
|
+
const endTimestamp = Math.floor(endOfWeek.getTime() / 1e3);
|
|
1255
|
+
const iterator = client.paginate(
|
|
1256
|
+
(page) => getAiredEpisodes(client, {
|
|
1257
|
+
airingAtGreater: startTimestamp,
|
|
1258
|
+
airingAtLesser: endTimestamp,
|
|
1259
|
+
page,
|
|
1260
|
+
perPage: 50
|
|
1261
|
+
})
|
|
1262
|
+
);
|
|
1263
|
+
for await (const episode of iterator) {
|
|
1264
|
+
const epDate = new Date(episode.airingAt * 1e3);
|
|
1265
|
+
const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1266
|
+
const dayName = names[epDate.getDay()];
|
|
1267
|
+
schedule[dayName].push(episode);
|
|
1268
|
+
}
|
|
1269
|
+
return schedule;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/client/staff.ts
|
|
1273
|
+
async function getStaff(client, id, include) {
|
|
1274
|
+
if (include?.media) {
|
|
1275
|
+
const perPage = typeof include.media === "object" ? include.media.perPage ?? 25 : 25;
|
|
1276
|
+
const data2 = await client.request(QUERY_STAFF_BY_ID_WITH_MEDIA, { id, perPage });
|
|
1277
|
+
return data2.Staff;
|
|
1278
|
+
}
|
|
1279
|
+
const data = await client.request(QUERY_STAFF_BY_ID, { id });
|
|
1280
|
+
return data.Staff;
|
|
1281
|
+
}
|
|
1282
|
+
async function searchStaff(client, options = {}) {
|
|
1283
|
+
const { query: search, page = 1, perPage = 20, sort } = options;
|
|
1284
|
+
return client.pagedRequest(
|
|
1285
|
+
QUERY_STAFF_SEARCH,
|
|
1286
|
+
{ search, sort, page, perPage: clampPerPage(perPage) },
|
|
1287
|
+
"staff"
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/client/studio.ts
|
|
1292
|
+
async function getStudio(client, id) {
|
|
1293
|
+
const data = await client.request(QUERY_STUDIO_BY_ID, { id });
|
|
1294
|
+
return data.Studio;
|
|
1295
|
+
}
|
|
1296
|
+
async function searchStudios(client, options = {}) {
|
|
1297
|
+
return client.pagedRequest(
|
|
1298
|
+
QUERY_STUDIO_SEARCH,
|
|
1299
|
+
{
|
|
1300
|
+
search: options.query,
|
|
1301
|
+
page: options.page ?? 1,
|
|
1302
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1303
|
+
},
|
|
1304
|
+
"studios"
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/client/thread.ts
|
|
1309
|
+
async function getThread(client, id) {
|
|
1310
|
+
const data = await client.request(QUERY_THREAD_BY_ID, { id });
|
|
1311
|
+
return data.Thread;
|
|
1312
|
+
}
|
|
1313
|
+
async function getRecentThreads(client, options = {}) {
|
|
1314
|
+
const { query: search, page = 1, perPage = 20, sort, mediaId, categoryId } = options;
|
|
1315
|
+
return client.pagedRequest(
|
|
1316
|
+
QUERY_THREAD_SEARCH,
|
|
1317
|
+
{
|
|
1318
|
+
search,
|
|
1319
|
+
mediaCategoryId: mediaId,
|
|
1320
|
+
categoryId,
|
|
1321
|
+
sort: sort ?? ["REPLIED_AT_DESC" /* REPLIED_AT_DESC */],
|
|
1322
|
+
page,
|
|
1323
|
+
perPage: clampPerPage(perPage)
|
|
1324
|
+
},
|
|
1325
|
+
"threads"
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/client/user.ts
|
|
1330
|
+
async function getUser(client, idOrName) {
|
|
1331
|
+
if (typeof idOrName === "number") {
|
|
1332
|
+
const data2 = await client.request(QUERY_USER_BY_ID, { id: idOrName });
|
|
1333
|
+
return data2.User;
|
|
1334
|
+
}
|
|
1335
|
+
const data = await client.request(QUERY_USER_BY_NAME, { name: idOrName });
|
|
1336
|
+
return data.User;
|
|
1337
|
+
}
|
|
1338
|
+
async function searchUsers(client, options = {}) {
|
|
1339
|
+
const { query: search, page = 1, perPage = 20, sort } = options;
|
|
1340
|
+
return client.pagedRequest(QUERY_USER_SEARCH, { search, sort, page, perPage: clampPerPage(perPage) }, "users");
|
|
1341
|
+
}
|
|
1342
|
+
async function getUserMediaList(client, options) {
|
|
1343
|
+
if (!options.userId && !options.userName) {
|
|
1344
|
+
throw new AniListError("getUserMediaList requires either userId or userName", 0, []);
|
|
1345
|
+
}
|
|
1346
|
+
return client.pagedRequest(
|
|
1347
|
+
QUERY_USER_MEDIA_LIST,
|
|
1348
|
+
{
|
|
1349
|
+
userId: options.userId,
|
|
1350
|
+
userName: options.userName,
|
|
1351
|
+
type: options.type,
|
|
1352
|
+
status: options.status,
|
|
1353
|
+
sort: options.sort,
|
|
1354
|
+
page: options.page ?? 1,
|
|
1355
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1356
|
+
},
|
|
1357
|
+
"mediaList"
|
|
1358
|
+
);
|
|
924
1359
|
}
|
|
925
1360
|
|
|
926
1361
|
// src/client/index.ts
|
|
@@ -940,9 +1375,8 @@ var AniListClient = class {
|
|
|
940
1375
|
this.rateLimiter = new RateLimiter(options.rateLimit);
|
|
941
1376
|
this.hooks = options.hooks ?? {};
|
|
942
1377
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
*/
|
|
1378
|
+
// ── Core infrastructure (internal) ──
|
|
1379
|
+
/** @internal */
|
|
946
1380
|
async request(query, variables = {}) {
|
|
947
1381
|
const cacheKey = MemoryCache.key(query, variables);
|
|
948
1382
|
const cached = await this.cacheAdapter.get(cacheKey);
|
|
@@ -965,7 +1399,7 @@ var AniListClient = class {
|
|
|
965
1399
|
async executeRequest(query, variables, cacheKey) {
|
|
966
1400
|
const start = Date.now();
|
|
967
1401
|
this.hooks.onRequest?.(query, variables);
|
|
968
|
-
const minifiedQuery = query
|
|
1402
|
+
const minifiedQuery = normalizeQuery(query);
|
|
969
1403
|
const res = await this.rateLimiter.fetchWithRetry(
|
|
970
1404
|
this.apiUrl,
|
|
971
1405
|
{
|
|
@@ -985,10 +1419,7 @@ var AniListClient = class {
|
|
|
985
1419
|
this.hooks.onResponse?.(query, Date.now() - start, false);
|
|
986
1420
|
return data;
|
|
987
1421
|
}
|
|
988
|
-
/**
|
|
989
|
-
* @internal
|
|
990
|
-
* Shorthand for paginated queries that follow the `Page { pageInfo, <field>[] }` pattern.
|
|
991
|
-
*/
|
|
1422
|
+
/** @internal */
|
|
992
1423
|
async pagedRequest(query, variables, field) {
|
|
993
1424
|
const data = await this.request(query, variables);
|
|
994
1425
|
const results = data.Page[field];
|
|
@@ -997,6 +1428,7 @@ var AniListClient = class {
|
|
|
997
1428
|
}
|
|
998
1429
|
return { pageInfo: data.Page.pageInfo, results };
|
|
999
1430
|
}
|
|
1431
|
+
// ── Media ──
|
|
1000
1432
|
/**
|
|
1001
1433
|
* Fetch a single media entry by its AniList ID.
|
|
1002
1434
|
*
|
|
@@ -1004,467 +1436,130 @@ var AniListClient = class {
|
|
|
1004
1436
|
*
|
|
1005
1437
|
* @param id - The AniList media ID
|
|
1006
1438
|
* @param include - Optional related data to include
|
|
1007
|
-
* @returns The media object
|
|
1008
|
-
*
|
|
1009
|
-
* @example
|
|
1010
|
-
* ```ts
|
|
1011
|
-
* // Basic usage — same as before (includes relations by default)
|
|
1012
|
-
* const anime = await client.getMedia(1);
|
|
1013
|
-
*
|
|
1014
|
-
* // Include characters sorted by role, 25 results
|
|
1015
|
-
* const anime = await client.getMedia(1, { characters: true });
|
|
1016
|
-
*
|
|
1017
|
-
* // Include characters with voice actors
|
|
1018
|
-
* const anime = await client.getMedia(1, { characters: { voiceActors: true } });
|
|
1019
|
-
*
|
|
1020
|
-
* // Full control
|
|
1021
|
-
* const anime = await client.getMedia(1, {
|
|
1022
|
-
* characters: { perPage: 50, sort: true },
|
|
1023
|
-
* staff: true,
|
|
1024
|
-
* relations: true,
|
|
1025
|
-
* streamingEpisodes: true,
|
|
1026
|
-
* externalLinks: true,
|
|
1027
|
-
* stats: true,
|
|
1028
|
-
* recommendations: { perPage: 5 },
|
|
1029
|
-
* });
|
|
1030
|
-
*
|
|
1031
|
-
* // Exclude relations for a lighter response
|
|
1032
|
-
* const anime = await client.getMedia(1, { characters: true, relations: false });
|
|
1033
|
-
* ```
|
|
1034
1439
|
*/
|
|
1035
1440
|
async getMedia(id, include) {
|
|
1036
|
-
|
|
1037
|
-
const data = await this.request(query, { id });
|
|
1038
|
-
return data.Media;
|
|
1441
|
+
return getMedia(this, id, include);
|
|
1039
1442
|
}
|
|
1040
1443
|
/**
|
|
1041
1444
|
* Search for anime or manga.
|
|
1042
1445
|
*
|
|
1043
1446
|
* @param options - Search / filter parameters
|
|
1044
1447
|
* @returns Paginated results with matching media
|
|
1045
|
-
*
|
|
1046
|
-
* @example
|
|
1047
|
-
* ```ts
|
|
1048
|
-
* const results = await client.searchMedia({
|
|
1049
|
-
* query: "Naruto",
|
|
1050
|
-
* type: MediaType.ANIME,
|
|
1051
|
-
* perPage: 5,
|
|
1052
|
-
* });
|
|
1053
|
-
* ```
|
|
1054
1448
|
*/
|
|
1055
1449
|
async searchMedia(options = {}) {
|
|
1056
|
-
|
|
1057
|
-
return this.pagedRequest(
|
|
1058
|
-
QUERY_MEDIA_SEARCH,
|
|
1059
|
-
{ search, ...filters, page, perPage: clampPerPage(perPage) },
|
|
1060
|
-
"media"
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
/**
|
|
1064
|
-
* Get currently trending anime or manga.
|
|
1065
|
-
*
|
|
1066
|
-
* @param type - `MediaType.ANIME` or `MediaType.MANGA` (defaults to ANIME)
|
|
1067
|
-
* @param page - Page number (default 1)
|
|
1068
|
-
* @param perPage - Results per page (default 20, max 50)
|
|
1069
|
-
*/
|
|
1070
|
-
async getTrending(type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1071
|
-
return this.pagedRequest(QUERY_TRENDING, { type, page, perPage: clampPerPage(perPage) }, "media");
|
|
1072
|
-
}
|
|
1073
|
-
/**
|
|
1074
|
-
* Fetch a character by AniList ID.
|
|
1075
|
-
*
|
|
1076
|
-
* @param id - The AniList character ID
|
|
1077
|
-
* @param include - Optional include options (e.g. voice actors)
|
|
1078
|
-
* @returns The character object
|
|
1079
|
-
*
|
|
1080
|
-
* @example
|
|
1081
|
-
* ```ts
|
|
1082
|
-
* const spike = await client.getCharacter(1);
|
|
1083
|
-
* console.log(spike.name.full); // "Spike Spiegel"
|
|
1084
|
-
*
|
|
1085
|
-
* // With voice actors
|
|
1086
|
-
* const spike = await client.getCharacter(1, { voiceActors: true });
|
|
1087
|
-
* spike.media?.edges?.forEach((e) => {
|
|
1088
|
-
* console.log(e.node.title.romaji);
|
|
1089
|
-
* e.voiceActors?.forEach((va) => console.log(` VA: ${va.name.full}`));
|
|
1090
|
-
* });
|
|
1091
|
-
* ```
|
|
1092
|
-
*/
|
|
1093
|
-
async getCharacter(id, include) {
|
|
1094
|
-
const query = include?.voiceActors ? QUERY_CHARACTER_BY_ID_WITH_VA : QUERY_CHARACTER_BY_ID;
|
|
1095
|
-
const data = await this.request(query, { id });
|
|
1096
|
-
return data.Character;
|
|
1097
|
-
}
|
|
1098
|
-
/**
|
|
1099
|
-
* Search for characters by name.
|
|
1100
|
-
*
|
|
1101
|
-
* @param options - Search / pagination parameters (includes optional `voiceActors`)
|
|
1102
|
-
* @returns Paginated results with matching characters
|
|
1103
|
-
*
|
|
1104
|
-
* @example
|
|
1105
|
-
* ```ts
|
|
1106
|
-
* const result = await client.searchCharacters({ query: "Luffy", perPage: 5 });
|
|
1107
|
-
*
|
|
1108
|
-
* // With voice actors
|
|
1109
|
-
* const result = await client.searchCharacters({ query: "Luffy", voiceActors: true });
|
|
1110
|
-
* ```
|
|
1111
|
-
*/
|
|
1112
|
-
async searchCharacters(options = {}) {
|
|
1113
|
-
const { query: search, page = 1, perPage = 20, voiceActors, ...rest } = options;
|
|
1114
|
-
const gqlQuery = voiceActors ? QUERY_CHARACTER_SEARCH_WITH_VA : QUERY_CHARACTER_SEARCH;
|
|
1115
|
-
return this.pagedRequest(
|
|
1116
|
-
gqlQuery,
|
|
1117
|
-
{ search, ...rest, page, perPage: clampPerPage(perPage) },
|
|
1118
|
-
"characters"
|
|
1119
|
-
);
|
|
1120
|
-
}
|
|
1121
|
-
/**
|
|
1122
|
-
* Fetch a staff member by AniList ID.
|
|
1123
|
-
*
|
|
1124
|
-
* @param id - The AniList staff ID
|
|
1125
|
-
* @param include - Optional include options to fetch related data (e.g. media)
|
|
1126
|
-
* @returns The staff object
|
|
1127
|
-
*
|
|
1128
|
-
* @example
|
|
1129
|
-
* ```ts
|
|
1130
|
-
* const staff = await client.getStaff(95001);
|
|
1131
|
-
* console.log(staff.name.full);
|
|
1132
|
-
*
|
|
1133
|
-
* // With media the staff worked on
|
|
1134
|
-
* const staff = await client.getStaff(95001, { media: true });
|
|
1135
|
-
* staff.staffMedia?.nodes.forEach((m) => console.log(m.title.romaji));
|
|
1136
|
-
* ```
|
|
1137
|
-
*/
|
|
1138
|
-
async getStaff(id, include) {
|
|
1139
|
-
if (include?.media) {
|
|
1140
|
-
const opts = typeof include.media === "object" ? include.media : {};
|
|
1141
|
-
const perPage = opts.perPage ?? 25;
|
|
1142
|
-
const data2 = await this.request(QUERY_STAFF_BY_ID_WITH_MEDIA, { id, perPage });
|
|
1143
|
-
return data2.Staff;
|
|
1144
|
-
}
|
|
1145
|
-
const data = await this.request(QUERY_STAFF_BY_ID, { id });
|
|
1146
|
-
return data.Staff;
|
|
1147
|
-
}
|
|
1148
|
-
/**
|
|
1149
|
-
* Search for staff (voice actors, directors, etc.).
|
|
1150
|
-
*
|
|
1151
|
-
* @param options - Search / pagination parameters
|
|
1152
|
-
* @returns Paginated results with matching staff
|
|
1153
|
-
*
|
|
1154
|
-
* @example
|
|
1155
|
-
* ```ts
|
|
1156
|
-
* const result = await client.searchStaff({ query: "Miyazaki", perPage: 5 });
|
|
1157
|
-
* ```
|
|
1158
|
-
*/
|
|
1159
|
-
async searchStaff(options = {}) {
|
|
1160
|
-
const { query: search, page = 1, perPage = 20, sort } = options;
|
|
1161
|
-
return this.pagedRequest(
|
|
1162
|
-
QUERY_STAFF_SEARCH,
|
|
1163
|
-
{ search, sort, page, perPage: clampPerPage(perPage) },
|
|
1164
|
-
"staff"
|
|
1165
|
-
);
|
|
1450
|
+
return searchMedia(this, options);
|
|
1166
1451
|
}
|
|
1167
|
-
/**
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
* @param id - The AniList user ID
|
|
1171
|
-
* @returns The user object
|
|
1172
|
-
*
|
|
1173
|
-
* @example
|
|
1174
|
-
* ```ts
|
|
1175
|
-
* const user = await client.getUser(1);
|
|
1176
|
-
* console.log(user.name);
|
|
1177
|
-
* ```
|
|
1178
|
-
*/
|
|
1179
|
-
async getUser(id) {
|
|
1180
|
-
const data = await this.request(QUERY_USER_BY_ID, { id });
|
|
1181
|
-
return data.User;
|
|
1452
|
+
/** Get currently trending anime or manga. */
|
|
1453
|
+
async getTrending(type, page, perPage) {
|
|
1454
|
+
return getTrending(this, type, page, perPage);
|
|
1182
1455
|
}
|
|
1183
|
-
/**
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
* @param name - The AniList username
|
|
1187
|
-
* @returns The user object
|
|
1188
|
-
*
|
|
1189
|
-
* @example
|
|
1190
|
-
* ```ts
|
|
1191
|
-
* const user = await client.getUserByName("AniList");
|
|
1192
|
-
* console.log(user.statistics);
|
|
1193
|
-
* ```
|
|
1194
|
-
*/
|
|
1195
|
-
async getUserByName(name) {
|
|
1196
|
-
const data = await this.request(QUERY_USER_BY_NAME, { name });
|
|
1197
|
-
return data.User;
|
|
1456
|
+
/** Get the most popular anime or manga. */
|
|
1457
|
+
async getPopular(type, page, perPage) {
|
|
1458
|
+
return getPopular(this, type, page, perPage);
|
|
1198
1459
|
}
|
|
1199
|
-
/**
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
*
|
|
1203
|
-
* @param query - A valid GraphQL query string
|
|
1204
|
-
* @param variables - Optional variables object
|
|
1205
|
-
*/
|
|
1206
|
-
async raw(query, variables) {
|
|
1207
|
-
return this.request(query, variables);
|
|
1460
|
+
/** Get the highest-rated anime or manga. */
|
|
1461
|
+
async getTopRated(type, page, perPage) {
|
|
1462
|
+
return getTopRated(this, type, page, perPage);
|
|
1208
1463
|
}
|
|
1209
|
-
/**
|
|
1210
|
-
* Get recently aired anime episodes.
|
|
1211
|
-
*
|
|
1212
|
-
* By default returns episodes that aired in the last 24 hours.
|
|
1213
|
-
*
|
|
1214
|
-
* @param options - Filter / pagination parameters
|
|
1215
|
-
* @returns Paginated list of airing schedule entries
|
|
1216
|
-
*
|
|
1217
|
-
* @example
|
|
1218
|
-
* ```ts
|
|
1219
|
-
* // Episodes that aired in the last 48h
|
|
1220
|
-
* const recent = await client.getAiredEpisodes({
|
|
1221
|
-
* airingAtGreater: Math.floor(Date.now() / 1000) - 48 * 3600,
|
|
1222
|
-
* });
|
|
1223
|
-
* ```
|
|
1224
|
-
*/
|
|
1464
|
+
/** Get recently aired anime episodes. */
|
|
1225
1465
|
async getAiredEpisodes(options = {}) {
|
|
1226
|
-
|
|
1227
|
-
const variables = {
|
|
1228
|
-
airingAt_greater: options.airingAtGreater ?? now - 24 * 3600,
|
|
1229
|
-
airingAt_lesser: options.airingAtLesser ?? now,
|
|
1230
|
-
sort: options.sort ?? ["TIME_DESC"],
|
|
1231
|
-
page: options.page ?? 1,
|
|
1232
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1233
|
-
};
|
|
1234
|
-
return this.pagedRequest(QUERY_AIRING_SCHEDULE, variables, "airingSchedules");
|
|
1466
|
+
return getAiredEpisodes(this, options);
|
|
1235
1467
|
}
|
|
1236
|
-
/**
|
|
1237
|
-
* Get manga that are currently releasing, sorted by most recently updated.
|
|
1238
|
-
*
|
|
1239
|
-
* This is the closest equivalent to "recently released chapters" on AniList,
|
|
1240
|
-
* since the API does not expose per-chapter airing schedules for manga.
|
|
1241
|
-
*
|
|
1242
|
-
* @param options - Pagination parameters
|
|
1243
|
-
* @returns Paginated list of currently releasing manga
|
|
1244
|
-
*
|
|
1245
|
-
* @example
|
|
1246
|
-
* ```ts
|
|
1247
|
-
* const chapters = await client.getAiredChapters({ perPage: 10 });
|
|
1248
|
-
* ```
|
|
1249
|
-
*/
|
|
1468
|
+
/** Get currently releasing manga. */
|
|
1250
1469
|
async getAiredChapters(options = {}) {
|
|
1251
|
-
return this
|
|
1252
|
-
QUERY_RECENT_CHAPTERS,
|
|
1253
|
-
{
|
|
1254
|
-
page: options.page ?? 1,
|
|
1255
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1256
|
-
},
|
|
1257
|
-
"media"
|
|
1258
|
-
);
|
|
1470
|
+
return getAiredChapters(this, options);
|
|
1259
1471
|
}
|
|
1260
|
-
/**
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
*
|
|
1266
|
-
* @example
|
|
1267
|
-
* ```ts
|
|
1268
|
-
* import { MediaType } from "ani-client";
|
|
1269
|
-
*
|
|
1270
|
-
* // Most anticipated upcoming anime
|
|
1271
|
-
* const planning = await client.getPlanning({ type: MediaType.ANIME, perPage: 10 });
|
|
1272
|
-
* ```
|
|
1273
|
-
*/
|
|
1472
|
+
/** Get the detailed schedule for the current week, sorted by day. */
|
|
1473
|
+
async getWeeklySchedule(date) {
|
|
1474
|
+
return getWeeklySchedule(this, date);
|
|
1475
|
+
}
|
|
1476
|
+
/** Get upcoming (not yet released) media. */
|
|
1274
1477
|
async getPlanning(options = {}) {
|
|
1275
|
-
return this
|
|
1276
|
-
QUERY_PLANNING,
|
|
1277
|
-
{
|
|
1278
|
-
type: options.type,
|
|
1279
|
-
sort: options.sort ?? ["POPULARITY_DESC"],
|
|
1280
|
-
page: options.page ?? 1,
|
|
1281
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1282
|
-
},
|
|
1283
|
-
"media"
|
|
1284
|
-
);
|
|
1478
|
+
return getPlanning(this, options);
|
|
1285
1479
|
}
|
|
1286
|
-
/**
|
|
1287
|
-
* Get recommendations for a specific media.
|
|
1288
|
-
*
|
|
1289
|
-
* Returns other anime/manga that users have recommended based on the given media.
|
|
1290
|
-
*
|
|
1291
|
-
* @param mediaId - The AniList media ID
|
|
1292
|
-
* @param options - Optional sort / pagination parameters
|
|
1293
|
-
* @returns Paginated list of recommendations
|
|
1294
|
-
*
|
|
1295
|
-
* @example
|
|
1296
|
-
* ```ts
|
|
1297
|
-
* // Get recommendations for Cowboy Bebop
|
|
1298
|
-
* const recs = await client.getRecommendations(1);
|
|
1299
|
-
* recs.results.forEach((r) =>
|
|
1300
|
-
* console.log(`${r.mediaRecommendation.title.romaji} (rating: ${r.rating})`)
|
|
1301
|
-
* );
|
|
1302
|
-
* ```
|
|
1303
|
-
*/
|
|
1480
|
+
/** Get recommendations for a specific media. */
|
|
1304
1481
|
async getRecommendations(mediaId, options = {}) {
|
|
1305
|
-
|
|
1306
|
-
mediaId,
|
|
1307
|
-
sort: options.sort ?? ["RATING_DESC"],
|
|
1308
|
-
page: options.page ?? 1,
|
|
1309
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1310
|
-
};
|
|
1311
|
-
const data = await this.request(QUERY_RECOMMENDATIONS, variables);
|
|
1312
|
-
return {
|
|
1313
|
-
pageInfo: data.Media.recommendations.pageInfo,
|
|
1314
|
-
results: data.Media.recommendations.nodes
|
|
1315
|
-
};
|
|
1482
|
+
return getRecommendations(this, mediaId, options);
|
|
1316
1483
|
}
|
|
1317
|
-
/**
|
|
1318
|
-
* Get anime (or manga) for a specific season and year.
|
|
1319
|
-
*
|
|
1320
|
-
* @param options - Season, year and optional filter / pagination parameters
|
|
1321
|
-
* @returns Paginated list of media for the given season
|
|
1322
|
-
*
|
|
1323
|
-
* @example
|
|
1324
|
-
* ```ts
|
|
1325
|
-
* import { MediaSeason } from "ani-client";
|
|
1326
|
-
*
|
|
1327
|
-
* const winter2026 = await client.getMediaBySeason({
|
|
1328
|
-
* season: MediaSeason.WINTER,
|
|
1329
|
-
* seasonYear: 2026,
|
|
1330
|
-
* perPage: 10,
|
|
1331
|
-
* });
|
|
1332
|
-
* ```
|
|
1333
|
-
*/
|
|
1484
|
+
/** Get anime (or manga) for a specific season and year. */
|
|
1334
1485
|
async getMediaBySeason(options) {
|
|
1335
|
-
return this
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1486
|
+
return getMediaBySeason(this, options);
|
|
1487
|
+
}
|
|
1488
|
+
// ── Characters ──
|
|
1489
|
+
/** Fetch a character by AniList ID. Pass `{ voiceActors: true }` to include VA data. */
|
|
1490
|
+
async getCharacter(id, include) {
|
|
1491
|
+
return getCharacter(this, id, include);
|
|
1492
|
+
}
|
|
1493
|
+
/** Search for characters by name. */
|
|
1494
|
+
async searchCharacters(options = {}) {
|
|
1495
|
+
return searchCharacters(this, options);
|
|
1496
|
+
}
|
|
1497
|
+
// ── Staff ──
|
|
1498
|
+
/** Fetch a staff member by AniList ID. Pass `{ media: true }` or `{ media: { perPage } }` for media credits. */
|
|
1499
|
+
async getStaff(id, include) {
|
|
1500
|
+
return getStaff(this, id, include);
|
|
1501
|
+
}
|
|
1502
|
+
/** Search for staff (voice actors, directors, etc.). */
|
|
1503
|
+
async searchStaff(options = {}) {
|
|
1504
|
+
return searchStaff(this, options);
|
|
1347
1505
|
}
|
|
1506
|
+
// ── Users ──
|
|
1348
1507
|
/**
|
|
1349
|
-
*
|
|
1350
|
-
*
|
|
1351
|
-
* Provide either `userId` or `userName` to identify the user.
|
|
1352
|
-
* Requires `type` (ANIME or MANGA). Optionally filter by list status.
|
|
1353
|
-
*
|
|
1354
|
-
* @param options - User identifier, media type, and optional filters
|
|
1355
|
-
* @returns Paginated list of media list entries
|
|
1356
|
-
*
|
|
1357
|
-
* @example
|
|
1358
|
-
* ```ts
|
|
1359
|
-
* import { MediaType, MediaListStatus } from "ani-client";
|
|
1508
|
+
* Fetch a user by AniList ID or username.
|
|
1360
1509
|
*
|
|
1361
|
-
*
|
|
1362
|
-
* const list = await client.getUserMediaList({
|
|
1363
|
-
* userName: "AniList",
|
|
1364
|
-
* type: MediaType.ANIME,
|
|
1365
|
-
* status: MediaListStatus.COMPLETED,
|
|
1366
|
-
* });
|
|
1367
|
-
* list.results.forEach((entry) =>
|
|
1368
|
-
* console.log(`${entry.media.title.romaji} — ${entry.score}/100`)
|
|
1369
|
-
* );
|
|
1370
|
-
* ```
|
|
1510
|
+
* @param idOrName - The AniList user ID (number) or username (string)
|
|
1371
1511
|
*/
|
|
1512
|
+
async getUser(idOrName) {
|
|
1513
|
+
return getUser(this, idOrName);
|
|
1514
|
+
}
|
|
1515
|
+
/** Search for users by name. */
|
|
1516
|
+
async searchUsers(options = {}) {
|
|
1517
|
+
return searchUsers(this, options);
|
|
1518
|
+
}
|
|
1519
|
+
/** Get a user's anime or manga list. */
|
|
1372
1520
|
async getUserMediaList(options) {
|
|
1373
|
-
|
|
1374
|
-
throw new Error("Either userId or userName must be provided");
|
|
1375
|
-
}
|
|
1376
|
-
return this.pagedRequest(
|
|
1377
|
-
QUERY_USER_MEDIA_LIST,
|
|
1378
|
-
{
|
|
1379
|
-
userId: options.userId,
|
|
1380
|
-
userName: options.userName,
|
|
1381
|
-
type: options.type,
|
|
1382
|
-
status: options.status,
|
|
1383
|
-
sort: options.sort,
|
|
1384
|
-
page: options.page ?? 1,
|
|
1385
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1386
|
-
},
|
|
1387
|
-
"mediaList"
|
|
1388
|
-
);
|
|
1521
|
+
return getUserMediaList(this, options);
|
|
1389
1522
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
*
|
|
1393
|
-
* Returns studio details along with its most popular productions.
|
|
1394
|
-
*
|
|
1395
|
-
* @param id - The AniList studio ID
|
|
1396
|
-
*/
|
|
1523
|
+
// ── Studios ──
|
|
1524
|
+
/** Fetch a studio by its AniList ID. */
|
|
1397
1525
|
async getStudio(id) {
|
|
1398
|
-
|
|
1399
|
-
return data.Studio;
|
|
1526
|
+
return getStudio(this, id);
|
|
1400
1527
|
}
|
|
1401
|
-
/**
|
|
1402
|
-
* Search for studios by name.
|
|
1403
|
-
*
|
|
1404
|
-
* @param options - Search / pagination parameters
|
|
1405
|
-
* @returns Paginated list of studios
|
|
1406
|
-
*
|
|
1407
|
-
* @example
|
|
1408
|
-
* ```ts
|
|
1409
|
-
* const studios = await client.searchStudios({ query: "MAPPA" });
|
|
1410
|
-
* ```
|
|
1411
|
-
*/
|
|
1528
|
+
/** Search for studios by name. */
|
|
1412
1529
|
async searchStudios(options = {}) {
|
|
1413
|
-
return this
|
|
1414
|
-
QUERY_STUDIO_SEARCH,
|
|
1415
|
-
{
|
|
1416
|
-
search: options.query,
|
|
1417
|
-
page: options.page ?? 1,
|
|
1418
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1419
|
-
},
|
|
1420
|
-
"studios"
|
|
1421
|
-
);
|
|
1530
|
+
return searchStudios(this, options);
|
|
1422
1531
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1532
|
+
// ── Metadata ──
|
|
1533
|
+
// ── Threads ──
|
|
1534
|
+
/** Fetch a forum thread by its AniList ID. */
|
|
1535
|
+
async getThread(id) {
|
|
1536
|
+
return getThread(this, id);
|
|
1537
|
+
}
|
|
1538
|
+
/** Get recent forum threads, optionally filtered by search, media, or category. */
|
|
1539
|
+
async getRecentThreads(options = {}) {
|
|
1540
|
+
return getRecentThreads(this, options);
|
|
1541
|
+
}
|
|
1542
|
+
/** Get all available genres on AniList. */
|
|
1428
1543
|
async getGenres() {
|
|
1429
1544
|
const data = await this.request(QUERY_GENRES);
|
|
1430
1545
|
return data.GenreCollection;
|
|
1431
1546
|
}
|
|
1432
|
-
/**
|
|
1433
|
-
* Get all available media tags on AniList.
|
|
1434
|
-
*
|
|
1435
|
-
* @returns Array of tag objects with id, name, description, category, isAdult
|
|
1436
|
-
*/
|
|
1547
|
+
/** Get all available media tags on AniList. */
|
|
1437
1548
|
async getTags() {
|
|
1438
1549
|
const data = await this.request(QUERY_TAGS);
|
|
1439
1550
|
return data.MediaTagCollection;
|
|
1440
1551
|
}
|
|
1552
|
+
// ── Raw query ──
|
|
1553
|
+
/** Execute an arbitrary GraphQL query against the AniList API. */
|
|
1554
|
+
async raw(query, variables) {
|
|
1555
|
+
return this.request(query, variables ?? {});
|
|
1556
|
+
}
|
|
1557
|
+
// ── Pagination ──
|
|
1441
1558
|
/**
|
|
1442
|
-
* Auto-paginating async iterator.
|
|
1443
|
-
*
|
|
1444
|
-
* Wraps any paginated method and yields individual items across all pages.
|
|
1445
|
-
* Stops when `hasNextPage` is `false` or `maxPages` is reached.
|
|
1559
|
+
* Auto-paginating async iterator. Yields individual items across all pages.
|
|
1446
1560
|
*
|
|
1447
1561
|
* @param fetchPage - A function that takes a page number and returns a `PagedResult<T>`
|
|
1448
1562
|
* @param maxPages - Maximum number of pages to fetch (default: Infinity)
|
|
1449
|
-
* @returns An async iterable iterator of individual items
|
|
1450
|
-
*
|
|
1451
|
-
* @example
|
|
1452
|
-
* ```ts
|
|
1453
|
-
* // Iterate over all search results
|
|
1454
|
-
* for await (const anime of client.paginate((page) =>
|
|
1455
|
-
* client.searchMedia({ query: "Naruto", page, perPage: 10 })
|
|
1456
|
-
* )) {
|
|
1457
|
-
* console.log(anime.title.romaji);
|
|
1458
|
-
* }
|
|
1459
|
-
*
|
|
1460
|
-
* // Limit to 3 pages
|
|
1461
|
-
* for await (const anime of client.paginate(
|
|
1462
|
-
* (page) => client.getTrending(MediaType.ANIME, page, 20),
|
|
1463
|
-
* 3,
|
|
1464
|
-
* )) {
|
|
1465
|
-
* console.log(anime.title.romaji);
|
|
1466
|
-
* }
|
|
1467
|
-
* ```
|
|
1468
1563
|
*/
|
|
1469
1564
|
async *paginate(fetchPage, maxPages = Number.POSITIVE_INFINITY) {
|
|
1470
1565
|
let page = 1;
|
|
@@ -1479,35 +1574,19 @@ var AniListClient = class {
|
|
|
1479
1574
|
}
|
|
1480
1575
|
}
|
|
1481
1576
|
// ── Batch queries ──
|
|
1482
|
-
/**
|
|
1483
|
-
* Fetch multiple media entries in a single API request.
|
|
1484
|
-
* Uses GraphQL aliases to batch up to 50 IDs per call.
|
|
1485
|
-
*
|
|
1486
|
-
* @param ids - Array of AniList media IDs
|
|
1487
|
-
* @returns Array of media objects (same order as input IDs)
|
|
1488
|
-
*/
|
|
1577
|
+
/** Fetch multiple media entries in a single API request. */
|
|
1489
1578
|
async getMediaBatch(ids) {
|
|
1490
1579
|
if (ids.length === 0) return [];
|
|
1491
1580
|
if (ids.length === 1) return [await this.getMedia(ids[0])];
|
|
1492
1581
|
return this.executeBatch(ids, buildBatchMediaQuery, "m");
|
|
1493
1582
|
}
|
|
1494
|
-
/**
|
|
1495
|
-
* Fetch multiple characters in a single API request.
|
|
1496
|
-
*
|
|
1497
|
-
* @param ids - Array of AniList character IDs
|
|
1498
|
-
* @returns Array of character objects (same order as input IDs)
|
|
1499
|
-
*/
|
|
1583
|
+
/** Fetch multiple characters in a single API request. */
|
|
1500
1584
|
async getCharacterBatch(ids) {
|
|
1501
1585
|
if (ids.length === 0) return [];
|
|
1502
1586
|
if (ids.length === 1) return [await this.getCharacter(ids[0])];
|
|
1503
1587
|
return this.executeBatch(ids, buildBatchCharacterQuery, "c");
|
|
1504
1588
|
}
|
|
1505
|
-
/**
|
|
1506
|
-
* Fetch multiple staff members in a single API request.
|
|
1507
|
-
*
|
|
1508
|
-
* @param ids - Array of AniList staff IDs
|
|
1509
|
-
* @returns Array of staff objects (same order as input IDs)
|
|
1510
|
-
*/
|
|
1589
|
+
/** Fetch multiple staff members in a single API request. */
|
|
1511
1590
|
async getStaffBatch(ids) {
|
|
1512
1591
|
if (ids.length === 0) return [];
|
|
1513
1592
|
if (ids.length === 1) return [await this.getStaff(ids[0])];
|
|
@@ -1525,25 +1604,15 @@ var AniListClient = class {
|
|
|
1525
1604
|
return chunkResults.flat();
|
|
1526
1605
|
}
|
|
1527
1606
|
// ── Cache management ──
|
|
1528
|
-
/**
|
|
1529
|
-
* Clear the entire response cache.
|
|
1530
|
-
*/
|
|
1607
|
+
/** Clear the entire response cache. */
|
|
1531
1608
|
async clearCache() {
|
|
1532
1609
|
await this.cacheAdapter.clear();
|
|
1533
1610
|
}
|
|
1534
|
-
/**
|
|
1535
|
-
* Number of entries currently in the cache.
|
|
1536
|
-
* For async adapters like Redis, this may return a Promise.
|
|
1537
|
-
*/
|
|
1611
|
+
/** Number of entries currently in the cache. */
|
|
1538
1612
|
get cacheSize() {
|
|
1539
1613
|
return this.cacheAdapter.size;
|
|
1540
1614
|
}
|
|
1541
|
-
/**
|
|
1542
|
-
* Remove cache entries whose key matches the given pattern.
|
|
1543
|
-
*
|
|
1544
|
-
* @param pattern — A string (converted to RegExp) or RegExp
|
|
1545
|
-
* @returns Number of entries removed
|
|
1546
|
-
*/
|
|
1615
|
+
/** Remove cache entries whose key matches the given pattern. */
|
|
1547
1616
|
async invalidateCache(pattern) {
|
|
1548
1617
|
if (this.cacheAdapter.invalidate) {
|
|
1549
1618
|
return this.cacheAdapter.invalidate(pattern);
|
|
@@ -1559,6 +1628,11 @@ var AniListClient = class {
|
|
|
1559
1628
|
}
|
|
1560
1629
|
return count;
|
|
1561
1630
|
}
|
|
1631
|
+
/** Clean up resources held by the client. */
|
|
1632
|
+
async destroy() {
|
|
1633
|
+
await this.cacheAdapter.clear();
|
|
1634
|
+
this.inFlight.clear();
|
|
1635
|
+
}
|
|
1562
1636
|
};
|
|
1563
1637
|
|
|
1564
1638
|
// src/cache/redis.ts
|
|
@@ -1644,6 +1718,6 @@ var RedisCache = class {
|
|
|
1644
1718
|
}
|
|
1645
1719
|
};
|
|
1646
1720
|
|
|
1647
|
-
export { AiringSort, AniListClient, AniListError, CharacterRole, CharacterSort, MediaFormat, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaStatus, MediaType, MemoryCache, RateLimiter, RecommendationSort, RedisCache, StaffSort };
|
|
1721
|
+
export { AiringSort, AniListClient, AniListError, CharacterRole, CharacterSort, MediaFormat, MediaListSort, MediaListStatus, MediaRelationType, MediaSeason, MediaSort, MediaSource, MediaStatus, MediaType, MemoryCache, RateLimiter, RecommendationSort, RedisCache, StaffSort, ThreadSort, UserSort, parseAniListMarkdown };
|
|
1648
1722
|
//# sourceMappingURL=index.mjs.map
|
|
1649
1723
|
//# sourceMappingURL=index.mjs.map
|