ani-client 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35,6 +35,13 @@ var MEDIA_FIELDS_BASE = `
35
35
  studios { nodes { id name isAnimationStudio siteUrl } }
36
36
  isAdult
37
37
  siteUrl
38
+ nextAiringEpisode {
39
+ id
40
+ airingAt
41
+ episode
42
+ mediaId
43
+ timeUntilAiring
44
+ }
38
45
  `;
39
46
  var RELATIONS_FIELDS = `
40
47
  relations {
@@ -202,6 +209,10 @@ query (
202
209
  $seasonYear: Int,
203
210
  $genre: String,
204
211
  $tag: String,
212
+ $genre_in: [String],
213
+ $tag_in: [String],
214
+ $genre_not_in: [String],
215
+ $tag_not_in: [String],
205
216
  $isAdult: Boolean,
206
217
  $sort: [MediaSort],
207
218
  $page: Int,
@@ -218,6 +229,10 @@ query (
218
229
  seasonYear: $seasonYear,
219
230
  genre: $genre,
220
231
  tag: $tag,
232
+ genre_in: $genre_in,
233
+ tag_in: $tag_in,
234
+ genre_not_in: $genre_not_in,
235
+ tag_not_in: $tag_not_in,
221
236
  isAdult: $isAdult,
222
237
  sort: $sort
223
238
  ) {
@@ -298,6 +313,15 @@ query ($name: String!) {
298
313
  ${USER_FIELDS}
299
314
  }
300
315
  }`;
