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/dist/index.mjs CHANGED
@@ -1,4 +1,157 @@
1
- // src/queries/index.ts
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 STUDIO_FIELDS = `
429
- id
430
- name
431
- isAnimationStudio
432
- siteUrl
433
- favourites
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/utils/index.ts
578
- function normalizeQuery(query) {
579
- return query.replace(/\s+/g, " ").trim();
580
- }
581
- function clampPerPage(value) {
582
- return Math.min(Math.max(value, 1), 50);
583
- }
584
- function chunk(arr, size) {
585
- const chunks = [];
586
- for (let i = 0; i < arr.length; i += size) {
587
- chunks.push(arr.slice(i, i + size));
588
- }
589
- return chunks;
590
- }
591
-
592
- // src/cache/index.ts
593
- var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
594
- var MemoryCache = class {
595
- constructor(options = {}) {
596
- this.store = /* @__PURE__ */ new Map();
597
- this.ttl = options.ttl ?? ONE_DAY_MS;
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
- /** Remove a specific entry. */
630
- delete(key) {
631
- return this.store.delete(key);
829
+ replyUser {
830
+ id
831
+ name
832
+ avatar { large medium }
632
833
  }
633
- /** Clear the entire cache. */
634
- clear() {
635
- this.store.clear();
834
+ categories {
835
+ id
836
+ name
636
837
  }
637
- /** Number of entries currently stored. */
638
- get size() {
639
- return this.store.size;
838
+ mediaCategories {
839
+ id
840
+ title { romaji english native userPreferred }
841
+ type
842
+ coverImage { large medium }
843
+ siteUrl
640
844
  }
641
- /** Return all cache keys. */
642
- keys() {
643
- return [...this.store.keys()];
845
+ likes {
846
+ id
847
+ name
644
848
  }
645
- /**
646
- * Remove all entries whose key matches the given pattern.
647
- *
648
- * - **String**: treated as a substring match (e.g. `"Media"` removes all keys containing `"Media"`).
649
- * - **RegExp**: tested against each key directly.
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
- // src/errors/index.ts
666
- var AniListError = class _AniListError extends Error {
667
- constructor(message, status, errors = []) {
668
- super(message);
669
- this.name = "AniListError";
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
- * @internal
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
- this.hooks.onResponse?.(query, Date.now() - start, false);
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
- const query = include ? buildMediaByIdQuery(include) : QUERY_MEDIA_BY_ID;
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
- const { query: search, page = 1, perPage = 20, genres, tags, genresExclude, tagsExclude, ...filters } = options;
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
- * Get currently trending anime or manga.
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
- * Get the most popular anime or manga.
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
- * Get the highest-rated anime or manga.
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
- * Fetch a character by AniList ID.
1186
- *
1187
- * @param id - The AniList character ID
1188
- * @param include - Optional include options (e.g. voice actors)
1189
- * @returns The character object
1190
- *
1191
- * @example
1192
- * ```ts
1193
- * const spike = await client.getCharacter(1);
1194
- * console.log(spike.name.full); // "Spike Spiegel"
1195
- *
1196
- * // With voice actors
1197
- * const spike = await client.getCharacter(1, { voiceActors: true });
1198
- * spike.media?.edges?.forEach((e) => {
1199
- * console.log(e.node.title.romaji);
1200
- * e.voiceActors?.forEach((va) => console.log(` VA: ${va.name.full}`));
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
- const query = include?.voiceActors ? QUERY_CHARACTER_BY_ID_WITH_VA : QUERY_CHARACTER_BY_ID;
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
- const { query: search, page = 1, perPage = 20, voiceActors, ...rest } = options;
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
- * Fetch a staff member by AniList ID.
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
- if (include?.media) {
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
- const { query: search, page = 1, perPage = 20, sort } = options;
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
- if (typeof idOrName === "number") {
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
- const { query: search, page = 1, perPage = 20, sort } = options;
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
- * Get anime (or manga) for a specific season and year.
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
- * Get a user's anime or manga list.
1670
+ * Fetch a user's favorite anime, manga, characters, staff, and studios.
1475
1671
  *
1476
- * Provide either `userId` or `userName` to identify the user.
1477
- * Requires `type` (ANIME or MANGA). Optionally filter by list status.
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
- * ```ts
1484
- * import { MediaType, MediaListStatus } from "ani-client";
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 getUserMediaList(options) {
1498
- if (!options.userId && !options.userName) {
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
- * Fetch a studio by its AniList ID.
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
- const data = await this.request(QUERY_STUDIO_BY_ID, { id });
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.pagedRequest(
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
- * Get all available genres on AniList.
1550
- *
1551
- * @returns Array of genre strings (e.g. "Action", "Adventure", ...)
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
- for (const idChunk of chunks) {
1646
- const query = buildQuery(idChunk);
1647
- const data = await this.request(query);
1648
- chunkResults.push(idChunk.map((_, i) => data[`${prefix}${i}`]));
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