ani-client 1.0.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 ADDED
@@ -0,0 +1,522 @@
1
+ // src/queries/index.ts
2
+ var MEDIA_FIELDS = `
3
+ id
4
+ idMal
5
+ title { romaji english native userPreferred }
6
+ type
7
+ format
8
+ status
9
+ description(asHtml: false)
10
+ startDate { year month day }
11
+ endDate { year month day }
12
+ season
13
+ seasonYear
14
+ episodes
15
+ duration
16
+ chapters
17
+ volumes
18
+ countryOfOrigin
19
+ isLicensed
20
+ source
21
+ hashtag
22
+ trailer { id site thumbnail }
23
+ coverImage { extraLarge large medium color }
24
+ bannerImage
25
+ genres
26
+ synonyms
27
+ averageScore
28
+ meanScore
29
+ popularity
30
+ favourites
31
+ trending
32
+ tags { id name description category rank isMediaSpoiler }
33
+ studios { nodes { id name isAnimationStudio siteUrl } }
34
+ isAdult
35
+ siteUrl
36
+ `;
37
+ var CHARACTER_FIELDS = `
38
+ id
39
+ name { first middle last full native alternative }
40
+ image { large medium }
41
+ description(asHtml: false)
42
+ gender
43
+ dateOfBirth { year month day }
44
+ age
45
+ bloodType
46
+ favourites
47
+ siteUrl
48
+ media(perPage: 10) {
49
+ nodes {
50
+ id
51
+ title { romaji english native userPreferred }
52
+ type
53
+ coverImage { large medium }
54
+ siteUrl
55
+ }
56
+ }
57
+ `;
58
+ var STAFF_FIELDS = `
59
+ id
60
+ name { first middle last full native }
61
+ language
62
+ image { large medium }
63
+ description(asHtml: false)
64
+ primaryOccupations
65
+ gender
66
+ dateOfBirth { year month day }
67
+ dateOfDeath { year month day }
68
+ age
69
+ yearsActive
70
+ homeTown
71
+ bloodType
72
+ favourites
73
+ siteUrl
74
+ `;
75
+ var USER_FIELDS = `
76
+ id
77
+ name
78
+ about(asHtml: false)
79
+ avatar { large medium }
80
+ bannerImage
81
+ isFollowing
82
+ isFollower
83
+ donatorTier
84
+ donatorBadge
85
+ createdAt
86
+ siteUrl
87
+ statistics {
88
+ anime { count meanScore minutesWatched episodesWatched chaptersRead volumesRead }
89
+ manga { count meanScore minutesWatched episodesWatched chaptersRead volumesRead }
90
+ }
91
+ `;
92
+ var QUERY_MEDIA_BY_ID = `
93
+ query ($id: Int!) {
94
+ Media(id: $id) {
95
+ ${MEDIA_FIELDS}
96
+ }
97
+ }`;
98
+ var QUERY_MEDIA_SEARCH = `
99
+ query (
100
+ $search: String,
101
+ $type: MediaType,
102
+ $format: MediaFormat,
103
+ $status: MediaStatus,
104
+ $season: MediaSeason,
105
+ $seasonYear: Int,
106
+ $genre: String,
107
+ $tag: String,
108
+ $isAdult: Boolean,
109
+ $sort: [MediaSort],
110
+ $page: Int,
111
+ $perPage: Int
112
+ ) {
113
+ Page(page: $page, perPage: $perPage) {
114
+ pageInfo { total perPage currentPage lastPage hasNextPage }
115
+ media(
116
+ search: $search,
117
+ type: $type,
118
+ format: $format,
119
+ status: $status,
120
+ season: $season,
121
+ seasonYear: $seasonYear,
122
+ genre: $genre,
123
+ tag: $tag,
124
+ isAdult: $isAdult,
125
+ sort: $sort
126
+ ) {
127
+ ${MEDIA_FIELDS}
128
+ }
129
+ }
130
+ }`;
131
+ var QUERY_TRENDING = `
132
+ query ($type: MediaType, $page: Int, $perPage: Int) {
133
+ Page(page: $page, perPage: $perPage) {
134
+ pageInfo { total perPage currentPage lastPage hasNextPage }
135
+ media(type: $type, sort: TRENDING_DESC) {
136
+ ${MEDIA_FIELDS}
137
+ }
138
+ }
139
+ }`;
140
+ var QUERY_CHARACTER_BY_ID = `
141
+ query ($id: Int!) {
142
+ Character(id: $id) {
143
+ ${CHARACTER_FIELDS}
144
+ }
145
+ }`;
146
+ var QUERY_CHARACTER_SEARCH = `
147
+ query ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {
148
+ Page(page: $page, perPage: $perPage) {
149
+ pageInfo { total perPage currentPage lastPage hasNextPage }
150
+ characters(search: $search, sort: $sort) {
151
+ ${CHARACTER_FIELDS}
152
+ }
153
+ }
154
+ }`;
155
+ var QUERY_STAFF_BY_ID = `
156
+ query ($id: Int!) {
157
+ Staff(id: $id) {
158
+ ${STAFF_FIELDS}
159
+ }
160
+ }`;
161
+ var QUERY_STAFF_SEARCH = `
162
+ query ($search: String, $page: Int, $perPage: Int) {
163
+ Page(page: $page, perPage: $perPage) {
164
+ pageInfo { total perPage currentPage lastPage hasNextPage }
165
+ staff(search: $search) {
166
+ ${STAFF_FIELDS}
167
+ }
168
+ }
169
+ }`;
170
+ var QUERY_USER_BY_ID = `
171
+ query ($id: Int!) {
172
+ User(id: $id) {
173
+ ${USER_FIELDS}
174
+ }
175
+ }`;
176
+ var QUERY_USER_BY_NAME = `
177
+ query ($name: String!) {
178
+ User(name: $name) {
179
+ ${USER_FIELDS}
180
+ }
181
+ }`;
182
+
183
+ // src/errors/index.ts
184
+ var AniListError = class extends Error {
185
+ constructor(message, status, errors = []) {
186
+ super(message);
187
+ this.name = "AniListError";
188
+ this.status = status;
189
+ this.errors = errors;
190
+ }
191
+ };
192
+
193
+ // src/cache/index.ts
194
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
195
+ var MemoryCache = class {
196
+ constructor(options = {}) {
197
+ this.store = /* @__PURE__ */ new Map();
198
+ this.ttl = options.ttl ?? ONE_DAY_MS;
199
+ this.maxSize = options.maxSize ?? 500;
200
+ this.enabled = options.enabled ?? true;
201
+ }
202
+ /** Build a deterministic cache key from a query + variables pair. */
203
+ static key(query, variables) {
204
+ return query.trim() + "|" + JSON.stringify(variables, Object.keys(variables).sort());
205
+ }
206
+ /** Retrieve a cached value, or `undefined` if missing / expired. */
207
+ get(key) {
208
+ if (!this.enabled) return void 0;
209
+ const entry = this.store.get(key);
210
+ if (!entry) return void 0;
211
+ if (Date.now() > entry.expiresAt) {
212
+ this.store.delete(key);
213
+ return void 0;
214
+ }
215
+ return entry.data;
216
+ }
217
+ /** Store a value in the cache. */
218
+ set(key, data) {
219
+ if (!this.enabled) return;
220
+ if (this.maxSize > 0 && this.store.size >= this.maxSize) {
221
+ const firstKey = this.store.keys().next().value;
222
+ if (firstKey !== void 0) this.store.delete(firstKey);
223
+ }
224
+ this.store.set(key, { data, expiresAt: Date.now() + this.ttl });
225
+ }
226
+ /** Remove a specific entry. */
227
+ delete(key) {
228
+ return this.store.delete(key);
229
+ }
230
+ /** Clear the entire cache. */
231
+ clear() {
232
+ this.store.clear();
233
+ }
234
+ /** Number of entries currently stored. */
235
+ get size() {
236
+ return this.store.size;
237
+ }
238
+ };
239
+
240
+ // src/rate-limiter/index.ts
241
+ var RateLimiter = class {
242
+ constructor(options = {}) {
243
+ /** @internal */
244
+ this.timestamps = [];
245
+ this.maxRequests = options.maxRequests ?? 85;
246
+ this.windowMs = options.windowMs ?? 6e4;
247
+ this.maxRetries = options.maxRetries ?? 3;
248
+ this.retryDelayMs = options.retryDelayMs ?? 2e3;
249
+ this.enabled = options.enabled ?? true;
250
+ }
251
+ /**
252
+ * Wait until it's safe to make a request (respects rate limit window).
253
+ */
254
+ async acquire() {
255
+ if (!this.enabled) return;
256
+ const now = Date.now();
257
+ this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs);
258
+ if (this.timestamps.length >= this.maxRequests) {
259
+ const oldest = this.timestamps[0];
260
+ const waitMs = this.windowMs - (now - oldest) + 50;
261
+ await this.sleep(waitMs);
262
+ return this.acquire();
263
+ }
264
+ this.timestamps.push(Date.now());
265
+ }
266
+ /**
267
+ * Execute a fetch with automatic retry on 429 responses.
268
+ */
269
+ async fetchWithRetry(url, init) {
270
+ await this.acquire();
271
+ let lastResponse;
272
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
273
+ const res = await fetch(url, init);
274
+ if (res.status !== 429) {
275
+ return res;
276
+ }
277
+ lastResponse = res;
278
+ if (attempt === this.maxRetries) break;
279
+ const retryAfter = res.headers.get("Retry-After");
280
+ const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : this.retryDelayMs * (attempt + 1);
281
+ await this.sleep(delayMs);
282
+ await this.acquire();
283
+ }
284
+ return lastResponse;
285
+ }
286
+ sleep(ms) {
287
+ return new Promise((resolve) => setTimeout(resolve, ms));
288
+ }
289
+ };
290
+
291
+ // src/client/index.ts
292
+ var DEFAULT_API_URL = "https://graphql.anilist.co";
293
+ var AniListClient = class {
294
+ constructor(options = {}) {
295
+ this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;
296
+ this.headers = {
297
+ "Content-Type": "application/json",
298
+ Accept: "application/json"
299
+ };
300
+ if (options.token) {
301
+ this.headers["Authorization"] = `Bearer ${options.token}`;
302
+ }
303
+ this.cache = new MemoryCache(options.cache);
304
+ this.rateLimiter = new RateLimiter(options.rateLimit);
305
+ }
306
+ /**
307
+ * @internal
308
+ */
309
+ async request(query, variables = {}) {
310
+ const cacheKey = MemoryCache.key(query, variables);
311
+ const cached = this.cache.get(cacheKey);
312
+ if (cached !== void 0) return cached;
313
+ const res = await this.rateLimiter.fetchWithRetry(this.apiUrl, {
314
+ method: "POST",
315
+ headers: this.headers,
316
+ body: JSON.stringify({ query, variables })
317
+ });
318
+ const json = await res.json();
319
+ if (!res.ok || json.errors) {
320
+ const message = json.errors?.[0]?.message ?? `AniList API error (HTTP ${res.status})`;
321
+ throw new AniListError(message, res.status, json.errors ?? []);
322
+ }
323
+ const data = json.data;
324
+ this.cache.set(cacheKey, data);
325
+ return data;
326
+ }
327
+ /**
328
+ * Fetch a single media entry by its AniList ID.
329
+ *
330
+ * @param id - The AniList media ID
331
+ * @returns The media object
332
+ */
333
+ async getMedia(id) {
334
+ const data = await this.request(QUERY_MEDIA_BY_ID, { id });
335
+ return data.Media;
336
+ }
337
+ /**
338
+ * Search for anime or manga.
339
+ *
340
+ * @param options - Search / filter parameters
341
+ * @returns Paginated results with matching media
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * const results = await client.searchMedia({
346
+ * query: "Naruto",
347
+ * type: MediaType.ANIME,
348
+ * perPage: 5,
349
+ * });
350
+ * ```
351
+ */
352
+ async searchMedia(options = {}) {
353
+ const variables = {
354
+ search: options.query,
355
+ type: options.type,
356
+ format: options.format,
357
+ status: options.status,
358
+ season: options.season,
359
+ seasonYear: options.seasonYear,
360
+ genre: options.genre,
361
+ tag: options.tag,
362
+ isAdult: options.isAdult,
363
+ sort: options.sort,
364
+ page: options.page ?? 1,
365
+ perPage: options.perPage ?? 20
366
+ };
367
+ const data = await this.request(QUERY_MEDIA_SEARCH, variables);
368
+ return { pageInfo: data.Page.pageInfo, results: data.Page.media };
369
+ }
370
+ /**
371
+ * Get currently trending anime or manga.
372
+ *
373
+ * @param type - `MediaType.ANIME` or `MediaType.MANGA` (defaults to ANIME)
374
+ * @param page - Page number (default 1)
375
+ * @param perPage - Results per page (default 20, max 50)
376
+ */
377
+ async getTrending(type = "ANIME", page = 1, perPage = 20) {
378
+ const data = await this.request(QUERY_TRENDING, { type, page, perPage });
379
+ return { pageInfo: data.Page.pageInfo, results: data.Page.media };
380
+ }
381
+ /**
382
+ * Fetch a character by AniList ID.
383
+ */
384
+ async getCharacter(id) {
385
+ const data = await this.request(QUERY_CHARACTER_BY_ID, { id });
386
+ return data.Character;
387
+ }
388
+ /**
389
+ * Search for characters by name.
390
+ */
391
+ async searchCharacters(options = {}) {
392
+ const variables = {
393
+ search: options.query,
394
+ sort: options.sort,
395
+ page: options.page ?? 1,
396
+ perPage: options.perPage ?? 20
397
+ };
398
+ const data = await this.request(QUERY_CHARACTER_SEARCH, variables);
399
+ return { pageInfo: data.Page.pageInfo, results: data.Page.characters };
400
+ }
401
+ /**
402
+ * Fetch a staff member by AniList ID.
403
+ */
404
+ async getStaff(id) {
405
+ const data = await this.request(QUERY_STAFF_BY_ID, { id });
406
+ return data.Staff;
407
+ }
408
+ /**
409
+ * Search for staff (voice actors, directors, etc.).
410
+ */
411
+ async searchStaff(options = {}) {
412
+ const variables = {
413
+ search: options.query,
414
+ page: options.page ?? 1,
415
+ perPage: options.perPage ?? 20
416
+ };
417
+ const data = await this.request(QUERY_STAFF_SEARCH, variables);
418
+ return { pageInfo: data.Page.pageInfo, results: data.Page.staff };
419
+ }
420
+ /**
421
+ * Fetch a user by AniList ID.
422
+ */
423
+ async getUser(id) {
424
+ const data = await this.request(QUERY_USER_BY_ID, { id });
425
+ return data.User;
426
+ }
427
+ /**
428
+ * Fetch a user by username.
429
+ */
430
+ async getUserByName(name) {
431
+ const data = await this.request(QUERY_USER_BY_NAME, { name });
432
+ return data.User;
433
+ }
434
+ /**
435
+ * Execute an arbitrary GraphQL query against the AniList API.
436
+ * Useful for advanced use-cases not covered by the built-in methods.
437
+ *
438
+ * @param query - A valid GraphQL query string
439
+ * @param variables - Optional variables object
440
+ */
441
+ async raw(query, variables) {
442
+ return this.request(query, variables);
443
+ }
444
+ /**
445
+ * Clear the entire response cache.
446
+ */
447
+ clearCache() {
448
+ this.cache.clear();
449
+ }
450
+ /**
451
+ * Number of entries currently in the cache.
452
+ */
453
+ get cacheSize() {
454
+ return this.cache.size;
455
+ }
456
+ };
457
+
458
+ // src/types/index.ts
459
+ var MediaType = /* @__PURE__ */ ((MediaType2) => {
460
+ MediaType2["ANIME"] = "ANIME";
461
+ MediaType2["MANGA"] = "MANGA";
462
+ return MediaType2;
463
+ })(MediaType || {});
464
+ var MediaFormat = /* @__PURE__ */ ((MediaFormat2) => {
465
+ MediaFormat2["TV"] = "TV";
466
+ MediaFormat2["TV_SHORT"] = "TV_SHORT";
467
+ MediaFormat2["MOVIE"] = "MOVIE";
468
+ MediaFormat2["SPECIAL"] = "SPECIAL";
469
+ MediaFormat2["OVA"] = "OVA";
470
+ MediaFormat2["ONA"] = "ONA";
471
+ MediaFormat2["MUSIC"] = "MUSIC";
472
+ MediaFormat2["MANGA"] = "MANGA";
473
+ MediaFormat2["NOVEL"] = "NOVEL";
474
+ MediaFormat2["ONE_SHOT"] = "ONE_SHOT";
475
+ return MediaFormat2;
476
+ })(MediaFormat || {});
477
+ var MediaStatus = /* @__PURE__ */ ((MediaStatus2) => {
478
+ MediaStatus2["FINISHED"] = "FINISHED";
479
+ MediaStatus2["RELEASING"] = "RELEASING";
480
+ MediaStatus2["NOT_YET_RELEASED"] = "NOT_YET_RELEASED";
481
+ MediaStatus2["CANCELLED"] = "CANCELLED";
482
+ MediaStatus2["HIATUS"] = "HIATUS";
483
+ return MediaStatus2;
484
+ })(MediaStatus || {});
485
+ var MediaSeason = /* @__PURE__ */ ((MediaSeason2) => {
486
+ MediaSeason2["WINTER"] = "WINTER";
487
+ MediaSeason2["SPRING"] = "SPRING";
488
+ MediaSeason2["SUMMER"] = "SUMMER";
489
+ MediaSeason2["FALL"] = "FALL";
490
+ return MediaSeason2;
491
+ })(MediaSeason || {});
492
+ var MediaSort = /* @__PURE__ */ ((MediaSort2) => {
493
+ MediaSort2["ID"] = "ID";
494
+ MediaSort2["TITLE_ROMAJI"] = "TITLE_ROMAJI";
495
+ MediaSort2["TITLE_ENGLISH"] = "TITLE_ENGLISH";
496
+ MediaSort2["TITLE_NATIVE"] = "TITLE_NATIVE";
497
+ MediaSort2["TYPE"] = "TYPE";
498
+ MediaSort2["FORMAT"] = "FORMAT";
499
+ MediaSort2["START_DATE"] = "START_DATE";
500
+ MediaSort2["END_DATE"] = "END_DATE";
501
+ MediaSort2["SCORE"] = "SCORE";
502
+ MediaSort2["POPULARITY"] = "POPULARITY";
503
+ MediaSort2["TRENDING"] = "TRENDING";
504
+ MediaSort2["EPISODES"] = "EPISODES";
505
+ MediaSort2["DURATION"] = "DURATION";
506
+ MediaSort2["STATUS"] = "STATUS";
507
+ MediaSort2["FAVOURITES"] = "FAVOURITES";
508
+ MediaSort2["UPDATED_AT"] = "UPDATED_AT";
509
+ MediaSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
510
+ return MediaSort2;
511
+ })(MediaSort || {});
512
+ var CharacterSort = /* @__PURE__ */ ((CharacterSort2) => {
513
+ CharacterSort2["ID"] = "ID";
514
+ CharacterSort2["ROLE"] = "ROLE";
515
+ CharacterSort2["SEARCH_MATCH"] = "SEARCH_MATCH";
516
+ CharacterSort2["FAVOURITES"] = "FAVOURITES";
517
+ return CharacterSort2;
518
+ })(CharacterSort || {});
519
+
520
+ export { AniListClient, AniListError, CharacterSort, MediaFormat, MediaSeason, MediaSort, MediaStatus, MediaType, MemoryCache, RateLimiter };
521
+ //# sourceMappingURL=index.mjs.map
522
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/queries/index.ts","../src/errors/index.ts","../src/cache/index.ts","../src/rate-limiter/index.ts","../src/client/index.ts","../src/types/index.ts"],"names":["MediaType","MediaFormat","MediaStatus","MediaSeason","MediaSort","CharacterSort"],"mappings":";AAAA,IAAM,YAAA,GAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAoCrB,IAAM,gBAAA,GAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAsBzB,IAAM,YAAA,GAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAkBrB,IAAM,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAkBb,IAAM,iBAAA,GAAoB;AAAA;AAAA;AAAA,IAAA,EAG3B,YAAY;AAAA;AAAA,CAAA,CAAA;AAIX,IAAM,kBAAA,GAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA,EA6B1B,YAAY;AAAA;AAAA;AAAA,CAAA,CAAA;AAKb,IAAM,cAAA,GAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA,EAKtB,YAAY;AAAA;AAAA;AAAA,CAAA,CAAA;AAKb,IAAM,qBAAA,GAAwB;AAAA;AAAA;AAAA,IAAA,EAG/B,gBAAgB;AAAA;AAAA,CAAA,CAAA;AAIf,IAAM,sBAAA,GAAyB;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA,EAK9B,gBAAgB;AAAA;AAAA;AAAA,CAAA,CAAA;AAKjB,IAAM,iBAAA,GAAoB;AAAA;AAAA;AAAA,IAAA,EAG3B,YAAY;AAAA;AAAA,CAAA,CAAA;AAIX,IAAM,kBAAA,GAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA,EAK1B,YAAY;AAAA;AAAA;AAAA,CAAA,CAAA;AAKb,IAAM,gBAAA,GAAmB;AAAA;AAAA;AAAA,IAAA,EAG1B,WAAW;AAAA;AAAA,CAAA,CAAA;AAIV,IAAM,kBAAA,GAAqB;AAAA;AAAA;AAAA,IAAA,EAG5B,WAAW;AAAA;AAAA,CAAA,CAAA;;;AC1LV,IAAM,YAAA,GAAN,cAA2B,KAAA,CAAM;AAAA,EAMtC,WAAA,CAAY,OAAA,EAAiB,MAAA,EAAgB,MAAA,GAAoB,EAAC,EAAG;AACnE,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AACF;;;ACGA,IAAM,UAAA,GAAa,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,GAAA;AAE3B,IAAM,cAAN,MAAkB;AAAA,EAMvB,WAAA,CAAY,OAAA,GAAwB,EAAC,EAAG;AAFxC,IAAA,IAAA,CAAiB,KAAA,uBAAY,GAAA,EAAiC;AAG5D,IAAA,IAAA,CAAK,GAAA,GAAM,QAAQ,GAAA,IAAO,UAAA;AAC1B,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,OAAA,IAAW,GAAA;AAClC,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,OAAA,IAAW,IAAA;AAAA,EACpC;AAAA;AAAA,EAGA,OAAO,GAAA,CAAI,KAAA,EAAe,SAAA,EAA4C;AACpE,IAAA,OAAO,KAAA,CAAM,IAAA,EAAK,GAAI,GAAA,GAAM,IAAA,CAAK,SAAA,CAAU,SAAA,EAAW,MAAA,CAAO,IAAA,CAAK,SAAS,CAAA,CAAE,IAAA,EAAM,CAAA;AAAA,EACrF;AAAA;AAAA,EAGA,IAAO,GAAA,EAA4B;AACjC,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,EAAS,OAAO,MAAA;AAC1B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAChC,IAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,IAAA,IAAI,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,SAAA,EAAW;AAChC,MAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AACrB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,OAAO,KAAA,CAAM,IAAA;AAAA,EACf;AAAA;AAAA,EAGA,GAAA,CAAO,KAAa,IAAA,EAAe;AACjC,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AAEnB,IAAA,IAAI,KAAK,OAAA,GAAU,CAAA,IAAK,KAAK,KAAA,CAAM,IAAA,IAAQ,KAAK,OAAA,EAAS;AACvD,MAAA,MAAM,WAAW,IAAA,CAAK,KAAA,CAAM,IAAA,EAAK,CAAE,MAAK,CAAE,KAAA;AAC1C,MAAA,IAAI,QAAA,KAAa,MAAA,EAAW,IAAA,CAAK,KAAA,CAAM,OAAO,QAAQ,CAAA;AAAA,IACxD;AAEA,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAA,EAAK,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,GAAI,IAAA,CAAK,GAAA,EAAK,CAAA;AAAA,EAChE;AAAA;AAAA,EAGA,OAAO,GAAA,EAAsB;AAC3B,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA;AAAA,EAC9B;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,MAAM,KAAA,EAAM;AAAA,EACnB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,KAAA,CAAM,IAAA;AAAA,EACpB;AACF;;;ACtDO,IAAM,cAAN,MAAkB;AAAA,EAUvB,WAAA,CAAY,OAAA,GAA4B,EAAC,EAAG;AAF5C;AAAA,IAAA,IAAA,CAAQ,aAAuB,EAAC;AAG9B,IAAA,IAAA,CAAK,WAAA,GAAc,QAAQ,WAAA,IAAe,EAAA;AAC1C,IAAA,IAAA,CAAK,QAAA,GAAW,QAAQ,QAAA,IAAY,GAAA;AACpC,IAAA,IAAA,CAAK,UAAA,GAAa,QAAQ,UAAA,IAAc,CAAA;AACxC,IAAA,IAAA,CAAK,YAAA,GAAe,QAAQ,YAAA,IAAgB,GAAA;AAC5C,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,OAAA,IAAW,IAAA;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAA,GAAyB;AAC7B,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AAEnB,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,IAAA,CAAK,UAAA,GAAa,KAAK,UAAA,CAAW,MAAA,CAAO,CAAC,CAAA,KAAM,GAAA,GAAM,CAAA,GAAI,IAAA,CAAK,QAAQ,CAAA;AAEvE,IAAA,IAAI,IAAA,CAAK,UAAA,CAAW,MAAA,IAAU,IAAA,CAAK,WAAA,EAAa;AAC9C,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,UAAA,CAAW,CAAC,CAAA;AAChC,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,QAAA,IAAY,GAAA,GAAM,MAAA,CAAA,GAAU,EAAA;AAChD,MAAA,MAAM,IAAA,CAAK,MAAM,MAAM,CAAA;AACvB,MAAA,OAAO,KAAK,OAAA,EAAQ;AAAA,IACtB;AAEA,IAAA,IAAA,CAAK,UAAA,CAAW,IAAA,CAAK,IAAA,CAAK,GAAA,EAAK,CAAA;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,cAAA,CACJ,GAAA,EACA,IAAA,EACmB;AACnB,IAAA,MAAM,KAAK,OAAA,EAAQ;AAEnB,IAAA,IAAI,YAAA;AAEJ,IAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,IAAA,CAAK,YAAY,OAAA,EAAA,EAAW;AAC3D,MAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,EAAK,IAAI,CAAA;AAEjC,MAAA,IAAI,GAAA,CAAI,WAAW,GAAA,EAAK;AACtB,QAAA,OAAO,GAAA;AAAA,MACT;AAEA,MAAA,YAAA,GAAe,GAAA;AAEf,MAAA,IAAI,OAAA,KAAY,KAAK,UAAA,EAAY;AAEjC,MAAA,MAAM,UAAA,GAAa,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAA;AAChD,MAAA,MAAM,OAAA,GAAU,aACZ,QAAA,CAAS,UAAA,EAAY,EAAE,CAAA,GAAI,GAAA,GAC3B,IAAA,CAAK,YAAA,IAAgB,OAAA,GAAU,CAAA,CAAA;AAEnC,MAAA,MAAM,IAAA,CAAK,MAAM,OAAO,CAAA;AACxB,MAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,IACrB;AAEA,IAAA,OAAO,YAAA;AAAA,EACT;AAAA,EAEQ,MAAM,EAAA,EAA2B;AACvC,IAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAAA,EACzD;AACF;;;AClEA,IAAM,eAAA,GAAkB,4BAAA;AAoBjB,IAAM,gBAAN,MAAoB;AAAA,EAMzB,WAAA,CAAY,OAAA,GAAgC,EAAC,EAAG;AAC9C,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,MAAA,IAAU,eAAA;AAChC,IAAA,IAAA,CAAK,OAAA,GAAU;AAAA,MACb,cAAA,EAAgB,kBAAA;AAAA,MAChB,MAAA,EAAQ;AAAA,KACV;AACA,IAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,MAAA,IAAA,CAAK,OAAA,CAAQ,eAAe,CAAA,GAAI,CAAA,OAAA,EAAU,QAAQ,KAAK,CAAA,CAAA;AAAA,IACzD;AACA,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAI,WAAA,CAAY,OAAA,CAAQ,KAAK,CAAA;AAC1C,IAAA,IAAA,CAAK,WAAA,GAAc,IAAI,WAAA,CAAY,OAAA,CAAQ,SAAS,CAAA;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,OAAA,CAAW,KAAA,EAAe,SAAA,GAAqC,EAAC,EAAe;AAC3F,IAAA,MAAM,QAAA,GAAW,WAAA,CAAY,GAAA,CAAI,KAAA,EAAO,SAAS,CAAA;AACjD,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAA,CAAO,QAAQ,CAAA;AACzC,IAAA,IAAI,MAAA,KAAW,QAAW,OAAO,MAAA;AAEjC,IAAA,MAAM,MAAM,MAAM,IAAA,CAAK,WAAA,CAAY,cAAA,CAAe,KAAK,MAAA,EAAQ;AAAA,MAC7D,MAAA,EAAQ,MAAA;AAAA,MACR,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,MAAM,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,WAAW;AAAA,KAC1C,CAAA;AAED,IAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAE7B,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,IAAM,IAAA,CAAK,MAAA,EAAQ;AAC1B,MAAA,MAAM,OAAA,GACH,KAAK,MAAA,GAAyC,CAAC,GAAG,OAAA,IACnD,CAAA,wBAAA,EAA2B,IAAI,MAAM,CAAA,CAAA,CAAA;AACvC,MAAA,MAAM,IAAI,aAAa,OAAA,EAAS,GAAA,CAAI,QAAQ,IAAA,CAAK,MAAA,IAAU,EAAE,CAAA;AAAA,IAC/D;AAEA,IAAA,MAAM,OAAO,IAAA,CAAK,IAAA;AAClB,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,QAAA,EAAU,IAAI,CAAA;AAC7B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,SAAS,EAAA,EAA4B;AACzC,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,QAA0B,iBAAA,EAAmB,EAAE,IAAI,CAAA;AAC3E,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,MAAM,WAAA,CAAY,OAAA,GAA8B,EAAC,EAAgC;AAC/E,IAAA,MAAM,SAAA,GAAqC;AAAA,MACzC,QAAQ,OAAA,CAAQ,KAAA;AAAA,MAChB,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,YAAY,OAAA,CAAQ,UAAA;AAAA,MACpB,OAAO,OAAA,CAAQ,KAAA;AAAA,MACf,KAAK,OAAA,CAAQ,GAAA;AAAA,MACb,SAAS,OAAA,CAAQ,OAAA;AAAA,MACjB,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,IAAA,EAAM,QAAQ,IAAA,IAAQ,CAAA;AAAA,MACtB,OAAA,EAAS,QAAQ,OAAA,IAAW;AAAA,KAC9B;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,OAAA,CAErB,oBAAoB,SAAS,CAAA;AAEhC,IAAA,OAAO,EAAE,UAAU,IAAA,CAAK,IAAA,CAAK,UAAU,OAAA,EAAS,IAAA,CAAK,KAAK,KAAA,EAAM;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,WAAA,CACJ,IAAA,GAAkB,SAClB,IAAA,GAAO,CAAA,EACP,UAAU,EAAA,EACmB;AAC7B,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,OAAA,CAErB,gBAAgB,EAAE,IAAA,EAAM,IAAA,EAAM,OAAA,EAAS,CAAA;AAE1C,IAAA,OAAO,EAAE,UAAU,IAAA,CAAK,IAAA,CAAK,UAAU,OAAA,EAAS,IAAA,CAAK,KAAK,KAAA,EAAM;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,EAAA,EAAgC;AACjD,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,QAAkC,qBAAA,EAAuB,EAAE,IAAI,CAAA;AACvF,IAAA,OAAO,IAAA,CAAK,SAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,gBAAA,CAAiB,OAAA,GAAkC,EAAC,EAAoC;AAC5F,IAAA,MAAM,SAAA,GAAqC;AAAA,MACzC,QAAQ,OAAA,CAAQ,KAAA;AAAA,MAChB,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,IAAA,EAAM,QAAQ,IAAA,IAAQ,CAAA;AAAA,MACtB,OAAA,EAAS,QAAQ,OAAA,IAAW;AAAA,KAC9B;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,OAAA,CAErB,wBAAwB,SAAS,CAAA;AAEpC,IAAA,OAAO,EAAE,UAAU,IAAA,CAAK,IAAA,CAAK,UAAU,OAAA,EAAS,IAAA,CAAK,KAAK,UAAA,EAAW;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAS,EAAA,EAA4B;AACzC,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,QAA0B,iBAAA,EAAmB,EAAE,IAAI,CAAA;AAC3E,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAA,CAAY,OAAA,GAA8B,EAAC,EAAgC;AAC/E,IAAA,MAAM,SAAA,GAAqC;AAAA,MACzC,QAAQ,OAAA,CAAQ,KAAA;AAAA,MAChB,IAAA,EAAM,QAAQ,IAAA,IAAQ,CAAA;AAAA,MACtB,OAAA,EAAS,QAAQ,OAAA,IAAW;AAAA,KAC9B;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,OAAA,CAErB,oBAAoB,SAAS,CAAA;AAEhC,IAAA,OAAO,EAAE,UAAU,IAAA,CAAK,IAAA,CAAK,UAAU,OAAA,EAAS,IAAA,CAAK,KAAK,KAAA,EAAM;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAQ,EAAA,EAA2B;AACvC,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,QAAwB,gBAAA,EAAkB,EAAE,IAAI,CAAA;AACxE,IAAA,OAAO,IAAA,CAAK,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,cAAc,IAAA,EAA6B;AAC/C,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,QAAwB,kBAAA,EAAoB,EAAE,MAAM,CAAA;AAC5E,IAAA,OAAO,IAAA,CAAK,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,GAAA,CAAiB,KAAA,EAAe,SAAA,EAAiD;AACrF,IAAA,OAAO,IAAA,CAAK,OAAA,CAAW,KAAA,EAAO,SAAS,CAAA;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,MAAM,KAAA,EAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,SAAA,GAAoB;AACtB,IAAA,OAAO,KAAK,KAAA,CAAM,IAAA;AAAA,EACpB;AACF;;;AC/PO,IAAK,SAAA,qBAAAA,UAAAA,KAAL;AACL,EAAAA,WAAA,OAAA,CAAA,GAAQ,OAAA;AACR,EAAAA,WAAA,OAAA,CAAA,GAAQ,OAAA;AAFE,EAAA,OAAAA,UAAAA;AAAA,CAAA,EAAA,SAAA,IAAA,EAAA;AAKL,IAAK,WAAA,qBAAAC,YAAAA,KAAL;AACL,EAAAA,aAAA,IAAA,CAAA,GAAK,IAAA;AACL,EAAAA,aAAA,UAAA,CAAA,GAAW,UAAA;AACX,EAAAA,aAAA,OAAA,CAAA,GAAQ,OAAA;AACR,EAAAA,aAAA,SAAA,CAAA,GAAU,SAAA;AACV,EAAAA,aAAA,KAAA,CAAA,GAAM,KAAA;AACN,EAAAA,aAAA,KAAA,CAAA,GAAM,KAAA;AACN,EAAAA,aAAA,OAAA,CAAA,GAAQ,OAAA;AACR,EAAAA,aAAA,OAAA,CAAA,GAAQ,OAAA;AACR,EAAAA,aAAA,OAAA,CAAA,GAAQ,OAAA;AACR,EAAAA,aAAA,UAAA,CAAA,GAAW,UAAA;AAVD,EAAA,OAAAA,YAAAA;AAAA,CAAA,EAAA,WAAA,IAAA,EAAA;AAaL,IAAK,WAAA,qBAAAC,YAAAA,KAAL;AACL,EAAAA,aAAA,UAAA,CAAA,GAAW,UAAA;AACX,EAAAA,aAAA,WAAA,CAAA,GAAY,WAAA;AACZ,EAAAA,aAAA,kBAAA,CAAA,GAAmB,kBAAA;AACnB,EAAAA,aAAA,WAAA,CAAA,GAAY,WAAA;AACZ,EAAAA,aAAA,QAAA,CAAA,GAAS,QAAA;AALC,EAAA,OAAAA,YAAAA;AAAA,CAAA,EAAA,WAAA,IAAA,EAAA;AAQL,IAAK,WAAA,qBAAAC,YAAAA,KAAL;AACL,EAAAA,aAAA,QAAA,CAAA,GAAS,QAAA;AACT,EAAAA,aAAA,QAAA,CAAA,GAAS,QAAA;AACT,EAAAA,aAAA,QAAA,CAAA,GAAS,QAAA;AACT,EAAAA,aAAA,MAAA,CAAA,GAAO,MAAA;AAJG,EAAA,OAAAA,YAAAA;AAAA,CAAA,EAAA,WAAA,IAAA,EAAA;AAOL,IAAK,SAAA,qBAAAC,UAAAA,KAAL;AACL,EAAAA,WAAA,IAAA,CAAA,GAAK,IAAA;AACL,EAAAA,WAAA,cAAA,CAAA,GAAe,cAAA;AACf,EAAAA,WAAA,eAAA,CAAA,GAAgB,eAAA;AAChB,EAAAA,WAAA,cAAA,CAAA,GAAe,cAAA;AACf,EAAAA,WAAA,MAAA,CAAA,GAAO,MAAA;AACP,EAAAA,WAAA,QAAA,CAAA,GAAS,QAAA;AACT,EAAAA,WAAA,YAAA,CAAA,GAAa,YAAA;AACb,EAAAA,WAAA,UAAA,CAAA,GAAW,UAAA;AACX,EAAAA,WAAA,OAAA,CAAA,GAAQ,OAAA;AACR,EAAAA,WAAA,YAAA,CAAA,GAAa,YAAA;AACb,EAAAA,WAAA,UAAA,CAAA,GAAW,UAAA;AACX,EAAAA,WAAA,UAAA,CAAA,GAAW,UAAA;AACX,EAAAA,WAAA,UAAA,CAAA,GAAW,UAAA;AACX,EAAAA,WAAA,QAAA,CAAA,GAAS,QAAA;AACT,EAAAA,WAAA,YAAA,CAAA,GAAa,YAAA;AACb,EAAAA,WAAA,YAAA,CAAA,GAAa,YAAA;AACb,EAAAA,WAAA,cAAA,CAAA,GAAe,cAAA;AAjBL,EAAA,OAAAA,UAAAA;AAAA,CAAA,EAAA,SAAA,IAAA,EAAA;AAoBL,IAAK,aAAA,qBAAAC,cAAAA,KAAL;AACL,EAAAA,eAAA,IAAA,CAAA,GAAK,IAAA;AACL,EAAAA,eAAA,MAAA,CAAA,GAAO,MAAA;AACP,EAAAA,eAAA,cAAA,CAAA,GAAe,cAAA;AACf,EAAAA,eAAA,YAAA,CAAA,GAAa,YAAA;AAJH,EAAA,OAAAA,cAAAA;AAAA,CAAA,EAAA,aAAA,IAAA,EAAA","file":"index.mjs","sourcesContent":["const MEDIA_FIELDS = `\n id\n idMal\n title { romaji english native userPreferred }\n type\n format\n status\n description(asHtml: false)\n startDate { year month day }\n endDate { year month day }\n season\n seasonYear\n episodes\n duration\n chapters\n volumes\n countryOfOrigin\n isLicensed\n source\n hashtag\n trailer { id site thumbnail }\n coverImage { extraLarge large medium color }\n bannerImage\n genres\n synonyms\n averageScore\n meanScore\n popularity\n favourites\n trending\n tags { id name description category rank isMediaSpoiler }\n studios { nodes { id name isAnimationStudio siteUrl } }\n isAdult\n siteUrl\n`;\n\nconst CHARACTER_FIELDS = `\n id\n name { first middle last full native alternative }\n image { large medium }\n description(asHtml: false)\n gender\n dateOfBirth { year month day }\n age\n bloodType\n favourites\n siteUrl\n media(perPage: 10) {\n nodes {\n id\n title { romaji english native userPreferred }\n type\n coverImage { large medium }\n siteUrl\n }\n }\n`;\n\nconst STAFF_FIELDS = `\n id\n name { first middle last full native }\n language\n image { large medium }\n description(asHtml: false)\n primaryOccupations\n gender\n dateOfBirth { year month day }\n dateOfDeath { year month day }\n age\n yearsActive\n homeTown\n bloodType\n favourites\n siteUrl\n`;\n\nconst USER_FIELDS = `\n id\n name\n about(asHtml: false)\n avatar { large medium }\n bannerImage\n isFollowing\n isFollower\n donatorTier\n donatorBadge\n createdAt\n siteUrl\n statistics {\n anime { count meanScore minutesWatched episodesWatched chaptersRead volumesRead }\n manga { count meanScore minutesWatched episodesWatched chaptersRead volumesRead }\n }\n`;\n\nexport const QUERY_MEDIA_BY_ID = `\nquery ($id: Int!) {\n Media(id: $id) {\n ${MEDIA_FIELDS}\n }\n}`;\n\nexport const QUERY_MEDIA_SEARCH = `\nquery (\n $search: String,\n $type: MediaType,\n $format: MediaFormat,\n $status: MediaStatus,\n $season: MediaSeason,\n $seasonYear: Int,\n $genre: String,\n $tag: String,\n $isAdult: Boolean,\n $sort: [MediaSort],\n $page: Int,\n $perPage: Int\n) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total perPage currentPage lastPage hasNextPage }\n media(\n search: $search,\n type: $type,\n format: $format,\n status: $status,\n season: $season,\n seasonYear: $seasonYear,\n genre: $genre,\n tag: $tag,\n isAdult: $isAdult,\n sort: $sort\n ) {\n ${MEDIA_FIELDS}\n }\n }\n}`;\n\nexport const QUERY_TRENDING = `\nquery ($type: MediaType, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total perPage currentPage lastPage hasNextPage }\n media(type: $type, sort: TRENDING_DESC) {\n ${MEDIA_FIELDS}\n }\n }\n}`;\n\nexport const QUERY_CHARACTER_BY_ID = `\nquery ($id: Int!) {\n Character(id: $id) {\n ${CHARACTER_FIELDS}\n }\n}`;\n\nexport const QUERY_CHARACTER_SEARCH = `\nquery ($search: String, $sort: [CharacterSort], $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total perPage currentPage lastPage hasNextPage }\n characters(search: $search, sort: $sort) {\n ${CHARACTER_FIELDS}\n }\n }\n}`;\n\nexport const QUERY_STAFF_BY_ID = `\nquery ($id: Int!) {\n Staff(id: $id) {\n ${STAFF_FIELDS}\n }\n}`;\n\nexport const QUERY_STAFF_SEARCH = `\nquery ($search: String, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total perPage currentPage lastPage hasNextPage }\n staff(search: $search) {\n ${STAFF_FIELDS}\n }\n }\n}`;\n\nexport const QUERY_USER_BY_ID = `\nquery ($id: Int!) {\n User(id: $id) {\n ${USER_FIELDS}\n }\n}`;\n\nexport const QUERY_USER_BY_NAME = `\nquery ($name: String!) {\n User(name: $name) {\n ${USER_FIELDS}\n }\n}`;\n","/**\n * Custom error class for AniList API errors.\n */\nexport class AniListError extends Error {\n /** HTTP status code returned by the API */\n public readonly status: number;\n /** Raw error body from the API response */\n public readonly errors: unknown[];\n\n constructor(message: string, status: number, errors: unknown[] = []) {\n super(message);\n this.name = \"AniListError\";\n this.status = status;\n this.errors = errors;\n }\n}\n","/**\n * Simple in-memory cache with configurable TTL.\n * Used internally by AniListClient to avoid redundant API calls.\n */\nexport interface CacheOptions {\n /** Time-to-live in milliseconds (default: 24 hours) */\n ttl?: number;\n /** Maximum number of entries to keep (default: 500, 0 = unlimited) */\n maxSize?: number;\n /** Disable caching entirely (default: false) */\n enabled?: boolean;\n}\n\ninterface CacheEntry<T> {\n data: T;\n expiresAt: number;\n}\n\nconst ONE_DAY_MS = 24 * 60 * 60 * 1000;\n\nexport class MemoryCache {\n private readonly ttl: number;\n private readonly maxSize: number;\n private readonly enabled: boolean;\n private readonly store = new Map<string, CacheEntry<unknown>>();\n\n constructor(options: CacheOptions = {}) {\n this.ttl = options.ttl ?? ONE_DAY_MS;\n this.maxSize = options.maxSize ?? 500;\n this.enabled = options.enabled ?? true;\n }\n\n /** Build a deterministic cache key from a query + variables pair. */\n static key(query: string, variables: Record<string, unknown>): string {\n return query.trim() + \"|\" + JSON.stringify(variables, Object.keys(variables).sort());\n }\n\n /** Retrieve a cached value, or `undefined` if missing / expired. */\n get<T>(key: string): T | undefined {\n if (!this.enabled) return undefined;\n const entry = this.store.get(key);\n if (!entry) return undefined;\n if (Date.now() > entry.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n return entry.data as T;\n }\n\n /** Store a value in the cache. */\n set<T>(key: string, data: T): void {\n if (!this.enabled) return;\n\n if (this.maxSize > 0 && this.store.size >= this.maxSize) {\n const firstKey = this.store.keys().next().value;\n if (firstKey !== undefined) this.store.delete(firstKey);\n }\n\n this.store.set(key, { data, expiresAt: Date.now() + this.ttl });\n }\n\n /** Remove a specific entry. */\n delete(key: string): boolean {\n return this.store.delete(key);\n }\n\n /** Clear the entire cache. */\n clear(): void {\n this.store.clear();\n }\n\n /** Number of entries currently stored. */\n get size(): number {\n return this.store.size;\n }\n}\n","/**\n * Rate limiter with automatic retry for AniList API.\n *\n * AniList allows 90 requests per minute.\n * When a 429 (Too Many Requests) is received, the client\n * waits for the Retry-After header and retries automatically.\n */\n\nexport interface RateLimitOptions {\n /** Max requests per window (default: 85, conservative under AniList's 90/min) */\n maxRequests?: number;\n /** Window size in milliseconds (default: 60 000 = 1 minute) */\n windowMs?: number;\n /** Max number of retries on 429 responses (default: 3) */\n maxRetries?: number;\n /** Default retry delay in ms when Retry-After header is missing (default: 2000) */\n retryDelayMs?: number;\n /** Disable rate limiting entirely (default: false) */\n enabled?: boolean;\n}\n\nexport class RateLimiter {\n private readonly maxRequests: number;\n private readonly windowMs: number;\n private readonly maxRetries: number;\n private readonly retryDelayMs: number;\n private readonly enabled: boolean;\n\n /** @internal */\n private timestamps: number[] = [];\n\n constructor(options: RateLimitOptions = {}) {\n this.maxRequests = options.maxRequests ?? 85;\n this.windowMs = options.windowMs ?? 60_000;\n this.maxRetries = options.maxRetries ?? 3;\n this.retryDelayMs = options.retryDelayMs ?? 2_000;\n this.enabled = options.enabled ?? true;\n }\n\n /**\n * Wait until it's safe to make a request (respects rate limit window).\n */\n async acquire(): Promise<void> {\n if (!this.enabled) return;\n\n const now = Date.now();\n this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs);\n\n if (this.timestamps.length >= this.maxRequests) {\n const oldest = this.timestamps[0]!;\n const waitMs = this.windowMs - (now - oldest) + 50;\n await this.sleep(waitMs);\n return this.acquire(); // Re-check after waiting\n }\n\n this.timestamps.push(Date.now());\n }\n\n /**\n * Execute a fetch with automatic retry on 429 responses.\n */\n async fetchWithRetry(\n url: string,\n init: RequestInit,\n ): Promise<Response> {\n await this.acquire();\n\n let lastResponse: Response | undefined;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n const res = await fetch(url, init);\n\n if (res.status !== 429) {\n return res;\n }\n\n lastResponse = res;\n\n if (attempt === this.maxRetries) break;\n\n const retryAfter = res.headers.get(\"Retry-After\");\n const delayMs = retryAfter\n ? parseInt(retryAfter, 10) * 1000\n : this.retryDelayMs * (attempt + 1);\n\n await this.sleep(delayMs);\n await this.acquire();\n }\n\n return lastResponse!;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","import {\n QUERY_MEDIA_BY_ID,\n QUERY_MEDIA_SEARCH,\n QUERY_TRENDING,\n QUERY_CHARACTER_BY_ID,\n QUERY_CHARACTER_SEARCH,\n QUERY_STAFF_BY_ID,\n QUERY_STAFF_SEARCH,\n QUERY_USER_BY_ID,\n QUERY_USER_BY_NAME,\n} from \"../queries\";\n\nimport { AniListError } from \"../errors\";\nimport { MemoryCache } from \"../cache\";\nimport { RateLimiter } from \"../rate-limiter\";\n\nimport type {\n AniListClientOptions,\n Media,\n Character,\n Staff,\n User,\n PagedResult,\n SearchMediaOptions,\n SearchCharacterOptions,\n SearchStaffOptions,\n MediaType,\n} from \"../types\";\n\nconst DEFAULT_API_URL = \"https://graphql.anilist.co\";\n\n/**\n * Lightweight AniList GraphQL client with built-in caching and rate limiting.\n *\n * @example\n * ```ts\n * import { AniListClient } from \"ani-client\";\n *\n * const client = new AniListClient();\n * const anime = await client.getMedia(1); // Cowboy Bebop\n * console.log(anime.title.romaji);\n *\n * // Custom cache & rate limit options\n * const client2 = new AniListClient({\n * cache: { ttl: 1000 * 60 * 60 }, // 1 hour cache\n * rateLimit: { maxRequests: 60 },\n * });\n * ```\n */\nexport class AniListClient {\n private readonly apiUrl: string;\n private readonly headers: Record<string, string>;\n private readonly cache: MemoryCache;\n private readonly rateLimiter: RateLimiter;\n\n constructor(options: AniListClientOptions = {}) {\n this.apiUrl = options.apiUrl ?? DEFAULT_API_URL;\n this.headers = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n };\n if (options.token) {\n this.headers[\"Authorization\"] = `Bearer ${options.token}`;\n }\n this.cache = new MemoryCache(options.cache);\n this.rateLimiter = new RateLimiter(options.rateLimit);\n }\n\n /**\n * @internal\n */\n private async request<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {\n const cacheKey = MemoryCache.key(query, variables);\n const cached = this.cache.get<T>(cacheKey);\n if (cached !== undefined) return cached;\n\n const res = await this.rateLimiter.fetchWithRetry(this.apiUrl, {\n method: \"POST\",\n headers: this.headers,\n body: JSON.stringify({ query, variables }),\n });\n\n const json = (await res.json()) as { data?: T; errors?: unknown[] };\n\n if (!res.ok || json.errors) {\n const message =\n (json.errors as Array<{ message?: string }>)?.[0]?.message ??\n `AniList API error (HTTP ${res.status})`;\n throw new AniListError(message, res.status, json.errors ?? []);\n }\n\n const data = json.data as T;\n this.cache.set(cacheKey, data);\n return data;\n }\n\n /**\n * Fetch a single media entry by its AniList ID.\n *\n * @param id - The AniList media ID\n * @returns The media object\n */\n async getMedia(id: number): Promise<Media> {\n const data = await this.request<{ Media: Media }>(QUERY_MEDIA_BY_ID, { id });\n return data.Media;\n }\n\n /**\n * Search for anime or manga.\n *\n * @param options - Search / filter parameters\n * @returns Paginated results with matching media\n *\n * @example\n * ```ts\n * const results = await client.searchMedia({\n * query: \"Naruto\",\n * type: MediaType.ANIME,\n * perPage: 5,\n * });\n * ```\n */\n async searchMedia(options: SearchMediaOptions = {}): Promise<PagedResult<Media>> {\n const variables: Record<string, unknown> = {\n search: options.query,\n type: options.type,\n format: options.format,\n status: options.status,\n season: options.season,\n seasonYear: options.seasonYear,\n genre: options.genre,\n tag: options.tag,\n isAdult: options.isAdult,\n sort: options.sort,\n page: options.page ?? 1,\n perPage: options.perPage ?? 20,\n };\n\n const data = await this.request<{\n Page: { pageInfo: PagedResult<Media>[\"pageInfo\"]; media: Media[] };\n }>(QUERY_MEDIA_SEARCH, variables);\n\n return { pageInfo: data.Page.pageInfo, results: data.Page.media };\n }\n\n /**\n * Get currently trending anime or manga.\n *\n * @param type - `MediaType.ANIME` or `MediaType.MANGA` (defaults to ANIME)\n * @param page - Page number (default 1)\n * @param perPage - Results per page (default 20, max 50)\n */\n async getTrending(\n type: MediaType = \"ANIME\" as MediaType,\n page = 1,\n perPage = 20,\n ): Promise<PagedResult<Media>> {\n const data = await this.request<{\n Page: { pageInfo: PagedResult<Media>[\"pageInfo\"]; media: Media[] };\n }>(QUERY_TRENDING, { type, page, perPage });\n\n return { pageInfo: data.Page.pageInfo, results: data.Page.media };\n }\n\n /**\n * Fetch a character by AniList ID.\n */\n async getCharacter(id: number): Promise<Character> {\n const data = await this.request<{ Character: Character }>(QUERY_CHARACTER_BY_ID, { id });\n return data.Character;\n }\n\n /**\n * Search for characters by name.\n */\n async searchCharacters(options: SearchCharacterOptions = {}): Promise<PagedResult<Character>> {\n const variables: Record<string, unknown> = {\n search: options.query,\n sort: options.sort,\n page: options.page ?? 1,\n perPage: options.perPage ?? 20,\n };\n\n const data = await this.request<{\n Page: { pageInfo: PagedResult<Character>[\"pageInfo\"]; characters: Character[] };\n }>(QUERY_CHARACTER_SEARCH, variables);\n\n return { pageInfo: data.Page.pageInfo, results: data.Page.characters };\n }\n\n /**\n * Fetch a staff member by AniList ID.\n */\n async getStaff(id: number): Promise<Staff> {\n const data = await this.request<{ Staff: Staff }>(QUERY_STAFF_BY_ID, { id });\n return data.Staff;\n }\n\n /**\n * Search for staff (voice actors, directors, etc.).\n */\n async searchStaff(options: SearchStaffOptions = {}): Promise<PagedResult<Staff>> {\n const variables: Record<string, unknown> = {\n search: options.query,\n page: options.page ?? 1,\n perPage: options.perPage ?? 20,\n };\n\n const data = await this.request<{\n Page: { pageInfo: PagedResult<Staff>[\"pageInfo\"]; staff: Staff[] };\n }>(QUERY_STAFF_SEARCH, variables);\n\n return { pageInfo: data.Page.pageInfo, results: data.Page.staff };\n }\n\n /**\n * Fetch a user by AniList ID.\n */\n async getUser(id: number): Promise<User> {\n const data = await this.request<{ User: User }>(QUERY_USER_BY_ID, { id });\n return data.User;\n }\n\n /**\n * Fetch a user by username.\n */\n async getUserByName(name: string): Promise<User> {\n const data = await this.request<{ User: User }>(QUERY_USER_BY_NAME, { name });\n return data.User;\n }\n\n /**\n * Execute an arbitrary GraphQL query against the AniList API.\n * Useful for advanced use-cases not covered by the built-in methods.\n *\n * @param query - A valid GraphQL query string\n * @param variables - Optional variables object\n */\n async raw<T = unknown>(query: string, variables?: Record<string, unknown>): Promise<T> {\n return this.request<T>(query, variables);\n }\n\n /**\n * Clear the entire response cache.\n */\n clearCache(): void {\n this.cache.clear();\n }\n\n /**\n * Number of entries currently in the cache.\n */\n get cacheSize(): number {\n return this.cache.size;\n }\n}\n","export enum MediaType {\n ANIME = \"ANIME\",\n MANGA = \"MANGA\",\n}\n\nexport enum MediaFormat {\n TV = \"TV\",\n TV_SHORT = \"TV_SHORT\",\n MOVIE = \"MOVIE\",\n SPECIAL = \"SPECIAL\",\n OVA = \"OVA\",\n ONA = \"ONA\",\n MUSIC = \"MUSIC\",\n MANGA = \"MANGA\",\n NOVEL = \"NOVEL\",\n ONE_SHOT = \"ONE_SHOT\",\n}\n\nexport enum MediaStatus {\n FINISHED = \"FINISHED\",\n RELEASING = \"RELEASING\",\n NOT_YET_RELEASED = \"NOT_YET_RELEASED\",\n CANCELLED = \"CANCELLED\",\n HIATUS = \"HIATUS\",\n}\n\nexport enum MediaSeason {\n WINTER = \"WINTER\",\n SPRING = \"SPRING\",\n SUMMER = \"SUMMER\",\n FALL = \"FALL\",\n}\n\nexport enum MediaSort {\n ID = \"ID\",\n TITLE_ROMAJI = \"TITLE_ROMAJI\",\n TITLE_ENGLISH = \"TITLE_ENGLISH\",\n TITLE_NATIVE = \"TITLE_NATIVE\",\n TYPE = \"TYPE\",\n FORMAT = \"FORMAT\",\n START_DATE = \"START_DATE\",\n END_DATE = \"END_DATE\",\n SCORE = \"SCORE\",\n POPULARITY = \"POPULARITY\",\n TRENDING = \"TRENDING\",\n EPISODES = \"EPISODES\",\n DURATION = \"DURATION\",\n STATUS = \"STATUS\",\n FAVOURITES = \"FAVOURITES\",\n UPDATED_AT = \"UPDATED_AT\",\n SEARCH_MATCH = \"SEARCH_MATCH\",\n}\n\nexport enum CharacterSort {\n ID = \"ID\",\n ROLE = \"ROLE\",\n SEARCH_MATCH = \"SEARCH_MATCH\",\n FAVOURITES = \"FAVOURITES\",\n}\n\nexport interface MediaTitle {\n romaji: string | null;\n english: string | null;\n native: string | null;\n userPreferred: string | null;\n}\n\nexport interface MediaCoverImage {\n extraLarge: string | null;\n large: string | null;\n medium: string | null;\n color: string | null;\n}\n\nexport interface FuzzyDate {\n year: number | null;\n month: number | null;\n day: number | null;\n}\n\nexport interface MediaTrailer {\n id: string | null;\n site: string | null;\n thumbnail: string | null;\n}\n\nexport interface MediaTag {\n id: number;\n name: string;\n description: string | null;\n category: string | null;\n rank: number | null;\n isMediaSpoiler: boolean | null;\n}\n\nexport interface Studio {\n id: number;\n name: string;\n isAnimationStudio: boolean;\n siteUrl: string | null;\n}\n\nexport interface StudioConnection {\n nodes: Studio[];\n}\n\nexport interface CharacterName {\n first: string | null;\n middle: string | null;\n last: string | null;\n full: string | null;\n native: string | null;\n alternative: string[];\n}\n\nexport interface CharacterImage {\n large: string | null;\n medium: string | null;\n}\n\nexport interface Media {\n id: number;\n idMal: number | null;\n title: MediaTitle;\n type: MediaType;\n format: MediaFormat | null;\n status: MediaStatus | null;\n description: string | null;\n startDate: FuzzyDate | null;\n endDate: FuzzyDate | null;\n season: MediaSeason | null;\n seasonYear: number | null;\n episodes: number | null;\n duration: number | null;\n chapters: number | null;\n volumes: number | null;\n countryOfOrigin: string | null;\n isLicensed: boolean | null;\n source: string | null;\n hashtag: string | null;\n trailer: MediaTrailer | null;\n coverImage: MediaCoverImage;\n bannerImage: string | null;\n genres: string[];\n synonyms: string[];\n averageScore: number | null;\n meanScore: number | null;\n popularity: number | null;\n favourites: number | null;\n trending: number | null;\n tags: MediaTag[];\n studios: StudioConnection;\n isAdult: boolean | null;\n siteUrl: string | null;\n}\n\nexport interface Character {\n id: number;\n name: CharacterName;\n image: CharacterImage;\n description: string | null;\n gender: string | null;\n dateOfBirth: FuzzyDate | null;\n age: string | null;\n bloodType: string | null;\n favourites: number | null;\n siteUrl: string | null;\n media: {\n nodes: Pick<Media, \"id\" | \"title\" | \"type\" | \"coverImage\" | \"siteUrl\">[];\n } | null;\n}\n\nexport interface StaffName {\n first: string | null;\n middle: string | null;\n last: string | null;\n full: string | null;\n native: string | null;\n}\n\nexport interface StaffImage {\n large: string | null;\n medium: string | null;\n}\n\nexport interface Staff {\n id: number;\n name: StaffName;\n language: string | null;\n image: StaffImage;\n description: string | null;\n primaryOccupations: string[];\n gender: string | null;\n dateOfBirth: FuzzyDate | null;\n dateOfDeath: FuzzyDate | null;\n age: number | null;\n yearsActive: number[];\n homeTown: string | null;\n bloodType: string | null;\n favourites: number | null;\n siteUrl: string | null;\n}\n\nexport interface UserAvatar {\n large: string | null;\n medium: string | null;\n}\n\nexport interface UserStatistics {\n count: number;\n meanScore: number;\n minutesWatched: number;\n episodesWatched: number;\n chaptersRead: number;\n volumesRead: number;\n}\n\nexport interface User {\n id: number;\n name: string;\n about: string | null;\n avatar: UserAvatar;\n bannerImage: string | null;\n isFollowing: boolean | null;\n isFollower: boolean | null;\n donatorTier: number | null;\n donatorBadge: string | null;\n createdAt: number | null;\n siteUrl: string | null;\n statistics: {\n anime: UserStatistics;\n manga: UserStatistics;\n } | null;\n}\n\nexport interface PageInfo {\n total: number | null;\n perPage: number | null;\n currentPage: number | null;\n lastPage: number | null;\n hasNextPage: boolean | null;\n}\n\nexport interface SearchMediaOptions {\n query?: string;\n type?: MediaType;\n format?: MediaFormat;\n status?: MediaStatus;\n season?: MediaSeason;\n seasonYear?: number;\n genre?: string;\n tag?: string;\n isAdult?: boolean;\n sort?: MediaSort[];\n page?: number;\n perPage?: number;\n}\n\nexport interface SearchCharacterOptions {\n query?: string;\n sort?: CharacterSort[];\n page?: number;\n perPage?: number;\n}\n\nexport interface SearchStaffOptions {\n query?: string;\n page?: number;\n perPage?: number;\n}\n\nexport interface PagedResult<T> {\n pageInfo: PageInfo;\n results: T[];\n}\n\nexport interface AniListClientOptions {\n /** Optional AniList OAuth token for authenticated requests */\n token?: string;\n /** Custom API endpoint (defaults to https://graphql.anilist.co) */\n apiUrl?: string;\n /** Cache configuration (enabled by default, 24h TTL) */\n cache?: {\n /** Time-to-live in milliseconds (default: 86 400 000 = 24h) */\n ttl?: number;\n /** Maximum number of cached entries (default: 500, 0 = unlimited) */\n maxSize?: number;\n /** Set to false to disable caching entirely */\n enabled?: boolean;\n };\n /** Rate limiter configuration (enabled by default, 85 req/min) */\n rateLimit?: {\n /** Max requests per window (default: 85) */\n maxRequests?: number;\n /** Window size in ms (default: 60 000) */\n windowMs?: number;\n /** Max retries on 429 (default: 3) */\n maxRetries?: number;\n /** Retry delay in ms when Retry-After header is absent (default: 2000) */\n retryDelayMs?: number;\n /** Set to false to disable rate limiting entirely */\n enabled?: boolean;\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "ani-client",
3
+ "version": "1.0.0",
4
+ "description": "A simple and typed client to fetch anime, manga, characters and user data from AniList",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "test": "tsx tests/client.test.ts",
22
+ "prepublishOnly": "pnpm run build"
23
+ },
24
+ "keywords": [
25
+ "anilist",
26
+ "anime",
27
+ "manga",
28
+ "graphql",
29
+ "api",
30
+ "client",
31
+ "typescript"
32
+ ],
33
+ "author": "gonzyui",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/gonzyui/ani-client.git"
38
+ },
39
+ "homepage": "https://github.com/gonzyui/ani-client#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/gonzyui/ani-client/issues"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^25.3.0",
45
+ "tsup": "^8.5.1",
46
+ "tsx": "^4.21.0",
47
+ "typescript": "^5.9.3"
48
+ },
49
+ "packageManager": "pnpm@10.30.0"
50
+ }