316
+ var QUERY_USER_SEARCH = `
317
+ query ($search: String, $sort: [UserSort], $page: Int, $perPage: Int) {
318
+ Page(page: $page, perPage: $perPage) {
319
+ pageInfo { total perPage currentPage lastPage hasNextPage }
320
+ users(search: $search, sort: $sort) {
321
+ ${USER_FIELDS}
322
+ }
323
+ }
324
+ }`;
301
325
  var QUERY_AIRING_SCHEDULE = `
302
326
  query ($airingAt_greater: Int, $airingAt_lesser: Int, $sort: [AiringSort], $page: Int, $perPage: Int) {
303
327
  Page(page: $page, perPage: $perPage) {
@@ -552,6 +576,21 @@ var buildBatchMediaQuery = (ids) => buildBatchQuery(ids, "Media", MEDIA_FIELDS_B
552
576
  var buildBatchCharacterQuery = (ids) => buildBatchQuery(ids, "Character", CHARACTER_FIELDS, "c");
553
577
  var buildBatchStaffQuery = (ids) => buildBatchQuery(ids, "Staff", STAFF_FIELDS, "s");
554
578
 
579
+ // src/utils/index.ts
580
+ function normalizeQuery(query) {
581
+ return query.replace(/\s+/g, " ").trim();
582
+ }
583
+ function clampPerPage(value) {
584
+ return Math.min(Math.max(value, 1), 50);
585
+ }
586
+ function chunk(arr, size) {
587
+ const chunks = [];
588
+ for (let i = 0; i < arr.length; i += size) {
589
+ chunks.push(arr.slice(i, i + size));
590
+ }
591
+ return chunks;
592
+ }
593
+
555
594
  // src/cache/index.ts
556
595
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
557
596
  var MemoryCache = class {
@@ -563,7 +602,7 @@ var MemoryCache = class {
563
602
  }
564
603
  /** Build a deterministic cache key from a query + variables pair. */
565
604
  static key(query, variables) {
566
- const normalized = query.replace(/\s+/g, " ").trim();
605
+ const normalized = normalizeQuery(query);
567
606
  return `${normalized}|${JSON.stringify(variables, Object.keys(variables).sort())}`;
568
607
  }
569
608
  /** Retrieve a cached value, or `undefined` if missing / expired. */
@@ -608,14 +647,17 @@ var MemoryCache = class {
608
647
  /**
609
648
  * Remove all entries whose key matches the given pattern.
610
649
  *
611
- * @param pattern A string (converted to RegExp) or RegExp.
650
+ * - **String**: treated as a substring match (e.g. `"Media"` removes all keys containing `"Media"`).
651
+ * - **RegExp**: tested against each key directly.
652
+ *
653
+ * @param pattern — A string (substring match) or RegExp.
612
654
  * @returns Number of entries removed.
613
655
  */
614
656
  invalidate(pattern) {
615
- const regex = typeof pattern === "string" ? new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) : pattern;
657
+ const test = typeof pattern === "string" ? (key) => key.includes(pattern) : (key) => pattern.test(key);
616
658
  const toDelete = [];
617
659
  for (const key of this.store.keys()) {
618
- if (regex.test(key)) toDelete.push(key);
660
+ if (test(key)) toDelete.push(key);
619
661
  }
620
662
  for (const key of toDelete) this.store.delete(key);
621
663
  return toDelete.length;
@@ -639,8 +681,8 @@ var AniListError = class _AniListError extends Error {
639
681
  // src/rate-limiter/index.ts
640
682
  var RateLimiter = class {
641
683
  constructor(options = {}) {
642
- /** @internal */
643
- this.timestamps = [];
684
+ this.head = 0;
685
+ this.count = 0;
644
686
  this.maxRequests = options.maxRequests ?? 85;
645
687
  this.windowMs = options.windowMs ?? 6e4;
646
688
  this.maxRetries = options.maxRetries ?? 3;
@@ -648,24 +690,34 @@ var RateLimiter = class {
648
690
  this.enabled = options.enabled ?? true;
649
691
  this.timeoutMs = options.timeoutMs ?? 3e4;
650
692
  this.retryOnNetworkError = options.retryOnNetworkError ?? true;
693
+ this.timestamps = new Array(this.maxRequests).fill(0);
651
694
  }
652
695
  /**
653
696
  * Wait until it's safe to make a request (respects rate limit window).
654
697
  */
655
698
  async acquire() {
656
699
  if (!this.enabled) return;
700
+ if (this.count >= this.maxRequests) {
701
+ const oldest = this.timestamps[this.head];
702
+ const now2 = Date.now();
703
+ const elapsed = now2 - oldest;
704
+ if (elapsed < this.windowMs) {
705
+ const waitMs = this.windowMs - elapsed + 50;
706
+ await this.sleep(waitMs);
707
+ }
708
+ }
657
709
  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();
710
+ if (this.count < this.maxRequests) {
711
+ this.timestamps[(this.head + this.count) % this.maxRequests] = now;
712
+ this.count++;
713
+ } else {
714
+ this.timestamps[this.head] = now;
715
+ this.head = (this.head + 1) % this.maxRequests;
664
716
  }
665
- this.timestamps.push(Date.now());
666
717
  }
667
718
  /**
668
719
  * Execute a fetch with automatic retry on 429 responses and network errors.
720
+ * Uses exponential backoff with jitter for retry delays.
669
721
  */
670
722
  async fetchWithRetry(url, init, hooks) {
671
723
  await this.acquire();
@@ -678,7 +730,7 @@ var RateLimiter = class {
678
730
  lastResponse = res;
679
731
  if (attempt === this.maxRetries) break;
680
732
  const retryAfter = res.headers.get("Retry-After");
681
- const delayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1e3 : this.retryDelayMs * (attempt + 1);
733
+ const delayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1e3 : this.exponentialDelay(attempt);
682
734
  hooks?.onRateLimit?.(delayMs);
683
735
  hooks?.onRetry?.(attempt + 1, "HTTP 429", delayMs);
684
736
  await this.sleep(delayMs);
@@ -686,7 +738,7 @@ var RateLimiter = class {
686
738
  } catch (err) {
687
739
  lastError = err;
688
740
  if (this.retryOnNetworkError && isNetworkError(err) && attempt < this.maxRetries) {
689
- const delayMs = this.retryDelayMs * (attempt + 1);
741
+ const delayMs = this.exponentialDelay(attempt);
690
742
  hooks?.onRetry?.(attempt + 1, `Network error: ${err.message}`, delayMs);
691
743
  await this.sleep(delayMs);
692
744
  await this.acquire();
@@ -698,6 +750,12 @@ var RateLimiter = class {
698
750
  if (lastResponse) return lastResponse;
699
751
  throw lastError;
700
752
  }
753
+ /** @internal — Exponential backoff with jitter, capped at 30s */
754
+ exponentialDelay(attempt) {
755
+ const base = this.retryDelayMs * 2 ** attempt;
756
+ const jitter = Math.random() * 1e3;
757
+ return Math.min(base + jitter, 3e4);
758
+ }
701
759
  /** @internal */
702
760
  async fetchWithTimeout(url, init) {
703
761
  if (this.timeoutMs <= 0) return fetch(url, init);
@@ -737,6 +795,24 @@ var MediaType = /* @__PURE__ */ ((MediaType2) => {
737
795
  MediaType2["MANGA"] = "MANGA";
738
796
  return MediaType2;
739
797
  })(MediaType || {});
798
+ var MediaSource = /* @__PURE__ */ ((MediaSource2) => {
799
+ MediaSource2["ORIGINAL"] = "ORIGINAL";
800
+ MediaSource2["MANGA"] = "MANGA";
801
+ MediaSource2["LIGHT_NOVEL"] = "LIGHT_NOVEL";
802
+ MediaSource2["VISUAL_NOVEL"] = "VISUAL_NOVEL";
803
+ MediaSource2["VIDEO_GAME"] = "VIDEO_GAME";
804
+ MediaSource2["OTHER"] = "OTHER";
805
+ MediaSource2["NOVEL"] = "NOVEL";
806
+ MediaSource2["DOUJINSHI"] = "DOUJINSHI";
807
+ MediaSource2["ANIME"] = "ANIME";
808
+ MediaSource2["WEB_NOVEL"] = "WEB_NOVEL";
809
+ MediaSource2["LIVE_ACTION"] = "LIVE_ACTION";
810
+ MediaSource2["GAME"] = "GAME";
811
+ MediaSource2["COMIC"] = "COMIC";
812
+ MediaSource2["MULTIMEDIA_PROJECT"] = "MULTIMEDIA_PROJECT";
813
+ MediaSource2["PICTURE_BOOK"] = "PICTURE_BOOK";
814
+ return MediaSource2;
815
+ })(MediaSource || {});
740
816
  var MediaFormat = /* @__PURE__ */ ((MediaFormat2) => {
741
817
  MediaFormat2["TV"] = "TV";
742
818
  MediaFormat2["TV_SHORT"] = "TV_SHORT";
@@ -869,6 +945,20 @@ var StaffSort = /* @__PURE__ */ ((StaffSort2) => {
869
945
  return StaffSort2;
870
946
  })(StaffSort || {});
871
947
 
948
+ // src/types/user.ts
949
+ var UserSort = /* @__PURE__ */ ((UserSort2) => {
950
+ UserSort2["ID"] = "ID";
951
+ UserSort2["ID_DESC"] = "ID_DESC";
952
+ UserSort2["USERNAME"] = "USERNAME";
953
+ UserSort2["USERNAME_DESC"] = "USERNAME_DESC";
954
+ UserSort2["WATCHED_TIME"] = "WATCHED_TIME";
955
+ UserSort2["WATCHED_TIME_DESC"] = "WATCHED_TIME_DESC";
956
+ UserSort2["CHAPTERS_READ"] = "CHAPTERS_READ";
957
+ UserSort2["CHAPTERS_READ_DESC"] = "CHAPTERS_READ_DESC";
958
+ UserSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
959
+ return UserSort2;
960
+ })(UserSort || {});
961
+
872
962
  // src/types/lists.ts
873
963
  var MediaListStatus = /* @__PURE__ */ ((MediaListStatus2) => {
874
964
  MediaListStatus2["CURRENT"] = "CURRENT";
@@ -913,18 +1003,6 @@ var MediaListSort = /* @__PURE__ */ ((MediaListSort2) => {
913
1003
  return MediaListSort2;
914
1004
  })(MediaListSort || {});
915
1005
 
916
- // src/utils/index.ts
917
- function clampPerPage(value) {
918
- return Math.min(Math.max(value, 1), 50);
919
- }
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;
926
- }
927
-
928
1006
  // src/client/index.ts
929
1007
  var DEFAULT_API_URL = "https://graphql.anilist.co";
930
1008
  var AniListClient = class {
@@ -967,7 +1045,7 @@ var AniListClient = class {
967
1045
  async executeRequest(query, variables, cacheKey) {
968
1046
  const start = Date.now();
969
1047
  this.hooks.onRequest?.(query, variables);
970
- const minifiedQuery = query.replace(/\s+/g, " ").trim();
1048
+ const minifiedQuery = normalizeQuery(query);
971
1049
  const res = await this.rateLimiter.fetchWithRetry(
972
1050
  this.apiUrl,
973
1051
  {
@@ -1055,10 +1133,19 @@ var AniListClient = class {
1055
1133
  * ```
