ani-client 1.4.4 → 1.5.1

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