ani-client 1.5.0 → 1.6.0
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 +42 -4
- package/dist/index.d.mts +248 -366
- package/dist/index.d.ts +248 -366
- package/dist/index.js +814 -709
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +813 -710
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,6 +1,159 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// src/
|
|
3
|
+
// src/utils/markdown.ts
|
|
4
|
+
function parseAniListMarkdown(text) {
|
|
5
|
+
if (!text) return "";
|
|
6
|
+
let html = text;
|
|
7
|
+
html = html.replace(/~!(.*?)!~/gs, '<span class="anilist-spoiler">$1</span>');
|
|
8
|
+
html = html.replace(/~~~(.*?)~~~/gs, '<div class="anilist-center">$1</div>');
|
|
9
|
+
html = html.replace(/img(\d+)\((.*?)\)/gi, '<img src="$2" width="$1" alt="" class="anilist-image" />');
|
|
10
|
+
html = html.replace(/img\((.*?)\)/gi, '<img src="$1" alt="" class="anilist-image" />');
|
|
11
|
+
html = html.replace(
|
|
12
|
+
/youtube\((.*?)\)/gi,
|
|
13
|
+
'<iframe src="https://www.youtube.com/embed/$1" frameborder="0" allowfullscreen class="anilist-youtube"></iframe>'
|
|
14
|
+
);
|
|
15
|
+
html = html.replace(/webm\((.*?)\)/gi, '<video src="$1" controls class="anilist-webm"></video>');
|
|
16
|
+
html = html.replace(/__(.*?)__/g, "<strong>$1</strong>");
|
|
17
|
+
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
|
18
|
+
html = html.replace(/_(.*?)_/g, "<em>$1</em>");
|
|
19
|
+
html = html.replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/g, "<em>$1</em>");
|
|
20
|
+
html = html.replace(/~~(.*?)~~/g, "<del>$1</del>");
|
|
21
|
+
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
22
|
+
html = html.replace(/\r\n/g, "\n");
|
|
23
|
+
const paragraphs = html.split(/\n{2,}/);
|
|
24
|
+
html = paragraphs.map((p) => {
|
|
25
|
+
const withBr = p.replace(/\n/g, "<br />");
|
|
26
|
+
if (withBr.match(/^(<div|<iframe|<video|<img)/)) {
|
|
27
|
+
return withBr;
|
|
28
|
+
}
|
|
29
|
+
return `<p>${withBr}</p>`;
|
|
30
|
+
}).join("\n");
|
|
31
|
+
return html;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/utils/index.ts
|
|
35
|
+
var WHITESPACE_RE = /\s+/g;
|
|
36
|
+
function normalizeQuery(query) {
|
|
37
|
+
return query.replace(WHITESPACE_RE, " ").trim();
|
|
38
|
+
}
|
|
39
|
+
function clampPerPage(value) {
|
|
40
|
+
return Math.min(Math.max(value, 1), 50);
|
|
41
|
+
}
|
|
42
|
+
function chunk(arr, size) {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
45
|
+
chunks.push(arr.slice(i, i + size));
|
|
46
|
+
}
|
|
47
|
+
return chunks;
|
|
48
|
+
}
|
|
49
|
+
function validateId(id, label = "id") {
|
|
50
|
+
if (!Number.isFinite(id) || !Number.isInteger(id) || id < 1) {
|
|
51
|
+
throw new RangeError(`Invalid ${label}: expected a positive integer, got ${id}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function validateIds(ids, label = "id") {
|
|
55
|
+
for (const id of ids) {
|
|
56
|
+
validateId(id, label);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function sortObjectKeys(obj) {
|
|
60
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
61
|
+
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
|
|
62
|
+
const sorted = {};
|
|
63
|
+
for (const key of Object.keys(obj).sort()) {
|
|
64
|
+
sorted[key] = sortObjectKeys(obj[key]);
|
|
65
|
+
}
|
|
66
|
+
return sorted;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/cache/index.ts
|
|
70
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
71
|
+
var MemoryCache = class {
|
|
72
|
+
constructor(options = {}) {
|
|
73
|
+
this.store = /* @__PURE__ */ new Map();
|
|
74
|
+
this.ttl = options.ttl ?? ONE_DAY_MS;
|
|
75
|
+
this.maxSize = options.maxSize ?? 500;
|
|
76
|
+
this.enabled = options.enabled ?? true;
|
|
77
|
+
}
|
|
78
|
+
/** Build a deterministic cache key from a query + variables pair. */
|
|
79
|
+
static key(query, variables) {
|
|
80
|
+
const normalized = normalizeQuery(query);
|
|
81
|
+
return `${normalized}|${JSON.stringify(sortObjectKeys(variables))}`;
|
|
82
|
+
}
|
|
83
|
+
/** Retrieve a cached value, or `undefined` if missing / expired. */
|
|
84
|
+
get(key) {
|
|
85
|
+
if (!this.enabled) return void 0;
|
|
86
|
+
const entry = this.store.get(key);
|
|
87
|
+
if (!entry) return void 0;
|
|
88
|
+
if (Date.now() > entry.expiresAt) {
|
|
89
|
+
this.store.delete(key);
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
this.store.delete(key);
|
|
93
|
+
this.store.set(key, entry);
|
|
94
|
+
return entry.data;
|
|
95
|
+
}
|
|
96
|
+
/** Store a value in the cache. */
|
|
97
|
+
set(key, data) {
|
|
98
|
+
if (!this.enabled) return;
|
|
99
|
+
this.store.delete(key);
|
|
100
|
+
if (this.maxSize > 0 && this.store.size >= this.maxSize) {
|
|
101
|
+
const firstKey = this.store.keys().next().value;
|
|
102
|
+
if (firstKey !== void 0) this.store.delete(firstKey);
|
|
103
|
+
}
|
|
104
|
+
this.store.set(key, { data, expiresAt: Date.now() + this.ttl });
|
|
105
|
+
}
|
|
106
|
+
/** Remove a specific entry. */
|
|
107
|
+
delete(key) {
|
|
108
|
+
return this.store.delete(key);
|
|
109
|
+
}
|
|
110
|
+
/** Clear the entire cache. */
|
|
111
|
+
clear() {
|
|
112
|
+
this.store.clear();
|
|
113
|
+
}
|
|
114
|
+
/** Number of entries currently stored. */
|
|
115
|
+
get size() {
|
|
116
|
+
return this.store.size;
|
|
117
|
+
}
|
|
118
|
+
/** Return all cache keys. */
|
|
119
|
+
keys() {
|
|
120
|
+
return [...this.store.keys()];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Remove all entries whose key matches the given pattern.
|
|
124
|
+
*
|
|
125
|
+
* - **String**: treated as a substring match (e.g. `"Media"` removes all keys containing `"Media"`).
|
|
126
|
+
* - **RegExp**: tested against each key directly.
|
|
127
|
+
*
|
|
128
|
+
* @param pattern — A string (substring match) or RegExp.
|
|
129
|
+
* @returns Number of entries removed.
|
|
130
|
+
*/
|
|
131
|
+
invalidate(pattern) {
|
|
132
|
+
const test = typeof pattern === "string" ? (key) => key.includes(pattern) : (key) => pattern.test(key);
|
|
133
|
+
const toDelete = [];
|
|
134
|
+
for (const key of this.store.keys()) {
|
|
135
|
+
if (test(key)) toDelete.push(key);
|
|
136
|
+
}
|
|
137
|
+
for (const key of toDelete) this.store.delete(key);
|
|
138
|
+
return toDelete.length;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/errors/index.ts
|
|
143
|
+
var AniListError = class _AniListError extends Error {
|
|
144
|
+
constructor(message, status, errors = []) {
|
|
145
|
+
super(message);
|
|
146
|
+
this.name = "AniListError";
|
|
147
|
+
this.status = status;
|
|
148
|
+
this.errors = errors;
|
|
149
|
+
Object.setPrototypeOf(this, _AniListError.prototype);
|
|
150
|
+
if (Error.captureStackTrace) {
|
|
151
|
+
Error.captureStackTrace(this, _AniListError);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// src/queries/fragments.ts
|
|
4
157
|
var MEDIA_FIELDS_BASE = `
|
|
5
158
|
id
|
|
6
159
|
idMal
|
|
@@ -193,6 +346,92 @@ var USER_FIELDS = `
|
|
|
193
346
|
manga { count meanScore minutesWatched episodesWatched chaptersRead volumesRead }
|
|
194
347
|
}
|
|
195
348
|
`;
|
|
349
|
+
var USER_FAVORITES_FIELDS = `
|
|
350
|
+
favourites {
|
|
351
|
+
anime(perPage: 25) {
|
|
352
|
+
nodes {
|
|
353
|
+
id
|
|
354
|
+
title { romaji english native userPreferred }
|
|
355
|
+
coverImage { large medium }
|
|
356
|
+
type
|
|
357
|
+
format
|
|
358
|
+
siteUrl
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
manga(perPage: 25) {
|
|
362
|
+
nodes {
|
|
363
|
+
id
|
|
364
|
+
title { romaji english native userPreferred }
|
|
365
|
+
coverImage { large medium }
|
|
366
|
+
type
|
|
367
|
+
format
|
|
368
|
+
siteUrl
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
characters(perPage: 25) {
|
|
372
|
+
nodes {
|
|
373
|
+
id
|
|
374
|
+
name { full native }
|
|
375
|
+
image { large medium }
|
|
376
|
+
siteUrl
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
staff(perPage: 25) {
|
|
380
|
+
nodes {
|
|
381
|
+
id
|
|
382
|
+
name { full native }
|
|
383
|
+
image { large medium }
|
|
384
|
+
siteUrl
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
studios(perPage: 25) {
|
|
388
|
+
nodes {
|
|
389
|
+
id
|
|
390
|
+
name
|
|
391
|
+
siteUrl
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
`;
|
|
396
|
+
var MEDIA_LIST_FIELDS = `
|
|
397
|
+
id
|
|
398
|
+
mediaId
|
|
399
|
+
status
|
|
400
|
+
score(format: POINT_100)
|
|
401
|
+
progress
|
|
402
|
+
progressVolumes
|
|
403
|
+
repeat
|
|
404
|
+
priority
|
|
405
|
+
private
|
|
406
|
+
notes
|
|
407
|
+
startedAt { year month day }
|
|
408
|
+
completedAt { year month day }
|
|
409
|
+
updatedAt
|
|
410
|
+
createdAt
|
|
411
|
+
media {
|
|
412
|
+
${MEDIA_FIELDS_BASE}
|
|
413
|
+
}
|
|
414
|
+
`;
|
|
415
|
+
var STUDIO_FIELDS = `
|
|
416
|
+
id
|
|
417
|
+
name
|
|
418
|
+
isAnimationStudio
|
|
419
|
+
siteUrl
|
|
420
|
+
favourites
|
|
421
|
+
media(page: 1, perPage: 25, sort: POPULARITY_DESC) {
|
|
422
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
423
|
+
nodes {
|
|
424
|
+
id
|
|
425
|
+
title { romaji english native userPreferred }
|
|
426
|
+
type
|
|
427
|
+
format
|
|
428
|
+
coverImage { large medium }
|
|
429
|
+
siteUrl
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
`;
|
|
433
|
+
|
|
434
|
+
// src/queries/media.ts
|
|
196
435
|
var QUERY_MEDIA_BY_ID = `
|
|
197
436
|
query ($id: Int!) {
|
|
198
437
|
Media(id: $id) {
|
|
@@ -249,79 +488,6 @@ query ($type: MediaType, $page: Int, $perPage: Int) {
|
|
|
249
488
|
}
|
|
250
489
|
}
|
|
251
490
|
}`;
|
|
252
|
-
var QUERY_CHARACTER_BY_ID = `
|
|
253
|
-
query ($id: Int!) {
|
|
254
|
-
Character(id: $id) {
|
|
255
|
-
${CHARACTER_FIELDS}
|
|
256
|
-
}
|
|
257
|
-
}`;
|
|
258
|
-
var QUERY_CHARACTER_BY_ID_WITH_VA = `
|
|
259
|
-
query ($id: Int!) {
|
|
260
|
-
Character(id: $id) {
|
|
261
|
-
${CHARACTER_FIELDS_WITH_VA}
|
|
262
|
-
}
|
|
263
|
-
}`;
|
|
264
|
-
var QUERY_CHARACTER_SEARCH = `
|
|
265
|
-
query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
|
|
266
|
-
Page(page: $page, perPage: $perPage) {
|
|
267
|
-
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
268
|
-
characters(search: $search, sort: $sort) {
|
|
269
|
-
${CHARACTER_FIELDS}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}`;
|
|
273
|
-
var QUERY_CHARACTER_SEARCH_WITH_VA = `
|
|
274
|
-
query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
|
|
275
|
-
Page(page: $page, perPage: $perPage) {
|
|
276
|
-
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
277
|
-
characters(search: $search, sort: $sort) {
|
|
278
|
-
${CHARACTER_FIELDS_WITH_VA}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}`;
|
|
282
|
-
var QUERY_STAFF_BY_ID = `
|
|
283
|
-
query ($id: Int!) {
|
|
284
|
-
Staff(id: $id) {
|
|
285
|
-
${STAFF_FIELDS}
|
|
286
|
-
}
|
|
287
|
-
}`;
|
|
288
|
-
var QUERY_STAFF_BY_ID_WITH_MEDIA = `
|
|
289
|
-
query ($id: Int!, $perPage: Int) {
|
|
290
|
-
Staff(id: $id) {
|
|
291
|
-
${STAFF_FIELDS}
|
|
292
|
-
${STAFF_MEDIA_FIELDS}
|
|
293
|
-
}
|
|
294
|
-
}`;
|
|
295
|
-
var QUERY_STAFF_SEARCH = `
|
|
296
|
-
query ($search: String, $sort: [StaffSort], $page: Int, $perPage: Int) {
|
|
297
|
-
Page(page: $page, perPage: $perPage) {
|
|
298
|
-
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
299
|
-
staff(search: $search, sort: $sort) {
|
|
300
|
-
${STAFF_FIELDS}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}`;
|
|
304
|
-
var QUERY_USER_BY_ID = `
|
|
305
|
-
query ($id: Int!) {
|
|
306
|
-
User(id: $id) {
|
|
307
|
-
${USER_FIELDS}
|
|
308
|
-
}
|
|
309
|
-
}`;
|
|
310
|
-
var QUERY_USER_BY_NAME = `
|
|
311
|
-
query ($name: String!) {
|
|
312
|
-
User(name: $name) {
|
|
313
|
-
${USER_FIELDS}
|
|
314
|
-
}
|
|
315
|
-
}`;
|
|
316
|
-
var QUERY_USER_SEARCH = `
|
|
317
|
-
query ($search: String, $sort: [UserSort], $page: Int, $perPage: Int) {
|
|
318
|
-
Page(page: $page, perPage: $perPage) {
|
|
319
|
-
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
320
|
-
users(search: $search, sort: $sort) {
|
|
321
|
-
${USER_FIELDS}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}`;
|
|
325
491
|
var QUERY_AIRING_SCHEDULE = `
|
|
326
492
|
query ($airingAt_greater: Int, $airingAt_lesser: Int, $sort: [AiringSort], $page: Int, $perPage: Int) {
|
|
327
493
|
Page(page: $page, perPage: $perPage) {
|
|
@@ -365,25 +531,6 @@ query ($season: MediaSeason!, $seasonYear: Int!, $type: MediaType, $sort: [Media
|
|
|
365
531
|
}
|
|
366
532
|
}
|
|
367
533
|
}`;
|
|
368
|
-
var MEDIA_LIST_FIELDS = `
|
|
369
|
-
id
|
|
370
|
-
mediaId
|
|
371
|
-
status
|
|
372
|
-
score(format: POINT_100)
|
|
373
|
-
progress
|
|
374
|
-
progressVolumes
|
|
375
|
-
repeat
|
|
376
|
-
priority
|
|
377
|
-
private
|
|
378
|
-
notes
|
|
379
|
-
startedAt { year month day }
|
|
380
|
-
completedAt { year month day }
|
|
381
|
-
updatedAt
|
|
382
|
-
createdAt
|
|
383
|
-
media {
|
|
384
|
-
${MEDIA_FIELDS_BASE}
|
|
385
|
-
}
|
|
386
|
-
`;
|
|
387
534
|
var QUERY_RECOMMENDATIONS = `
|
|
388
535
|
query ($mediaId: Int!, $page: Int, $perPage: Int, $sort: [RecommendationSort]) {
|
|
389
536
|
Media(id: $mediaId) {
|
|
@@ -418,6 +565,85 @@ query ($mediaId: Int!, $page: Int, $perPage: Int, $sort: [RecommendationSort]) {
|
|
|
418
565
|
}
|
|
419
566
|
}
|
|
420
567
|
}`;
|
|
568
|
+
|
|
569
|
+
// src/queries/character.ts
|
|
570
|
+
var QUERY_CHARACTER_BY_ID = `
|
|
571
|
+
query ($id: Int!) {
|
|
572
|
+
Character(id: $id) {
|
|
573
|
+
${CHARACTER_FIELDS}
|
|
574
|
+
}
|
|
575
|
+
}`;
|
|
576
|
+
var QUERY_CHARACTER_BY_ID_WITH_VA = `
|
|
577
|
+
query ($id: Int!) {
|
|
578
|
+
Character(id: $id) {
|
|
579
|
+
${CHARACTER_FIELDS_WITH_VA}
|
|
580
|
+
}
|
|
581
|
+
}`;
|
|
582
|
+
var QUERY_CHARACTER_SEARCH = `
|
|
583
|
+
query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
|
|
584
|
+
Page(page: $page, perPage: $perPage) {
|
|
585
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
586
|
+
characters(search: $search, sort: $sort) {
|
|
587
|
+
${CHARACTER_FIELDS}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}`;
|
|
591
|
+
var QUERY_CHARACTER_SEARCH_WITH_VA = `
|
|
592
|
+
query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
|
|
593
|
+
Page(page: $page, perPage: $perPage) {
|
|
594
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
595
|
+
characters(search: $search, sort: $sort) {
|
|
596
|
+
${CHARACTER_FIELDS_WITH_VA}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}`;
|
|
600
|
+
|
|
601
|
+
// src/queries/staff.ts
|
|
602
|
+
var QUERY_STAFF_BY_ID = `
|
|
603
|
+
query ($id: Int!) {
|
|
604
|
+
Staff(id: $id) {
|
|
605
|
+
${STAFF_FIELDS}
|
|
606
|
+
}
|
|
607
|
+
}`;
|
|
608
|
+
var QUERY_STAFF_BY_ID_WITH_MEDIA = `
|
|
609
|
+
query ($id: Int!, $perPage: Int) {
|
|
610
|
+
Staff(id: $id) {
|
|
611
|
+
${STAFF_FIELDS}
|
|
612
|
+
${STAFF_MEDIA_FIELDS}
|
|
613
|
+
}
|
|
614
|
+
}`;
|
|
615
|
+
var QUERY_STAFF_SEARCH = `
|
|
616
|
+
query ($search: String, $sort: [StaffSort], $page: Int, $perPage: Int) {
|
|
617
|
+
Page(page: $page, perPage: $perPage) {
|
|
618
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
619
|
+
staff(search: $search, sort: $sort) {
|
|
620
|
+
${STAFF_FIELDS}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}`;
|
|
624
|
+
|
|
625
|
+
// src/queries/user.ts
|
|
626
|
+
var QUERY_USER_BY_ID = `
|
|
627
|
+
query ($id: Int!) {
|
|
628
|
+
User(id: $id) {
|
|
629
|
+
${USER_FIELDS}
|
|
630
|
+
}
|
|
631
|
+
}`;
|
|
632
|
+
var QUERY_USER_BY_NAME = `
|
|
633
|
+
query ($name: String!) {
|
|
634
|
+
User(name: $name) {
|
|
635
|
+
${USER_FIELDS}
|
|
636
|
+
}
|
|
637
|
+
}`;
|
|
638
|
+
var QUERY_USER_SEARCH = `
|
|
639
|
+
query ($search: String, $sort: [UserSort], $page: Int, $perPage: Int) {
|
|
640
|
+
Page(page: $page, perPage: $perPage) {
|
|
641
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
642
|
+
users(search: $search, sort: $sort) {
|
|
643
|
+
${USER_FIELDS}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}`;
|
|
421
647
|
var QUERY_USER_MEDIA_LIST = `
|
|
422
648
|
query ($userId: Int, $userName: String, $type: MediaType!, $status: MediaListStatus, $sort: [MediaListSort], $page: Int, $perPage: Int) {
|
|
423
649
|
Page(page: $page, perPage: $perPage) {
|
|
@@ -427,24 +653,24 @@ query ($userId: Int, $userName: String, $type: MediaType!, $status: MediaListSta
|
|
|
427
653
|
}
|
|
428
654
|
}
|
|
429
655
|
}`;
|
|
430
|
-
var
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
media(page: 1, perPage: 25, sort: POPULARITY_DESC) {
|
|
437
|
-
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
438
|
-
nodes {
|
|
439
|
-
id
|
|
440
|
-
title { romaji english native userPreferred }
|
|
441
|
-
type
|
|
442
|
-
format
|
|
443
|
-
coverImage { large medium }
|
|
444
|
-
siteUrl
|
|
445
|
-
}
|
|
656
|
+
var QUERY_USER_FAVORITES_BY_ID = `
|
|
657
|
+
query ($id: Int!) {
|
|
658
|
+
User(id: $id) {
|
|
659
|
+
id
|
|
660
|
+
name
|
|
661
|
+
${USER_FAVORITES_FIELDS}
|
|
446
662
|
}
|
|
447
|
-
`;
|
|
663
|
+
}`;
|
|
664
|
+
var QUERY_USER_FAVORITES_BY_NAME = `
|
|
665
|
+
query ($name: String!) {
|
|
666
|
+
User(name: $name) {
|
|
667
|
+
id
|
|
668
|
+
name
|
|
669
|
+
${USER_FAVORITES_FIELDS}
|
|
670
|
+
}
|
|
671
|
+
}`;
|
|
672
|
+
|
|
673
|
+
// src/queries/studio.ts
|
|
448
674
|
var QUERY_STUDIO_BY_ID = `
|
|
449
675
|
query ($id: Int!) {
|
|
450
676
|
Studio(id: $id) {
|
|
@@ -460,6 +686,8 @@ query ($search: String, $page: Int, $perPage: Int) {
|
|
|
460
686
|
}
|
|
461
687
|
}
|
|
462
688
|
}`;
|
|
689
|
+
|
|
690
|
+
// src/queries/metadata.ts
|
|
463
691
|
var QUERY_GENRES = `
|
|
464
692
|
query {
|
|
465
693
|
GenreCollection
|
|
@@ -474,6 +702,8 @@ query {
|
|
|
474
702
|
isAdult
|
|
475
703
|
}
|
|
476
704
|
}`;
|
|
705
|
+
|
|
706
|
+
// src/queries/builders.ts
|
|
477
707
|
function buildMediaByIdQuery(include) {
|
|
478
708
|
if (!include) return QUERY_MEDIA_BY_ID;
|
|
479
709
|
const extra = [];
|
|
@@ -576,107 +806,64 @@ var buildBatchMediaQuery = (ids) => buildBatchQuery(ids, "Media", MEDIA_FIELDS_B
|
|
|
576
806
|
var buildBatchCharacterQuery = (ids) => buildBatchQuery(ids, "Character", CHARACTER_FIELDS, "c");
|
|
577
807
|
var buildBatchStaffQuery = (ids) => buildBatchQuery(ids, "Staff", STAFF_FIELDS, "s");
|
|
578
808
|
|
|
579
|
-
// src/
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
this.maxSize = options.maxSize ?? 500;
|
|
601
|
-
this.enabled = options.enabled ?? true;
|
|
602
|
-
}
|
|
603
|
-
/** Build a deterministic cache key from a query + variables pair. */
|
|
604
|
-
static key(query, variables) {
|
|
605
|
-
const normalized = normalizeQuery(query);
|
|
606
|
-
return `${normalized}|${JSON.stringify(variables, Object.keys(variables).sort())}`;
|
|
607
|
-
}
|
|
608
|
-
/** Retrieve a cached value, or `undefined` if missing / expired. */
|
|
609
|
-
get(key) {
|
|
610
|
-
if (!this.enabled) return void 0;
|
|
611
|
-
const entry = this.store.get(key);
|
|
612
|
-
if (!entry) return void 0;
|
|
613
|
-
if (Date.now() > entry.expiresAt) {
|
|
614
|
-
this.store.delete(key);
|
|
615
|
-
return void 0;
|
|
616
|
-
}
|
|
617
|
-
this.store.delete(key);
|
|
618
|
-
this.store.set(key, entry);
|
|
619
|
-
return entry.data;
|
|
620
|
-
}
|
|
621
|
-
/** Store a value in the cache. */
|
|
622
|
-
set(key, data) {
|
|
623
|
-
if (!this.enabled) return;
|
|
624
|
-
this.store.delete(key);
|
|
625
|
-
if (this.maxSize > 0 && this.store.size >= this.maxSize) {
|
|
626
|
-
const firstKey = this.store.keys().next().value;
|
|
627
|
-
if (firstKey !== void 0) this.store.delete(firstKey);
|
|
628
|
-
}
|
|
629
|
-
this.store.set(key, { data, expiresAt: Date.now() + this.ttl });
|
|
809
|
+
// src/queries/thread.ts
|
|
810
|
+
var THREAD_FIELDS = `
|
|
811
|
+
id
|
|
812
|
+
title
|
|
813
|
+
body(asHtml: false)
|
|
814
|
+
userId
|
|
815
|
+
replyUserId
|
|
816
|
+
replyCommentId
|
|
817
|
+
replyCount
|
|
818
|
+
viewCount
|
|
819
|
+
isLocked
|
|
820
|
+
isSticky
|
|
821
|
+
isSubscribed
|
|
822
|
+
repliedAt
|
|
823
|
+
createdAt
|
|
824
|
+
updatedAt
|
|
825
|
+
siteUrl
|
|
826
|
+
user {
|
|
827
|
+
id
|
|
828
|
+
name
|
|
829
|
+
avatar { large medium }
|
|
630
830
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
831
|
+
replyUser {
|
|
832
|
+
id
|
|
833
|
+
name
|
|
834
|
+
avatar { large medium }
|
|
634
835
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
836
|
+
categories {
|
|
837
|
+
id
|
|
838
|
+
name
|
|
638
839
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
840
|
+
mediaCategories {
|
|
841
|
+
id
|
|
842
|
+
title { romaji english native userPreferred }
|
|
843
|
+
type
|
|
844
|
+
coverImage { large medium }
|
|
845
|
+
siteUrl
|
|
642
846
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
847
|
+
likes {
|
|
848
|
+
id
|
|
849
|
+
name
|
|
646
850
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
*
|
|
653
|
-
* @param pattern — A string (substring match) or RegExp.
|
|
654
|
-
* @returns Number of entries removed.
|
|
655
|
-
*/
|
|
656
|
-
invalidate(pattern) {
|
|
657
|
-
const test = typeof pattern === "string" ? (key) => key.includes(pattern) : (key) => pattern.test(key);
|
|
658
|
-
const toDelete = [];
|
|
659
|
-
for (const key of this.store.keys()) {
|
|
660
|
-
if (test(key)) toDelete.push(key);
|
|
661
|
-
}
|
|
662
|
-
for (const key of toDelete) this.store.delete(key);
|
|
663
|
-
return toDelete.length;
|
|
851
|
+
`;
|
|
852
|
+
var QUERY_THREAD_BY_ID = `
|
|
853
|
+
query ($id: Int!) {
|
|
854
|
+
Thread(id: $id) {
|
|
855
|
+
${THREAD_FIELDS}
|
|
664
856
|
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
this.status = status;
|
|
673
|
-
this.errors = errors;
|
|
674
|
-
Object.setPrototypeOf(this, _AniListError.prototype);
|
|
675
|
-
if (Error.captureStackTrace) {
|
|
676
|
-
Error.captureStackTrace(this, _AniListError);
|
|
857
|
+
}`;
|
|
858
|
+
var QUERY_THREAD_SEARCH = `
|
|
859
|
+
query ($search: String, $mediaCategoryId: Int, $categoryId: Int, $sort: [ThreadSort], $page: Int, $perPage: Int) {
|
|
860
|
+
Page(page: $page, perPage: $perPage) {
|
|
861
|
+
pageInfo { total perPage currentPage lastPage hasNextPage }
|
|
862
|
+
threads(search: $search, mediaCategoryId: $mediaCategoryId, categoryId: $categoryId, sort: $sort) {
|
|
863
|
+
${THREAD_FIELDS}
|
|
677
864
|
}
|
|
678
865
|
}
|
|
679
|
-
}
|
|
866
|
+
}`;
|
|
680
867
|
|
|
681
868
|
// src/rate-limiter/index.ts
|
|
682
869
|
var RateLimiter = class {
|
|
@@ -690,6 +877,7 @@ var RateLimiter = class {
|
|
|
690
877
|
this.enabled = options.enabled ?? true;
|
|
691
878
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
692
879
|
this.retryOnNetworkError = options.retryOnNetworkError ?? true;
|
|
880
|
+
this.retryStrategy = options.retryStrategy;
|
|
693
881
|
this.timestamps = new Array(this.maxRequests).fill(0);
|
|
694
882
|
}
|
|
695
883
|
/**
|
|
@@ -750,8 +938,11 @@ var RateLimiter = class {
|
|
|
750
938
|
if (lastResponse) return lastResponse;
|
|
751
939
|
throw lastError;
|
|
752
940
|
}
|
|
753
|
-
/** @internal — Exponential backoff with jitter, capped at 30s */
|
|
941
|
+
/** @internal — Exponential backoff with jitter, capped at 30s (or custom strategy) */
|
|
754
942
|
exponentialDelay(attempt) {
|
|
943
|
+
if (this.retryStrategy) {
|
|
944
|
+
return this.retryStrategy(attempt, this.retryDelayMs);
|
|
945
|
+
}
|
|
755
946
|
const base = this.retryDelayMs * 2 ** attempt;
|
|
756
947
|
const jitter = Math.random() * 1e3;
|
|
757
948
|
return Math.min(base + jitter, 3e4);
|
|
@@ -789,6 +980,19 @@ function isNetworkError(err) {
|
|
|
789
980
|
return false;
|
|
790
981
|
}
|
|
791
982
|
|
|
983
|
+
// src/client/character.ts
|
|
984
|
+
async function getCharacter(client, id, include) {
|
|
985
|
+
validateId(id, "characterId");
|
|
986
|
+
const query = include?.voiceActors ? QUERY_CHARACTER_BY_ID_WITH_VA : QUERY_CHARACTER_BY_ID;
|
|
987
|
+
const data = await client.request(query, { id });
|
|
988
|
+
return data.Character;
|
|
989
|
+
}
|
|
990
|
+
async function searchCharacters(client, options = {}) {
|
|
991
|
+
const { query: search, page = 1, perPage = 20, sort, voiceActors } = options;
|
|
992
|
+
const gqlQuery = voiceActors ? QUERY_CHARACTER_SEARCH_WITH_VA : QUERY_CHARACTER_SEARCH;
|
|
993
|
+
return client.pagedRequest(gqlQuery, { search, sort, page, perPage: clampPerPage(perPage) }, "characters");
|
|
994
|
+
}
|
|
995
|
+
|
|
792
996
|
// src/types/media.ts
|
|
793
997
|
var MediaType = /* @__PURE__ */ ((MediaType2) => {
|
|
794
998
|
MediaType2["ANIME"] = "ANIME";
|
|
@@ -1003,6 +1207,274 @@ var MediaListSort = /* @__PURE__ */ ((MediaListSort2) => {
|
|
|
1003
1207
|
return MediaListSort2;
|
|
1004
1208
|
})(MediaListSort || {});
|
|
1005
1209
|
|
|
1210
|
+
// src/types/thread.ts
|
|
1211
|
+
var ThreadSort = /* @__PURE__ */ ((ThreadSort2) => {
|
|
1212
|
+
ThreadSort2["ID"] = "ID";
|
|
1213
|
+
ThreadSort2["ID_DESC"] = "ID_DESC";
|
|
1214
|
+
ThreadSort2["TITLE"] = "TITLE";
|
|
1215
|
+
ThreadSort2["TITLE_DESC"] = "TITLE_DESC";
|
|
1216
|
+
ThreadSort2["CREATED_AT"] = "CREATED_AT";
|
|
1217
|
+
ThreadSort2["CREATED_AT_DESC"] = "CREATED_AT_DESC";
|
|
1218
|
+
ThreadSort2["UPDATED_AT"] = "UPDATED_AT";
|
|
1219
|
+
ThreadSort2["UPDATED_AT_DESC"] = "UPDATED_AT_DESC";
|
|
1220
|
+
ThreadSort2["REPLIED_AT"] = "REPLIED_AT";
|
|
1221
|
+
ThreadSort2["REPLIED_AT_DESC"] = "REPLIED_AT_DESC";
|
|
1222
|
+
ThreadSort2["REPLY_COUNT"] = "REPLY_COUNT";
|
|
1223
|
+
ThreadSort2["REPLY_COUNT_DESC"] = "REPLY_COUNT_DESC";
|
|
1224
|
+
ThreadSort2["VIEW_COUNT"] = "VIEW_COUNT";
|
|
1225
|
+
ThreadSort2["VIEW_COUNT_DESC"] = "VIEW_COUNT_DESC";
|
|
1226
|
+
ThreadSort2["IS_STICKY"] = "IS_STICKY";
|
|
1227
|
+
ThreadSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
|
|
1228
|
+
return ThreadSort2;
|
|
1229
|
+
})(ThreadSort || {});
|
|
1230
|
+
|
|
1231
|
+
// src/client/media.ts
|
|
1232
|
+
async function getMedia(client, id, include) {
|
|
1233
|
+
validateId(id, "mediaId");
|
|
1234
|
+
const query = buildMediaByIdQuery(include);
|
|
1235
|
+
const data = await client.request(query, { id });
|
|
1236
|
+
return data.Media;
|
|
1237
|
+
}
|
|
1238
|
+
async function searchMedia(client, options = {}) {
|
|
1239
|
+
const { query: search, page = 1, perPage = 20, genres, tags, genresExclude, tagsExclude, ...filters } = options;
|
|
1240
|
+
return client.pagedRequest(
|
|
1241
|
+
QUERY_MEDIA_SEARCH,
|
|
1242
|
+
{
|
|
1243
|
+
search,
|
|
1244
|
+
...filters,
|
|
1245
|
+
genre_in: genres,
|
|
1246
|
+
tag_in: tags,
|
|
1247
|
+
genre_not_in: genresExclude,
|
|
1248
|
+
tag_not_in: tagsExclude,
|
|
1249
|
+
page,
|
|
1250
|
+
perPage: clampPerPage(perPage)
|
|
1251
|
+
},
|
|
1252
|
+
"media"
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
async function getTrending(client, type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1256
|
+
return client.pagedRequest(QUERY_TRENDING, { type, page, perPage: clampPerPage(perPage) }, "media");
|
|
1257
|
+
}
|
|
1258
|
+
async function getPopular(client, type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1259
|
+
return searchMedia(client, { type, sort: ["POPULARITY_DESC" /* POPULARITY_DESC */], page, perPage });
|
|
1260
|
+
}
|
|
1261
|
+
async function getTopRated(client, type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1262
|
+
return searchMedia(client, { type, sort: ["SCORE_DESC" /* SCORE_DESC */], page, perPage });
|
|
1263
|
+
}
|
|
1264
|
+
async function getAiredEpisodes(client, options = {}) {
|
|
1265
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1266
|
+
return client.pagedRequest(
|
|
1267
|
+
QUERY_AIRING_SCHEDULE,
|
|
1268
|
+
{
|
|
1269
|
+
airingAt_greater: options.airingAtGreater ?? now - 24 * 3600,
|
|
1270
|
+
airingAt_lesser: options.airingAtLesser ?? now,
|
|
1271
|
+
sort: options.sort,
|
|
1272
|
+
page: options.page ?? 1,
|
|
1273
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1274
|
+
},
|
|
1275
|
+
"airingSchedules"
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
async function getAiredChapters(client, options = {}) {
|
|
1279
|
+
return client.pagedRequest(
|
|
1280
|
+
QUERY_RECENT_CHAPTERS,
|
|
1281
|
+
{
|
|
1282
|
+
page: options.page ?? 1,
|
|
1283
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1284
|
+
},
|
|
1285
|
+
"media"
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
async function getPlanning(client, options = {}) {
|
|
1289
|
+
return client.pagedRequest(
|
|
1290
|
+
QUERY_PLANNING,
|
|
1291
|
+
{
|
|
1292
|
+
type: options.type,
|
|
1293
|
+
sort: options.sort ?? ["POPULARITY_DESC" /* POPULARITY_DESC */],
|
|
1294
|
+
page: options.page ?? 1,
|
|
1295
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1296
|
+
},
|
|
1297
|
+
"media"
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
async function getRecommendations(client, mediaId, options = {}) {
|
|
1301
|
+
const data = await client.request(QUERY_RECOMMENDATIONS, {
|
|
1302
|
+
mediaId,
|
|
1303
|
+
page: options.page ?? 1,
|
|
1304
|
+
perPage: clampPerPage(options.perPage ?? 20),
|
|
1305
|
+
sort: options.sort
|
|
1306
|
+
});
|
|
1307
|
+
return {
|
|
1308
|
+
pageInfo: data.Media.recommendations.pageInfo,
|
|
1309
|
+
results: data.Media.recommendations.nodes
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
async function getMediaBySeason(client, options) {
|
|
1313
|
+
return client.pagedRequest(
|
|
1314
|
+
QUERY_MEDIA_BY_SEASON,
|
|
1315
|
+
{
|
|
1316
|
+
season: options.season,
|
|
1317
|
+
seasonYear: options.seasonYear,
|
|
1318
|
+
type: options.type,
|
|
1319
|
+
sort: options.sort,
|
|
1320
|
+
page: options.page ?? 1,
|
|
1321
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1322
|
+
},
|
|
1323
|
+
"media"
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
async function getWeeklySchedule(client, date = /* @__PURE__ */ new Date()) {
|
|
1327
|
+
const schedule = {
|
|
1328
|
+
Monday: [],
|
|
1329
|
+
Tuesday: [],
|
|
1330
|
+
Wednesday: [],
|
|
1331
|
+
Thursday: [],
|
|
1332
|
+
Friday: [],
|
|
1333
|
+
Saturday: [],
|
|
1334
|
+
Sunday: []
|
|
1335
|
+
};
|
|
1336
|
+
const startOfWeek = new Date(date);
|
|
1337
|
+
const day = startOfWeek.getDay();
|
|
1338
|
+
const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1);
|
|
1339
|
+
startOfWeek.setDate(diff);
|
|
1340
|
+
startOfWeek.setHours(0, 0, 0, 0);
|
|
1341
|
+
const endOfWeek = new Date(startOfWeek);
|
|
1342
|
+
endOfWeek.setDate(startOfWeek.getDate() + 6);
|
|
1343
|
+
endOfWeek.setHours(23, 59, 59, 999);
|
|
1344
|
+
const startTimestamp = Math.floor(startOfWeek.getTime() / 1e3);
|
|
1345
|
+
const endTimestamp = Math.floor(endOfWeek.getTime() / 1e3);
|
|
1346
|
+
const iterator = client.paginate(
|
|
1347
|
+
(page) => getAiredEpisodes(client, {
|
|
1348
|
+
airingAtGreater: startTimestamp,
|
|
1349
|
+
airingAtLesser: endTimestamp,
|
|
1350
|
+
page,
|
|
1351
|
+
perPage: 50
|
|
1352
|
+
})
|
|
1353
|
+
);
|
|
1354
|
+
const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1355
|
+
for await (const episode of iterator) {
|
|
1356
|
+
const epDate = new Date(episode.airingAt * 1e3);
|
|
1357
|
+
const dayName = names[epDate.getDay()];
|
|
1358
|
+
schedule[dayName].push(episode);
|
|
1359
|
+
}
|
|
1360
|
+
return schedule;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/client/staff.ts
|
|
1364
|
+
async function getStaff(client, id, include) {
|
|
1365
|
+
validateId(id, "staffId");
|
|
1366
|
+
if (include?.media) {
|
|
1367
|
+
const perPage = typeof include.media === "object" ? include.media.perPage ?? 25 : 25;
|
|
1368
|
+
const data2 = await client.request(QUERY_STAFF_BY_ID_WITH_MEDIA, { id, perPage });
|
|
1369
|
+
return data2.Staff;
|
|
1370
|
+
}
|
|
1371
|
+
const data = await client.request(QUERY_STAFF_BY_ID, { id });
|
|
1372
|
+
return data.Staff;
|
|
1373
|
+
}
|
|
1374
|
+
async function searchStaff(client, options = {}) {
|
|
1375
|
+
const { query: search, page = 1, perPage = 20, sort } = options;
|
|
1376
|
+
return client.pagedRequest(
|
|
1377
|
+
QUERY_STAFF_SEARCH,
|
|
1378
|
+
{ search, sort, page, perPage: clampPerPage(perPage) },
|
|
1379
|
+
"staff"
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// src/client/studio.ts
|
|
1384
|
+
async function getStudio(client, id) {
|
|
1385
|
+
validateId(id, "studioId");
|
|
1386
|
+
const data = await client.request(QUERY_STUDIO_BY_ID, { id });
|
|
1387
|
+
return data.Studio;
|
|
1388
|
+
}
|
|
1389
|
+
async function searchStudios(client, options = {}) {
|
|
1390
|
+
return client.pagedRequest(
|
|
1391
|
+
QUERY_STUDIO_SEARCH,
|
|
1392
|
+
{
|
|
1393
|
+
search: options.query,
|
|
1394
|
+
page: options.page ?? 1,
|
|
1395
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1396
|
+
},
|
|
1397
|
+
"studios"
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/client/thread.ts
|
|
1402
|
+
async function getThread(client, id) {
|
|
1403
|
+
validateId(id, "threadId");
|
|
1404
|
+
const data = await client.request(QUERY_THREAD_BY_ID, { id });
|
|
1405
|
+
return data.Thread;
|
|
1406
|
+
}
|
|
1407
|
+
async function getRecentThreads(client, options = {}) {
|
|
1408
|
+
const { query: search, page = 1, perPage = 20, sort, mediaId, categoryId } = options;
|
|
1409
|
+
return client.pagedRequest(
|
|
1410
|
+
QUERY_THREAD_SEARCH,
|
|
1411
|
+
{
|
|
1412
|
+
search,
|
|
1413
|
+
mediaCategoryId: mediaId,
|
|
1414
|
+
categoryId,
|
|
1415
|
+
sort: sort ?? ["REPLIED_AT_DESC" /* REPLIED_AT_DESC */],
|
|
1416
|
+
page,
|
|
1417
|
+
perPage: clampPerPage(perPage)
|
|
1418
|
+
},
|
|
1419
|
+
"threads"
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/client/user.ts
|
|
1424
|
+
async function getUser(client, idOrName) {
|
|
1425
|
+
if (typeof idOrName === "number") {
|
|
1426
|
+
validateId(idOrName, "userId");
|
|
1427
|
+
const data2 = await client.request(QUERY_USER_BY_ID, { id: idOrName });
|
|
1428
|
+
return data2.User;
|
|
1429
|
+
}
|
|
1430
|
+
const data = await client.request(QUERY_USER_BY_NAME, { name: idOrName });
|
|
1431
|
+
return data.User;
|
|
1432
|
+
}
|
|
1433
|
+
async function searchUsers(client, options = {}) {
|
|
1434
|
+
const { query: search, page = 1, perPage = 20, sort } = options;
|
|
1435
|
+
return client.pagedRequest(QUERY_USER_SEARCH, { search, sort, page, perPage: clampPerPage(perPage) }, "users");
|
|
1436
|
+
}
|
|
1437
|
+
async function getUserMediaList(client, options) {
|
|
1438
|
+
if (!options.userId && !options.userName) {
|
|
1439
|
+
throw new AniListError("getUserMediaList requires either userId or userName", 0, []);
|
|
1440
|
+
}
|
|
1441
|
+
return client.pagedRequest(
|
|
1442
|
+
QUERY_USER_MEDIA_LIST,
|
|
1443
|
+
{
|
|
1444
|
+
userId: options.userId,
|
|
1445
|
+
userName: options.userName,
|
|
1446
|
+
type: options.type,
|
|
1447
|
+
status: options.status,
|
|
1448
|
+
sort: options.sort,
|
|
1449
|
+
page: options.page ?? 1,
|
|
1450
|
+
perPage: clampPerPage(options.perPage ?? 20)
|
|
1451
|
+
},
|
|
1452
|
+
"mediaList"
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
async function getUserFavorites(client, idOrName) {
|
|
1456
|
+
if (typeof idOrName === "number") {
|
|
1457
|
+
validateId(idOrName, "userId");
|
|
1458
|
+
const data2 = await client.request(QUERY_USER_FAVORITES_BY_ID, {
|
|
1459
|
+
id: idOrName
|
|
1460
|
+
});
|
|
1461
|
+
return mapFavorites(data2.User.favourites);
|
|
1462
|
+
}
|
|
1463
|
+
const data = await client.request(QUERY_USER_FAVORITES_BY_NAME, {
|
|
1464
|
+
name: idOrName
|
|
1465
|
+
});
|
|
1466
|
+
return mapFavorites(data.User.favourites);
|
|
1467
|
+
}
|
|
1468
|
+
function mapFavorites(fav) {
|
|
1469
|
+
return {
|
|
1470
|
+
anime: fav.anime?.nodes ?? [],
|
|
1471
|
+
manga: fav.manga?.nodes ?? [],
|
|
1472
|
+
characters: fav.characters?.nodes ?? [],
|
|
1473
|
+
staff: fav.staff?.nodes ?? [],
|
|
1474
|
+
studios: fav.studios?.nodes ?? []
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1006
1478
|
// src/client/index.ts
|
|
1007
1479
|
var DEFAULT_API_URL = "https://graphql.anilist.co";
|
|
1008
1480
|
var AniListClient = class {
|
|
@@ -1019,15 +1491,31 @@ var AniListClient = class {
|
|
|
1019
1491
|
this.cacheAdapter = options.cacheAdapter ?? new MemoryCache(options.cache);
|
|
1020
1492
|
this.rateLimiter = new RateLimiter(options.rateLimit);
|
|
1021
1493
|
this.hooks = options.hooks ?? {};
|
|
1494
|
+
this.signal = options.signal;
|
|
1022
1495
|
}
|
|
1023
1496
|
/**
|
|
1024
|
-
*
|
|
1497
|
+
* The current rate limit information from the last API response.
|
|
1498
|
+
* Updated after every non-cached request.
|
|
1499
|
+
*/
|
|
1500
|
+
get rateLimitInfo() {
|
|
1501
|
+
return this._rateLimitInfo;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Metadata about the last request (duration, cache status, rate limit info).
|
|
1505
|
+
* Useful for debugging and monitoring.
|
|
1025
1506
|
*/
|
|
1507
|
+
get lastRequestMeta() {
|
|
1508
|
+
return this._lastRequestMeta;
|
|
1509
|
+
}
|
|
1510
|
+
// ── Core infrastructure (internal) ──
|
|
1511
|
+
/** @internal */
|
|
1026
1512
|
async request(query, variables = {}) {
|
|
1027
1513
|
const cacheKey = MemoryCache.key(query, variables);
|
|
1028
1514
|
const cached = await this.cacheAdapter.get(cacheKey);
|
|
1029
1515
|
if (cached !== void 0) {
|
|
1030
1516
|
this.hooks.onCacheHit?.(cacheKey);
|
|
1517
|
+
const meta = { durationMs: 0, fromCache: true };
|
|
1518
|
+
this._lastRequestMeta = meta;
|
|
1031
1519
|
this.hooks.onResponse?.(query, 0, true);
|
|
1032
1520
|
return cached;
|
|
1033
1521
|
}
|
|
@@ -1051,7 +1539,8 @@ var AniListClient = class {
|
|
|
1051
1539
|
{
|
|
1052
1540
|
method: "POST",
|
|
1053
1541
|
headers: this.headers,
|
|
1054
|
-
body: JSON.stringify({ query: minifiedQuery, variables })
|
|
1542
|
+
body: JSON.stringify({ query: minifiedQuery, variables }),
|
|
1543
|
+
signal: this.signal
|
|
1055
1544
|
},
|
|
1056
1545
|
{ onRetry: this.hooks.onRetry, onRateLimit: this.hooks.onRateLimit }
|
|
1057
1546
|
);
|
|
@@ -1060,15 +1549,25 @@ var AniListClient = class {
|
|
|
1060
1549
|
const message = json.errors?.[0]?.message ?? `AniList API error (HTTP ${res.status})`;
|
|
1061
1550
|
throw new AniListError(message, res.status, json.errors ?? []);
|
|
1062
1551
|
}
|
|
1552
|
+
const rlLimit = res.headers.get("X-RateLimit-Limit");
|
|
1553
|
+
const rlRemaining = res.headers.get("X-RateLimit-Remaining");
|
|
1554
|
+
const rlReset = res.headers.get("X-RateLimit-Reset");
|
|
1555
|
+
if (rlLimit && rlRemaining && rlReset) {
|
|
1556
|
+
this._rateLimitInfo = {
|
|
1557
|
+
limit: Number.parseInt(rlLimit, 10),
|
|
1558
|
+
remaining: Number.parseInt(rlRemaining, 10),
|
|
1559
|
+
reset: Number.parseInt(rlReset, 10)
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
const durationMs = Date.now() - start;
|
|
1063
1563
|
const data = json.data;
|
|
1064
1564
|
await this.cacheAdapter.set(cacheKey, data);
|
|
1065
|
-
|
|
1565
|
+
const meta = { durationMs, fromCache: false, rateLimitInfo: this._rateLimitInfo };
|
|
1566
|
+
this._lastRequestMeta = meta;
|
|
1567
|
+
this.hooks.onResponse?.(query, durationMs, false, this._rateLimitInfo);
|
|
1066
1568
|
return data;
|
|
1067
1569
|
}
|
|
1068
|
-
/**
|
|
1069
|
-
* @internal
|
|
1070
|
-
* Shorthand for paginated queries that follow the `Page { pageInfo, <field>[] }` pattern.
|
|
1071
|
-
*/
|
|
1570
|
+
/** @internal */
|
|
1072
1571
|
async pagedRequest(query, variables, field) {
|
|
1073
1572
|
const data = await this.request(query, variables);
|
|
1074
1573
|
const results = data.Page[field];
|
|
@@ -1077,6 +1576,7 @@ var AniListClient = class {
|
|
|
1077
1576
|
}
|
|
1078
1577
|
return { pageInfo: data.Page.pageInfo, results };
|
|
1079
1578
|
}
|
|
1579
|
+
// ── Media ──
|
|
1080
1580
|
/**
|
|
1081
1581
|
* Fetch a single media entry by its AniList ID.
|
|
1082
1582
|
*
|
|
@@ -1084,514 +1584,145 @@ var AniListClient = class {
|
|
|
1084
1584
|
*
|
|
1085
1585
|
* @param id - The AniList media ID
|
|
1086
1586
|
* @param include - Optional related data to include
|
|
1087
|
-
* @returns The media object
|
|
1088
|
-
*
|
|
1089
|
-
* @example
|
|
1090
|
-
* ```ts
|
|
1091
|
-
* // Basic usage — same as before (includes relations by default)
|
|
1092
|
-
* const anime = await client.getMedia(1);
|
|
1093
|
-
*
|
|
1094
|
-
* // Include characters sorted by role, 25 results
|
|
1095
|
-
* const anime = await client.getMedia(1, { characters: true });
|
|
1096
|
-
*
|
|
1097
|
-
* // Include characters with voice actors
|
|
1098
|
-
* const anime = await client.getMedia(1, { characters: { voiceActors: true } });
|
|
1099
|
-
*
|
|
1100
|
-
* // Full control
|
|
1101
|
-
* const anime = await client.getMedia(1, {
|
|
1102
|
-
* characters: { perPage: 50, sort: true },
|
|
1103
|
-
* staff: true,
|
|
1104
|
-
* relations: true,
|
|
1105
|
-
* streamingEpisodes: true,
|
|
1106
|
-
* externalLinks: true,
|
|
1107
|
-
* stats: true,
|
|
1108
|
-
* recommendations: { perPage: 5 },
|
|
1109
|
-
* });
|
|
1110
|
-
*
|
|
1111
|
-
* // Exclude relations for a lighter response
|
|
1112
|
-
* const anime = await client.getMedia(1, { characters: true, relations: false });
|
|
1113
|
-
* ```
|
|
1114
1587
|
*/
|
|
1115
1588
|
async getMedia(id, include) {
|
|
1116
|
-
|
|
1117
|
-
const data = await this.request(query, { id });
|
|
1118
|
-
return data.Media;
|
|
1589
|
+
return getMedia(this, id, include);
|
|
1119
1590
|
}
|
|
1120
1591
|
/**
|
|
1121
1592
|
* Search for anime or manga.
|
|
1122
1593
|
*
|
|
1123
1594
|
* @param options - Search / filter parameters
|
|
1124
1595
|
* @returns Paginated results with matching media
|
|
1125
|
-
*
|
|
1126
|
-
* @example
|
|
1127
|
-
* ```ts
|
|
1128
|
-
* const results = await client.searchMedia({
|
|
1129
|
-
* query: "Naruto",
|
|
1130
|
-
* type: MediaType.ANIME,
|
|
1131
|
-
* perPage: 5,
|
|
1132
|
-
* });
|
|
1133
|
-
* ```
|
|
1134
1596
|
*/
|
|
1135
1597
|
async searchMedia(options = {}) {
|
|
1136
|
-
|
|
1137
|
-
return this.pagedRequest(
|
|
1138
|
-
QUERY_MEDIA_SEARCH,
|
|
1139
|
-
{
|
|
1140
|
-
search,
|
|
1141
|
-
...filters,
|
|
1142
|
-
genre_in: genres,
|
|
1143
|
-
tag_in: tags,
|
|
1144
|
-
genre_not_in: genresExclude,
|
|
1145
|
-
tag_not_in: tagsExclude,
|
|
1146
|
-
page,
|
|
1147
|
-
perPage: clampPerPage(perPage)
|
|
1148
|
-
},
|
|
1149
|
-
"media"
|
|
1150
|
-
);
|
|
1598
|
+
return searchMedia(this, options);
|
|
1151
1599
|
}
|
|
1152
|
-
/**
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
* @param type - `MediaType.ANIME` or `MediaType.MANGA` (defaults to ANIME)
|
|
1156
|
-
* @param page - Page number (default 1)
|
|
1157
|
-
* @param perPage - Results per page (default 20, max 50)
|
|
1158
|
-
*/
|
|
1159
|
-
async getTrending(type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1160
|
-
return this.pagedRequest(QUERY_TRENDING, { type, page, perPage: clampPerPage(perPage) }, "media");
|
|
1600
|
+
/** Get currently trending anime or manga. */
|
|
1601
|
+
async getTrending(type, page, perPage) {
|
|
1602
|
+
return getTrending(this, type, page, perPage);
|
|
1161
1603
|
}
|
|
1162
|
-
/**
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
* Convenience wrapper around `searchMedia` with `sort: POPULARITY_DESC`.
|
|
1166
|
-
*
|
|
1167
|
-
* @param type - `MediaType.ANIME` or `MediaType.MANGA` (defaults to ANIME)
|
|
1168
|
-
* @param page - Page number (default 1)
|
|
1169
|
-
* @param perPage - Results per page (default 20, max 50)
|
|
1170
|
-
*/
|
|
1171
|
-
async getPopular(type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1172
|
-
return this.searchMedia({ type, sort: ["POPULARITY_DESC" /* POPULARITY_DESC */], page, perPage });
|
|
1604
|
+
/** Get the most popular anime or manga. */
|
|
1605
|
+
async getPopular(type, page, perPage) {
|
|
1606
|
+
return getPopular(this, type, page, perPage);
|
|
1173
1607
|
}
|
|
1174
|
-
/**
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
* Convenience wrapper around `searchMedia` with `sort: SCORE_DESC`.
|
|
1178
|
-
*
|
|
1179
|
-
* @param type - `MediaType.ANIME` or `MediaType.MANGA` (defaults to ANIME)
|
|
1180
|
-
* @param page - Page number (default 1)
|
|
1181
|
-
* @param perPage - Results per page (default 20, max 50)
|
|
1182
|
-
*/
|
|
1183
|
-
async getTopRated(type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
|
|
1184
|
-
return this.searchMedia({ type, sort: ["SCORE_DESC" /* SCORE_DESC */], page, perPage });
|
|
1608
|
+
/** Get the highest-rated anime or manga. */
|
|
1609
|
+
async getTopRated(type, page, perPage) {
|
|
1610
|
+
return getTopRated(this, type, page, perPage);
|
|
1185
1611
|
}
|
|
1186
|
-
/**
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1612
|
+
/** Get recently aired anime episodes. */
|
|
1613
|
+
async getAiredEpisodes(options = {}) {
|
|
1614
|
+
return getAiredEpisodes(this, options);
|
|
1615
|
+
}
|
|
1616
|
+
/** Get currently releasing manga. */
|
|
1617
|
+
async getAiredChapters(options = {}) {
|
|
1618
|
+
return getAiredChapters(this, options);
|
|
1619
|
+
}
|
|
1620
|
+
/** Get the detailed schedule for the current week, sorted by day. */
|
|
1621
|
+
async getWeeklySchedule(date) {
|
|
1622
|
+
return getWeeklySchedule(this, date);
|
|
1623
|
+
}
|
|
1624
|
+
/** Get upcoming (not yet released) media. */
|
|
1625
|
+
async getPlanning(options = {}) {
|
|
1626
|
+
return getPlanning(this, options);
|
|
1627
|
+
}
|
|
1628
|
+
/** Get recommendations for a specific media. */
|
|
1629
|
+
async getRecommendations(mediaId, options = {}) {
|
|
1630
|
+
return getRecommendations(this, mediaId, options);
|
|
1631
|
+
}
|
|
1632
|
+
/** Get anime (or manga) for a specific season and year. */
|
|
1633
|
+
async getMediaBySeason(options) {
|
|
1634
|
+
return getMediaBySeason(this, options);
|
|
1635
|
+
}
|
|
1636
|
+
// ── Characters ──
|
|
1637
|
+
/** Fetch a character by AniList ID. Pass `{ voiceActors: true }` to include VA data. */
|
|
1206
1638
|
async getCharacter(id, include) {
|
|
1207
|
-
|
|
1208
|
-
const data = await this.request(query, { id });
|
|
1209
|
-
return data.Character;
|
|
1639
|
+
return getCharacter(this, id, include);
|
|
1210
1640
|
}
|
|
1211
|
-
/**
|
|
1212
|
-
* Search for characters by name.
|
|
1213
|
-
*
|
|
1214
|
-
* @param options - Search / pagination parameters (includes optional `voiceActors`)
|
|
1215
|
-
* @returns Paginated results with matching characters
|
|
1216
|
-
*
|
|
1217
|
-
* @example
|
|
1218
|
-
* ```ts
|
|
1219
|
-
* const result = await client.searchCharacters({ query: "Luffy", perPage: 5 });
|
|
1220
|
-
*
|
|
1221
|
-
* // With voice actors
|
|
1222
|
-
* const result = await client.searchCharacters({ query: "Luffy", voiceActors: true });
|
|
1223
|
-
* ```
|
|
1224
|
-
*/
|
|
1641
|
+
/** Search for characters by name. */
|
|
1225
1642
|
async searchCharacters(options = {}) {
|
|
1226
|
-
|
|
1227
|
-
const gqlQuery = voiceActors ? QUERY_CHARACTER_SEARCH_WITH_VA : QUERY_CHARACTER_SEARCH;
|
|
1228
|
-
return this.pagedRequest(
|
|
1229
|
-
gqlQuery,
|
|
1230
|
-
{ search, ...rest, page, perPage: clampPerPage(perPage) },
|
|
1231
|
-
"characters"
|
|
1232
|
-
);
|
|
1643
|
+
return searchCharacters(this, options);
|
|
1233
1644
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
*
|
|
1237
|
-
* @param id - The AniList staff ID
|
|
1238
|
-
* @param include - Optional include options to fetch related data (e.g. media)
|
|
1239
|
-
* @returns The staff object
|
|
1240
|
-
*
|
|
1241
|
-
* @example
|
|
1242
|
-
* ```ts
|
|
1243
|
-
* const staff = await client.getStaff(95001);
|
|
1244
|
-
* console.log(staff.name.full);
|
|
1245
|
-
*
|
|
1246
|
-
* // With media the staff worked on
|
|
1247
|
-
* const staff = await client.getStaff(95001, { media: true });
|
|
1248
|
-
* staff.staffMedia?.nodes.forEach((m) => console.log(m.title.romaji));
|
|
1249
|
-
* ```
|
|
1250
|
-
*/
|
|
1645
|
+
// ── Staff ──
|
|
1646
|
+
/** Fetch a staff member by AniList ID. Pass `{ media: true }` or `{ media: { perPage } }` for media credits. */
|
|
1251
1647
|
async getStaff(id, include) {
|
|
1252
|
-
|
|
1253
|
-
const opts = typeof include.media === "object" ? include.media : {};
|
|
1254
|
-
const perPage = opts.perPage ?? 25;
|
|
1255
|
-
const data2 = await this.request(QUERY_STAFF_BY_ID_WITH_MEDIA, { id, perPage });
|
|
1256
|
-
return data2.Staff;
|
|
1257
|
-
}
|
|
1258
|
-
const data = await this.request(QUERY_STAFF_BY_ID, { id });
|
|
1259
|
-
return data.Staff;
|
|
1648
|
+
return getStaff(this, id, include);
|
|
1260
1649
|
}
|
|
1261
|
-
/**
|
|
1262
|
-
* Search for staff (voice actors, directors, etc.).
|
|
1263
|
-
*
|
|
1264
|
-
* @param options - Search / pagination parameters
|
|
1265
|
-
* @returns Paginated results with matching staff
|
|
1266
|
-
*
|
|
1267
|
-
* @example
|
|
1268
|
-
* ```ts
|
|
1269
|
-
* const result = await client.searchStaff({ query: "Miyazaki", perPage: 5 });
|
|
1270
|
-
* ```
|
|
1271
|
-
*/
|
|
1650
|
+
/** Search for staff (voice actors, directors, etc.). */
|
|
1272
1651
|
async searchStaff(options = {}) {
|
|
1273
|
-
|
|
1274
|
-
return this.pagedRequest(
|
|
1275
|
-
QUERY_STAFF_SEARCH,
|
|
1276
|
-
{ search, sort, page, perPage: clampPerPage(perPage) },
|
|
1277
|
-
"staff"
|
|
1278
|
-
);
|
|
1652
|
+
return searchStaff(this, options);
|
|
1279
1653
|
}
|
|
1654
|
+
// ── Users ──
|
|
1280
1655
|
/**
|
|
1281
1656
|
* Fetch a user by AniList ID or username.
|
|
1282
1657
|
*
|
|
1283
1658
|
* @param idOrName - The AniList user ID (number) or username (string)
|
|
1284
|
-
* @returns The user object
|
|
1285
|
-
*
|
|
1286
|
-
* @example
|
|
1287
|
-
* ```ts
|
|
1288
|
-
* const user = await client.getUser(1);
|
|
1289
|
-
* const user2 = await client.getUser("AniList");
|
|
1290
|
-
* console.log(user.name);
|
|
1291
|
-
* ```
|
|
1292
1659
|
*/
|
|
1293
1660
|
async getUser(idOrName) {
|
|
1294
|
-
|
|
1295
|
-
const data2 = await this.request(QUERY_USER_BY_ID, { id: idOrName });
|
|
1296
|
-
return data2.User;
|
|
1297
|
-
}
|
|
1298
|
-
const data = await this.request(QUERY_USER_BY_NAME, { name: idOrName });
|
|
1299
|
-
return data.User;
|
|
1300
|
-
}
|
|
1301
|
-
/**
|
|
1302
|
-
* Fetch a user by username.
|
|
1303
|
-
*
|
|
1304
|
-
* @deprecated Use `getUser(name)` instead.
|
|
1305
|
-
* @param name - The AniList username
|
|
1306
|
-
* @returns The user object
|
|
1307
|
-
*/
|
|
1308
|
-
async getUserByName(name) {
|
|
1309
|
-
return this.getUser(name);
|
|
1661
|
+
return getUser(this, idOrName);
|
|
1310
1662
|
}
|
|
1311
|
-
/**
|
|
1312
|
-
* Search for users by name.
|
|
1313
|
-
*
|
|
1314
|
-
* @param options - Search / pagination parameters
|
|
1315
|
-
* @returns Paginated results with matching users
|
|
1316
|
-
*
|
|
1317
|
-
* @example
|
|
1318
|
-
* ```ts
|
|
1319
|
-
* const result = await client.searchUsers({ query: "AniList", perPage: 5 });
|
|
1320
|
-
* ```
|
|
1321
|
-
*/
|
|
1663
|
+
/** Search for users by name. */
|
|
1322
1664
|
async searchUsers(options = {}) {
|
|
1323
|
-
|
|
1324
|
-
return this.pagedRequest(QUERY_USER_SEARCH, { search, sort, page, perPage: clampPerPage(perPage) }, "users");
|
|
1325
|
-
}
|
|
1326
|
-
/**
|
|
1327
|
-
* Execute an arbitrary GraphQL query against the AniList API.
|
|
1328
|
-
* Useful for advanced use-cases not covered by the built-in methods.
|
|
1329
|
-
*
|
|
1330
|
-
* @param query - A valid GraphQL query string
|
|
1331
|
-
* @param variables - Optional variables object
|
|
1332
|
-
*/
|
|
1333
|
-
async raw(query, variables) {
|
|
1334
|
-
return this.request(query, variables);
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Get recently aired anime episodes.
|
|
1338
|
-
*
|
|
1339
|
-
* By default returns episodes that aired in the last 24 hours.
|
|
1340
|
-
*
|
|
1341
|
-
* @param options - Filter / pagination parameters
|
|
1342
|
-
* @returns Paginated list of airing schedule entries
|
|
1343
|
-
*
|
|
1344
|
-
* @example
|
|
1345
|
-
* ```ts
|
|
1346
|
-
* // Episodes that aired in the last 48h
|
|
1347
|
-
* const recent = await client.getAiredEpisodes({
|
|
1348
|
-
* airingAtGreater: Math.floor(Date.now() / 1000) - 48 * 3600,
|
|
1349
|
-
* });
|
|
1350
|
-
* ```
|
|
1351
|
-
*/
|
|
1352
|
-
async getAiredEpisodes(options = {}) {
|
|
1353
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1354
|
-
const variables = {
|
|
1355
|
-
airingAt_greater: options.airingAtGreater ?? now - 24 * 3600,
|
|
1356
|
-
airingAt_lesser: options.airingAtLesser ?? now,
|
|
1357
|
-
sort: options.sort ?? ["TIME_DESC"],
|
|
1358
|
-
page: options.page ?? 1,
|
|
1359
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1360
|
-
};
|
|
1361
|
-
return this.pagedRequest(QUERY_AIRING_SCHEDULE, variables, "airingSchedules");
|
|
1362
|
-
}
|
|
1363
|
-
/**
|
|
1364
|
-
* Get manga that are currently releasing, sorted by most recently updated.
|
|
1365
|
-
*
|
|
1366
|
-
* This is the closest equivalent to "recently released chapters" on AniList,
|
|
1367
|
-
* since the API does not expose per-chapter airing schedules for manga.
|
|
1368
|
-
*
|
|
1369
|
-
* @param options - Pagination parameters
|
|
1370
|
-
* @returns Paginated list of currently releasing manga
|
|
1371
|
-
*
|
|
1372
|
-
* @example
|
|
1373
|
-
* ```ts
|
|
1374
|
-
* const chapters = await client.getAiredChapters({ perPage: 10 });
|
|
1375
|
-
* ```
|
|
1376
|
-
*/
|
|
1377
|
-
async getAiredChapters(options = {}) {
|
|
1378
|
-
return this.pagedRequest(
|
|
1379
|
-
QUERY_RECENT_CHAPTERS,
|
|
1380
|
-
{
|
|
1381
|
-
page: options.page ?? 1,
|
|
1382
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1383
|
-
},
|
|
1384
|
-
"media"
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1387
|
-
/**
|
|
1388
|
-
* Get upcoming (not yet released) anime and/or manga, sorted by popularity.
|
|
1389
|
-
*
|
|
1390
|
-
* @param options - Filter / pagination parameters
|
|
1391
|
-
* @returns Paginated list of planned media
|
|
1392
|
-
*
|
|
1393
|
-
* @example
|
|
1394
|
-
* ```ts
|
|
1395
|
-
* import { MediaType } from "ani-client";
|
|
1396
|
-
*
|
|
1397
|
-
* // Most anticipated upcoming anime
|
|
1398
|
-
* const planning = await client.getPlanning({ type: MediaType.ANIME, perPage: 10 });
|
|
1399
|
-
* ```
|
|
1400
|
-
*/
|
|
1401
|
-
async getPlanning(options = {}) {
|
|
1402
|
-
return this.pagedRequest(
|
|
1403
|
-
QUERY_PLANNING,
|
|
1404
|
-
{
|
|
1405
|
-
type: options.type,
|
|
1406
|
-
sort: options.sort ?? ["POPULARITY_DESC"],
|
|
1407
|
-
page: options.page ?? 1,
|
|
1408
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1409
|
-
},
|
|
1410
|
-
"media"
|
|
1411
|
-
);
|
|
1412
|
-
}
|
|
1413
|
-
/**
|
|
1414
|
-
* Get recommendations for a specific media.
|
|
1415
|
-
*
|
|
1416
|
-
* Returns other anime/manga that users have recommended based on the given media.
|
|
1417
|
-
*
|
|
1418
|
-
* @param mediaId - The AniList media ID
|
|
1419
|
-
* @param options - Optional sort / pagination parameters
|
|
1420
|
-
* @returns Paginated list of recommendations
|
|
1421
|
-
*
|
|
1422
|
-
* @example
|
|
1423
|
-
* ```ts
|
|
1424
|
-
* // Get recommendations for Cowboy Bebop
|
|
1425
|
-
* const recs = await client.getRecommendations(1);
|
|
1426
|
-
* recs.results.forEach((r) =>
|
|
1427
|
-
* console.log(`${r.mediaRecommendation.title.romaji} (rating: ${r.rating})`)
|
|
1428
|
-
* );
|
|
1429
|
-
* ```
|
|
1430
|
-
*/
|
|
1431
|
-
async getRecommendations(mediaId, options = {}) {
|
|
1432
|
-
const variables = {
|
|
1433
|
-
mediaId,
|
|
1434
|
-
sort: options.sort ?? ["RATING_DESC"],
|
|
1435
|
-
page: options.page ?? 1,
|
|
1436
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1437
|
-
};
|
|
1438
|
-
const data = await this.request(QUERY_RECOMMENDATIONS, variables);
|
|
1439
|
-
return {
|
|
1440
|
-
pageInfo: data.Media.recommendations.pageInfo,
|
|
1441
|
-
results: data.Media.recommendations.nodes
|
|
1442
|
-
};
|
|
1665
|
+
return searchUsers(this, options);
|
|
1443
1666
|
}
|
|
1444
|
-
/**
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
* @param options - Season, year and optional filter / pagination parameters
|
|
1448
|
-
* @returns Paginated list of media for the given season
|
|
1449
|
-
*
|
|
1450
|
-
* @example
|
|
1451
|
-
* ```ts
|
|
1452
|
-
* import { MediaSeason } from "ani-client";
|
|
1453
|
-
*
|
|
1454
|
-
* const winter2026 = await client.getMediaBySeason({
|
|
1455
|
-
* season: MediaSeason.WINTER,
|
|
1456
|
-
* seasonYear: 2026,
|
|
1457
|
-
* perPage: 10,
|
|
1458
|
-
* });
|
|
1459
|
-
* ```
|
|
1460
|
-
*/
|
|
1461
|
-
async getMediaBySeason(options) {
|
|
1462
|
-
return this.pagedRequest(
|
|
1463
|
-
QUERY_MEDIA_BY_SEASON,
|
|
1464
|
-
{
|
|
1465
|
-
season: options.season,
|
|
1466
|
-
seasonYear: options.seasonYear,
|
|
1467
|
-
type: options.type ?? "ANIME",
|
|
1468
|
-
sort: options.sort ?? ["POPULARITY_DESC"],
|
|
1469
|
-
page: options.page ?? 1,
|
|
1470
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1471
|
-
},
|
|
1472
|
-
"media"
|
|
1473
|
-
);
|
|
1667
|
+
/** Get a user's anime or manga list. */
|
|
1668
|
+
async getUserMediaList(options) {
|
|
1669
|
+
return getUserMediaList(this, options);
|
|
1474
1670
|
}
|
|
1475
1671
|
/**
|
|
1476
|
-
*
|
|
1672
|
+
* Fetch a user's favorite anime, manga, characters, staff, and studios.
|
|
1477
1673
|
*
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
* @param options - User identifier, media type, and optional filters
|
|
1482
|
-
* @returns Paginated list of media list entries
|
|
1674
|
+
* @param idOrName - AniList user ID (number) or username (string)
|
|
1675
|
+
* @returns The user's favorites grouped by category
|
|
1483
1676
|
*
|
|
1484
1677
|
* @example
|
|
1485
|
-
* ```
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1488
|
-
* // Get a user's completed anime list
|
|
1489
|
-
* const list = await client.getUserMediaList({
|
|
1490
|
-
* userName: "AniList",
|
|
1491
|
-
* type: MediaType.ANIME,
|
|
1492
|
-
* status: MediaListStatus.COMPLETED,
|
|
1493
|
-
* });
|
|
1494
|
-
* list.results.forEach((entry) =>
|
|
1495
|
-
* console.log(`${entry.media.title.romaji} — ${entry.score}/100`)
|
|
1496
|
-
* );
|
|
1678
|
+
* ```typescript
|
|
1679
|
+
* const favs = await client.getUserFavorites("AniList");
|
|
1680
|
+
* favs.anime.forEach(a => console.log(a.title.romaji));
|
|
1497
1681
|
* ```
|
|
1498
1682
|
*/
|
|
1499
|
-
async
|
|
1500
|
-
|
|
1501
|
-
throw new Error("Either userId or userName must be provided");
|
|
1502
|
-
}
|
|
1503
|
-
return this.pagedRequest(
|
|
1504
|
-
QUERY_USER_MEDIA_LIST,
|
|
1505
|
-
{
|
|
1506
|
-
userId: options.userId,
|
|
1507
|
-
userName: options.userName,
|
|
1508
|
-
type: options.type,
|
|
1509
|
-
status: options.status,
|
|
1510
|
-
sort: options.sort,
|
|
1511
|
-
page: options.page ?? 1,
|
|
1512
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1513
|
-
},
|
|
1514
|
-
"mediaList"
|
|
1515
|
-
);
|
|
1683
|
+
async getUserFavorites(idOrName) {
|
|
1684
|
+
return getUserFavorites(this, idOrName);
|
|
1516
1685
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
*
|
|
1520
|
-
* Returns studio details along with its most popular productions.
|
|
1521
|
-
*
|
|
1522
|
-
* @param id - The AniList studio ID
|
|
1523
|
-
*/
|
|
1686
|
+
// ── Studios ──
|
|
1687
|
+
/** Fetch a studio by its AniList ID. */
|
|
1524
1688
|
async getStudio(id) {
|
|
1525
|
-
|
|
1526
|
-
return data.Studio;
|
|
1689
|
+
return getStudio(this, id);
|
|
1527
1690
|
}
|
|
1528
|
-
/**
|
|
1529
|
-
* Search for studios by name.
|
|
1530
|
-
*
|
|
1531
|
-
* @param options - Search / pagination parameters
|
|
1532
|
-
* @returns Paginated list of studios
|
|
1533
|
-
*
|
|
1534
|
-
* @example
|
|
1535
|
-
* ```ts
|
|
1536
|
-
* const studios = await client.searchStudios({ query: "MAPPA" });
|
|
1537
|
-
* ```
|
|
1538
|
-
*/
|
|
1691
|
+
/** Search for studios by name. */
|
|
1539
1692
|
async searchStudios(options = {}) {
|
|
1540
|
-
return this
|
|
1541
|
-
QUERY_STUDIO_SEARCH,
|
|
1542
|
-
{
|
|
1543
|
-
search: options.query,
|
|
1544
|
-
page: options.page ?? 1,
|
|
1545
|
-
perPage: clampPerPage(options.perPage ?? 20)
|
|
1546
|
-
},
|
|
1547
|
-
"studios"
|
|
1548
|
-
);
|
|
1693
|
+
return searchStudios(this, options);
|
|
1549
1694
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1695
|
+
// ── Metadata ──
|
|
1696
|
+
// ── Threads ──
|
|
1697
|
+
/** Fetch a forum thread by its AniList ID. */
|
|
1698
|
+
async getThread(id) {
|
|
1699
|
+
return getThread(this, id);
|
|
1700
|
+
}
|
|
1701
|
+
/** Get recent forum threads, optionally filtered by search, media, or category. */
|
|
1702
|
+
async getRecentThreads(options = {}) {
|
|
1703
|
+
return getRecentThreads(this, options);
|
|
1704
|
+
}
|
|
1705
|
+
/** Get all available genres on AniList. */
|
|
1555
1706
|
async getGenres() {
|
|
1556
1707
|
const data = await this.request(QUERY_GENRES);
|
|
1557
1708
|
return data.GenreCollection;
|
|
1558
1709
|
}
|
|
1559
|
-
/**
|
|
1560
|
-
* Get all available media tags on AniList.
|
|
1561
|
-
*
|
|
1562
|
-
* @returns Array of tag objects with id, name, description, category, isAdult
|
|
1563
|
-
*/
|
|
1710
|
+
/** Get all available media tags on AniList. */
|
|
1564
1711
|
async getTags() {
|
|
1565
1712
|
const data = await this.request(QUERY_TAGS);
|
|
1566
1713
|
return data.MediaTagCollection;
|
|
1567
1714
|
}
|
|
1715
|
+
// ── Raw query ──
|
|
1716
|
+
/** Execute an arbitrary GraphQL query against the AniList API. */
|
|
1717
|
+
async raw(query, variables) {
|
|
1718
|
+
return this.request(query, variables ?? {});
|
|
1719
|
+
}
|
|
1720
|
+
// ── Pagination ──
|
|
1568
1721
|
/**
|
|
1569
|
-
* Auto-paginating async iterator.
|
|
1570
|
-
*
|
|
1571
|
-
* Wraps any paginated method and yields individual items across all pages.
|
|
1572
|
-
* Stops when `hasNextPage` is `false` or `maxPages` is reached.
|
|
1722
|
+
* Auto-paginating async iterator. Yields individual items across all pages.
|
|
1573
1723
|
*
|
|
1574
1724
|
* @param fetchPage - A function that takes a page number and returns a `PagedResult<T>`
|
|
1575
1725
|
* @param maxPages - Maximum number of pages to fetch (default: Infinity)
|
|
1576
|
-
* @returns An async iterable iterator of individual items
|
|
1577
|
-
*
|
|
1578
|
-
* @example
|
|
1579
|
-
* ```ts
|
|
1580
|
-
* // Iterate over all search results
|
|
1581
|
-
* for await (const anime of client.paginate((page) =>
|
|
1582
|
-
* client.searchMedia({ query: "Naruto", page, perPage: 10 })
|
|
1583
|
-
* )) {
|
|
1584
|
-
* console.log(anime.title.romaji);
|
|
1585
|
-
* }
|
|
1586
|
-
*
|
|
1587
|
-
* // Limit to 3 pages
|
|
1588
|
-
* for await (const anime of client.paginate(
|
|
1589
|
-
* (page) => client.getTrending(MediaType.ANIME, page, 20),
|
|
1590
|
-
* 3,
|
|
1591
|
-
* )) {
|
|
1592
|
-
* console.log(anime.title.romaji);
|
|
1593
|
-
* }
|
|
1594
|
-
* ```
|
|
1595
1726
|
*/
|
|
1596
1727
|
async *paginate(fetchPage, maxPages = Number.POSITIVE_INFINITY) {
|
|
1597
1728
|
let page = 1;
|
|
@@ -1606,71 +1737,49 @@ var AniListClient = class {
|
|
|
1606
1737
|
}
|
|
1607
1738
|
}
|
|
1608
1739
|
// ── Batch queries ──
|
|
1609
|
-
/**
|
|
1610
|
-
* Fetch multiple media entries in a single API request.
|
|
1611
|
-
* Uses GraphQL aliases to batch up to 50 IDs per call.
|
|
1612
|
-
*
|
|
1613
|
-
* @param ids - Array of AniList media IDs
|
|
1614
|
-
* @returns Array of media objects (same order as input IDs)
|
|
1615
|
-
*/
|
|
1740
|
+
/** Fetch multiple media entries in a single API request. */
|
|
1616
1741
|
async getMediaBatch(ids) {
|
|
1617
1742
|
if (ids.length === 0) return [];
|
|
1743
|
+
validateIds(ids, "mediaId");
|
|
1618
1744
|
if (ids.length === 1) return [await this.getMedia(ids[0])];
|
|
1619
1745
|
return this.executeBatch(ids, buildBatchMediaQuery, "m");
|
|
1620
1746
|
}
|
|
1621
|
-
/**
|
|
1622
|
-
* Fetch multiple characters in a single API request.
|
|
1623
|
-
*
|
|
1624
|
-
* @param ids - Array of AniList character IDs
|
|
1625
|
-
* @returns Array of character objects (same order as input IDs)
|
|
1626
|
-
*/
|
|
1747
|
+
/** Fetch multiple characters in a single API request. */
|
|
1627
1748
|
async getCharacterBatch(ids) {
|
|
1628
1749
|
if (ids.length === 0) return [];
|
|
1750
|
+
validateIds(ids, "characterId");
|
|
1629
1751
|
if (ids.length === 1) return [await this.getCharacter(ids[0])];
|
|
1630
1752
|
return this.executeBatch(ids, buildBatchCharacterQuery, "c");
|
|
1631
1753
|
}
|
|
1632
|
-
/**
|
|
1633
|
-
* Fetch multiple staff members in a single API request.
|
|
1634
|
-
*
|
|
1635
|
-
* @param ids - Array of AniList staff IDs
|
|
1636
|
-
* @returns Array of staff objects (same order as input IDs)
|
|
1637
|
-
*/
|
|
1754
|
+
/** Fetch multiple staff members in a single API request. */
|
|
1638
1755
|
async getStaffBatch(ids) {
|
|
1639
1756
|
if (ids.length === 0) return [];
|
|
1757
|
+
validateIds(ids, "staffId");
|
|
1640
1758
|
if (ids.length === 1) return [await this.getStaff(ids[0])];
|
|
1641
1759
|
return this.executeBatch(ids, buildBatchStaffQuery, "s");
|
|
1642
1760
|
}
|
|
1643
1761
|
/** @internal */
|
|
1644
1762
|
async executeBatch(ids, buildQuery, prefix) {
|
|
1645
1763
|
const chunks = chunk(ids, 50);
|
|
1646
|
-
const chunkResults =
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1764
|
+
const chunkResults = await Promise.all(
|
|
1765
|
+
chunks.map(async (idChunk) => {
|
|
1766
|
+
const query = buildQuery(idChunk);
|
|
1767
|
+
const data = await this.request(query);
|
|
1768
|
+
return idChunk.map((_, i) => data[`${prefix}${i}`]);
|
|
1769
|
+
})
|
|
1770
|
+
);
|
|
1652
1771
|
return chunkResults.flat();
|
|
1653
1772
|
}
|
|
1654
1773
|
// ── Cache management ──
|
|
1655
|
-
/**
|
|
1656
|
-
* Clear the entire response cache.
|
|
1657
|
-
*/
|
|
1774
|
+
/** Clear the entire response cache. */
|
|
1658
1775
|
async clearCache() {
|
|
1659
1776
|
await this.cacheAdapter.clear();
|
|
1660
1777
|
}
|
|
1661
|
-
/**
|
|
1662
|
-
* Number of entries currently in the cache.
|
|
1663
|
-
* For async adapters like Redis, this may return a Promise.
|
|
1664
|
-
*/
|
|
1778
|
+
/** Number of entries currently in the cache. */
|
|
1665
1779
|
get cacheSize() {
|
|
1666
1780
|
return this.cacheAdapter.size;
|
|
1667
1781
|
}
|
|
1668
|
-
/**
|
|
1669
|
-
* Remove cache entries whose key matches the given pattern.
|
|
1670
|
-
*
|
|
1671
|
-
* @param pattern — A string (converted to RegExp) or RegExp
|
|
1672
|
-
* @returns Number of entries removed
|
|
1673
|
-
*/
|
|
1782
|
+
/** Remove cache entries whose key matches the given pattern. */
|
|
1674
1783
|
async invalidateCache(pattern) {
|
|
1675
1784
|
if (this.cacheAdapter.invalidate) {
|
|
1676
1785
|
return this.cacheAdapter.invalidate(pattern);
|
|
@@ -1686,13 +1795,7 @@ var AniListClient = class {
|
|
|
1686
1795
|
}
|
|
1687
1796
|
return count;
|
|
1688
1797
|
}
|
|
1689
|
-
/**
|
|
1690
|
-
* Clean up resources held by the client.
|
|
1691
|
-
*
|
|
1692
|
-
* Clears the in-memory cache and aborts any pending in-flight requests.
|
|
1693
|
-
* If using a custom cache adapter (e.g. Redis), call its close/disconnect
|
|
1694
|
-
* method separately.
|
|
1695
|
-
*/
|
|
1798
|
+
/** Clean up resources held by the client. */
|
|
1696
1799
|
async destroy() {
|
|
1697
1800
|
await this.cacheAdapter.clear();
|
|
1698
1801
|
this.inFlight.clear();
|
|
@@ -1801,6 +1904,8 @@ exports.RateLimiter = RateLimiter;
|
|
|
1801
1904
|
exports.RecommendationSort = RecommendationSort;
|
|
1802
1905
|
exports.RedisCache = RedisCache;
|
|
1803
1906
|
exports.StaffSort = StaffSort;
|
|
1907
|
+
exports.ThreadSort = ThreadSort;
|
|
1804
1908
|
exports.UserSort = UserSort;
|
|
1909
|
+
exports.parseAniListMarkdown = parseAniListMarkdown;
|
|
1805
1910
|
//# sourceMappingURL=index.js.map
|
|
1806
1911
|
//# sourceMappingURL=index.js.map
|