1056
1134
  */
1057
1135
  async searchMedia(options = {}) {
1058
- const { query: search, page = 1, perPage = 20, ...filters } = options;
1136
+ const { query: search, page = 1, perPage = 20, genres, tags, genresExclude, tagsExclude, ...filters } = options;
1059
1137
  return this.pagedRequest(
1060
1138
  QUERY_MEDIA_SEARCH,
1061
- { search, ...filters, page, perPage: clampPerPage(perPage) },
1139
+ {
1140
+ search,
1141
+ ...filters,
1142
+ genre_in: genres,
1143
+ tag_in: tags,
1144
+ genre_not_in: genresExclude,
1145
+ tag_not_in: tagsExclude,
1146
+ page,
1147
+ perPage: clampPerPage(perPage)
1148
+ },
1062
1149
  "media"
1063
1150
  );
1064
1151
  }
@@ -1072,6 +1159,30 @@ var AniListClient = class {
1072
1159
  async getTrending(type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
1073
1160
  return this.pagedRequest(QUERY_TRENDING, { type, page, perPage: clampPerPage(perPage) }, "media");
1074
1161
  }
1162
+ /**
1163
+ * Get the most popular anime or manga.
1164
+ *
1165
+ * Convenience wrapper around `searchMedia` with `sort: POPULARITY_DESC`.
1166
+ *
1167
+ * @param type - `MediaType.ANIME` or `MediaType.MANGA` (defaults to ANIME)
1168
+ * @param page - Page number (default 1)
1169
+ * @param perPage - Results per page (default 20, max 50)
1170
+ */
1171
+ async getPopular(type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
1172
+ return this.searchMedia({ type, sort: ["POPULARITY_DESC" /* POPULARITY_DESC */], page, perPage });
1173
+ }
1174
+ /**
1175
+ * Get the highest-rated anime or manga.
1176
+ *
1177
+ * Convenience wrapper around `searchMedia` with `sort: SCORE_DESC`.
1178
+ *
1179
+ * @param type - `MediaType.ANIME` or `MediaType.MANGA` (defaults to ANIME)
1180
+ * @param page - Page number (default 1)
1181
+ * @param perPage - Results per page (default 20, max 50)
1182
+ */
1183
+ async getTopRated(type = "ANIME" /* ANIME */, page = 1, perPage = 20) {
1184
+ return this.searchMedia({ type, sort: ["SCORE_DESC" /* SCORE_DESC */], page, perPage });
1185
+ }
1075
1186
  /**
1076
1187
  * Fetch a character by AniList ID.
1077
1188
  *
@@ -1167,36 +1278,50 @@ var AniListClient = class {
1167
1278
  );
1168
1279
  }
1169
1280
  /**
1170
- * Fetch a user by AniList ID.
1281
+ * Fetch a user by AniList ID or username.
1171
1282
  *
1172
- * @param id - The AniList user ID
1283
+ * @param idOrName - The AniList user ID (number) or username (string)
1173
1284
  * @returns The user object
1174
1285
  *
1175
1286
  * @example
1176
1287
  * ```ts
1177
1288
  * const user = await client.getUser(1);
1289
+ * const user2 = await client.getUser("AniList");
1178
1290
  * console.log(user.name);
1179
1291
  * ```
1180
1292
  */
1181
- async getUser(id) {
1182
- const data = await this.request(QUERY_USER_BY_ID, { id });
1293
+ async getUser(idOrName) {
1294
+ if (typeof idOrName === "number") {
1295
+ const data2 = await this.request(QUERY_USER_BY_ID, { id: idOrName });
1296
+ return data2.User;
1297
+ }
1298
+ const data = await this.request(QUERY_USER_BY_NAME, { name: idOrName });
1183
1299
  return data.User;
1184
1300
  }
1185
1301
  /**
1186
1302
  * Fetch a user by username.
1187
1303
  *
1304
+ * @deprecated Use `getUser(name)` instead.
1188
1305
  * @param name - The AniList username
1189
1306
  * @returns The user object
1307
+ */
1308
+ async getUserByName(name) {
1309
+ return this.getUser(name);
1310
+ }
1311
+ /**
1312
+ * Search for users by name.
1313
+ *
1314
+ * @param options - Search / pagination parameters
1315
+ * @returns Paginated results with matching users
1190
1316
  *
1191
1317
  * @example
1192
1318
  * ```ts
1193
- * const user = await client.getUserByName("AniList");
1194
- * console.log(user.statistics);
1319
+ * const result = await client.searchUsers({ query: "AniList", perPage: 5 });
1195
1320
  * ```
1196
1321
  */
1197
- async getUserByName(name) {
1198
- const data = await this.request(QUERY_USER_BY_NAME, { name });
1199
- return data.User;
1322
+ async searchUsers(options = {}) {
1323
+ const { query: search, page = 1, perPage = 20, sort } = options;
1324
+ return this.pagedRequest(QUERY_USER_SEARCH, { search, sort, page, perPage: clampPerPage(perPage) }, "users");
1200
1325
  }
1201
1326
  /**
1202
1327
  * Execute an arbitrary GraphQL query against the AniList API.
@@ -1561,6 +1686,17 @@ var AniListClient = class {
1561
1686
  }
1562
1687
  return count;
1563
1688
  }
1689
+ /**
1690
+ * Clean up resources held by the client.
1691
+ *
1692
+ * Clears the in-memory cache and aborts any pending in-flight requests.
1693
+ * If using a custom cache adapter (e.g. Redis), call its close/disconnect
1694
+ * method separately.
1695
+ */
1696
+ async destroy() {
1697
+ await this.cacheAdapter.clear();
1698
+ this.inFlight.clear();
1699
+ }
1564
1700
  };
1565
1701
 
1566
1702
  // src/cache/redis.ts
@@ -1657,6 +1793,7 @@ exports.MediaListStatus = MediaListStatus;
1657
1793
  exports.MediaRelationType = MediaRelationType;
1658
1794
  exports.MediaSeason = MediaSeason;
1659
1795
  exports.MediaSort = MediaSort;
1796
+ exports.MediaSource = MediaSource;
1660
1797
  exports.MediaStatus = MediaStatus;
1661
1798
  exports.MediaType = MediaType;
1662
1799
  exports.MemoryCache = MemoryCache;
@@ -1664,5 +1801,6 @@ exports.RateLimiter = RateLimiter;
1664
1801
  exports.RecommendationSort = RecommendationSort;
1665
1802
  exports.RedisCache = RedisCache;
1666
1803
  exports.StaffSort = StaffSort;
1804
+ exports.UserSort = UserSort;
1667
1805
  //# sourceMappingURL=index.js.map
1668
1806
  //# sourceMappingURL=index.js.map