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