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