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