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