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