ani-client 1.1.0 → 1.3.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/README.md CHANGED
@@ -1,9 +1,52 @@
1
1
  # ani-client
2
2
 
3
+ [![CI](https://github.com/gonzyui/ani-client/actions/workflows/ci.yml/badge.svg)](https://github.com/gonzyui/ani-client/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/ani-client)](https://www.npmjs.com/package/ani-client)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
3
7
  > A simple, typed client to fetch anime, manga, character, staff and user data from [AniList](https://anilist.co).
4
8
 
5
- Works in **Node.js ≥ 18**, **Bun**, **Deno** and modern **browsers** anywhere the `fetch` API is available.
6
- Ships ESM + CJS bundles with full **TypeScript** declarations.
9
+ - **Zero dependencies** — uses the native `fetch` API
10
+ - **Universal** Node.js 18, Bun, Deno and modern browsers
11
+ - **Dual format** — ships ESM + CJS with full TypeScript declarations
12
+ - **Built-in caching** — in-memory LRU with optional Redis adapter
13
+ - **Rate-limit aware** — auto-retry on 429, configurable timeout & network error retry
14
+ - **Request deduplication** — identical in-flight requests are coalesced
15
+ - **Event hooks** — observe every request, cache hit, retry and response
16
+ - **Batch queries** — fetch multiple IDs in a single GraphQL call
17
+
18
+ ## Table of contents
19
+
20
+ - [Install](#install)
21
+ - [Quick start](#quick-start)
22
+ - [Client options](#client-options)
23
+ - [API reference](#api-reference)
24
+ - [Media](#media)
25
+ - [Characters](#characters)
26
+ - [Staff](#staff)
27
+ - [Users](#users)
28
+ - [Airing, Chapters & Planning](#airing-chapters--planning)
29
+ - [Season charts](#season-charts)
30
+ - [User media lists](#user-media-lists)
31
+ - [Recommendations](#recommendations)
32
+ - [Relations](#relations)
33
+ - [Studios](#studios)
34
+ - [Genres & Tags](#genres--tags)
35
+ - [Batch queries](#batch-queries)
36
+ - [Raw queries](#raw-queries)
37
+ - [Auto-pagination](#auto-pagination)
38
+ - [Caching](#caching)
39
+ - [Memory cache](#memory-cache)
40
+ - [Redis cache](#redis-cache)
41
+ - [Custom adapter](#custom-adapter)
42
+ - [Cache invalidation](#cache-invalidation)
43
+ - [Rate limiting](#rate-limiting)
44
+ - [Request deduplication](#request-deduplication)
45
+ - [Event hooks](#event-hooks)
46
+ - [Error handling](#error-handling)
47
+ - [Types](#types)
48
+ - [Contributing](#contributing)
49
+ - [License](#license)
7
50
 
8
51
  ## Install
9
52
 
@@ -42,7 +85,8 @@ const trending = await client.getTrending(MediaType.ANIME);
42
85
  console.log(trending.results[0].title.romaji);
43
86
  ```
44
87
 
45
- ### CommonJS
88
+ <details>
89
+ <summary>CommonJS</summary>
46
90
 
47
91
  ```js
48
92
  const { AniListClient } = require("ani-client");
@@ -51,212 +95,404 @@ const client = new AniListClient();
51
95
  client.getMedia(1).then((anime) => console.log(anime.title.romaji));
52
96
  ```
53
97
 
54
- ## API
98
+ </details>
55
99
 
56
- ### `new AniListClient(options?)`
100
+ ## Client options
57
101
 
58
- | Option | Type | Default | Description |
59
- | ----------- | -------- | -------------------------------- | ---------------------------------- |
60
- | `token` | `string` | — | AniList OAuth bearer token |
61
- | `apiUrl` | `string` | `https://graphql.anilist.co` | Custom API endpoint |
62
- | `cache` | `object` | `{ ttl: 86400000, maxSize: 500, enabled: true }` | Cache configuration |
63
- | `rateLimit` | `object` | `{ maxRequests: 85, windowMs: 60000, maxRetries: 3, enabled: true }` | Rate limiter configuration |
102
+ ```ts
103
+ const client = new AniListClient({
104
+ // AniList OAuth bearer token (optional)
105
+ token: "your-token",
106
+
107
+ // Custom API endpoint
108
+ apiUrl: "https://graphql.anilist.co",
109
+
110
+ // In-memory cache settings
111
+ cache: {
112
+ ttl: 86_400_000, // 24 hours (ms)
113
+ maxSize: 500, // max entries (LRU eviction)
114
+ enabled: true,
115
+ },
116
+
117
+ // Or bring your own adapter (takes precedence over `cache`)
118
+ cacheAdapter: new RedisCache({ client: redisClient }),
119
+
120
+ // Rate limiter
121
+ rateLimit: {
122
+ maxRequests: 85, // per window (AniList allows 90)
123
+ windowMs: 60_000, // 1 minute
124
+ maxRetries: 3, // retries on 429
125
+ retryDelayMs: 2_000, // delay between retries
126
+ timeoutMs: 30_000, // per-request timeout (0 = none)
127
+ retryOnNetworkError: true, // retry ECONNRESET, ETIMEDOUT, etc.
128
+ enabled: true,
129
+ },
130
+
131
+ // Event hooks
132
+ hooks: {
133
+ onRequest: (query, variables) => {},
134
+ onResponse: (query, durationMs, fromCache) => {},
135
+ onCacheHit: (key) => {},
136
+ onRateLimit: (retryAfterMs) => {},
137
+ onRetry: (attempt, reason, delayMs) => {},
138
+ },
139
+ });
140
+ ```
141
+
142
+ ## API reference
64
143
 
65
144
  ### Media
66
145
 
67
- | Method | Description |
68
- | ----------------------------------- | ------------------------------------ |
69
- | `getMedia(id: number)` | Fetch a single anime / manga by ID |
70
- | `searchMedia(options?)` | Search & filter anime / manga |
71
- | `getTrending(type?, page?, perPage?)` | Get currently trending entries |
146
+ | Method | Description |
147
+ | --- | --- |
148
+ | `getMedia(id)` | Fetch a single anime / manga by ID |
149
+ | `searchMedia(options?)` | Search & filter anime / manga |
150
+ | `getTrending(type?, page?, perPage?)` | Currently trending entries |
151
+ | `getMediaBySeason(options)` | Anime/manga for a given season & year |
152
+ | `getRecommendations(mediaId, options?)` | User recommendations for a media |
153
+
154
+ ```ts
155
+ const anime = await client.getMedia(1);
156
+
157
+ const results = await client.searchMedia({
158
+ query: "Naruto",
159
+ type: MediaType.ANIME,
160
+ format: MediaFormat.TV,
161
+ genre: "Action",
162
+ perPage: 10,
163
+ });
164
+ ```
72
165
 
73
166
  ### Characters
74
167
 
75
- | Method | Description |
76
- | ----------------------------------- | ------------------------------------ |
77
- | `getCharacter(id: number)` | Fetch a character by ID |
78
- | `searchCharacters(options?)` | Search characters by name |
168
+ | Method | Description |
169
+ | --- | --- |
170
+ | `getCharacter(id)` | Fetch a character by ID |
171
+ | `searchCharacters(options?)` | Search characters by name |
172
+
173
+ ```ts
174
+ const spike = await client.getCharacter(1);
175
+ const results = await client.searchCharacters({ query: "Luffy", perPage: 5 });
176
+ ```
79
177
 
80
178
  ### Staff
81
179
 
82
- | Method | Description |
83
- | ----------------------------------- | ------------------------------------ |
84
- | `getStaff(id: number)` | Fetch a staff member by ID |
85
- | `searchStaff(options?)` | Search for staff members |
180
+ | Method | Description |
181
+ | --- | --- |
182
+ | `getStaff(id)` | Fetch a staff member by ID |
183
+ | `searchStaff(options?)` | Search for staff members |
184
+
185
+ ```ts
186
+ const staff = await client.getStaff(95001);
187
+ const results = await client.searchStaff({ query: "Miyazaki" });
188
+ ```
86
189
 
87
190
  ### Users
88
191
 
89
- | Method | Description |
90
- | ----------------------------------- | ------------------------------------ |
91
- | `getUser(id: number)` | Fetch a user by ID |
92
- | `getUserByName(name: string)` | Fetch a user by username |
192
+ | Method | Description |
193
+ | --- | --- |
194
+ | `getUser(id)` | Fetch a user by ID |
195
+ | `getUserByName(name)` | Fetch a user by username |
196
+ | `getUserMediaList(options)` | Get a user's anime or manga list |
93
197
 
94
198
  ### Airing, Chapters & Planning
95
199
 
96
- | Method | Description |
97
- | ----------------------------------- | ------------------------------------ |
98
- | `getAiredEpisodes(options?)` | Recently aired anime episodes (last 24 h by default) |
99
- | `getAiredChapters(options?)` | Recently updated releasing manga |
100
- | `getPlanning(options?)` | Upcoming not-yet-released anime / manga |
200
+ | Method | Description |
201
+ | --- | --- |
202
+ | `getAiredEpisodes(options?)` | Recently aired anime episodes (last 24 h by default) |
203
+ | `getAiredChapters(options?)` | Recently updated releasing manga |
204
+ | `getPlanning(options?)` | Upcoming not-yet-released anime / manga |
101
205
 
102
- ### Raw queries
206
+ ```ts
207
+ // Episodes aired in the last 7 days
208
+ const week = await client.getAiredEpisodes({
209
+ airingAtGreater: Math.floor(Date.now() / 1000) - 7 * 24 * 3600,
210
+ perPage: 50,
211
+ });
103
212
 
104
- | Method | Description |
105
- | ----------------------------------- | ------------------------------------ |
106
- | `raw<T>(query, variables?)` | Execute any GraphQL query |
213
+ // Most anticipated upcoming anime
214
+ const upcoming = await client.getPlanning({
215
+ type: MediaType.ANIME,
216
+ perPage: 10,
217
+ });
218
+ ```
107
219
 
108
- ### Cache management
220
+ ### Season charts
109
221
 
110
- | Method / Property | Description |
111
- | ----------------------------------- | ------------------------------------ |
112
- | `clearCache()` | Clear the entire response cache |
113
- | `cacheSize` | Number of entries currently cached |
222
+ ```ts
223
+ import { MediaSeason } from "ani-client";
114
224
 
115
- ## Caching
225
+ const winter2026 = await client.getMediaBySeason({
226
+ season: MediaSeason.WINTER,
227
+ seasonYear: 2026,
228
+ perPage: 25,
229
+ });
230
+ ```
231
+
232
+ | Option | Type | Default | Description |
233
+ | --- | --- | --- | --- |
234
+ | `season` | `MediaSeason` | **(required)** | WINTER, SPRING, SUMMER, or FALL |
235
+ | `seasonYear` | `number` | **(required)** | The year |
236
+ | `type` | `MediaType` | `ANIME` | Filter by ANIME or MANGA |
237
+ | `sort` | `MediaSort[]` | `["POPULARITY_DESC"]` | Sort order |
238
+ | `page` | `number` | `1` | Page number |
239
+ | `perPage` | `number` | `20` | Results per page (max 50) |
116
240
 
117
- All responses are cached in-memory by default (**24 hours TTL**, max 500 entries). Configure it per-client:
241
+ ### User media lists
118
242
 
119
243
  ```ts
120
- const client = new AniListClient({
121
- cache: {
122
- ttl: 1000 * 60 * 60, // 1 hour
123
- maxSize: 200, // keep at most 200 entries
124
- enabled: true, // set to false to disable
125
- },
244
+ import { MediaType, MediaListStatus } from "ani-client";
245
+
246
+ const list = await client.getUserMediaList({
247
+ userName: "AniList",
248
+ type: MediaType.ANIME,
249
+ status: MediaListStatus.COMPLETED,
126
250
  });
251
+ list.results.forEach((entry) =>
252
+ console.log(`${entry.media.title.romaji} — ${entry.score}/100`)
253
+ );
254
+ ```
255
+
256
+ Provide either `userId` or `userName`. The `type` field is required.
127
257
 
128
- // Manual cache control
129
- client.clearCache();
130
- console.log(client.cacheSize); // 0
258
+ ### Recommendations
259
+
260
+ ```ts
261
+ const recs = await client.getRecommendations(1); // Cowboy Bebop
262
+ recs.results.forEach((r) =>
263
+ console.log(`${r.mediaRecommendation.title.romaji} (rating: ${r.rating})`)
264
+ );
131
265
  ```
132
266
 
133
- ## Rate limiting
267
+ ### Relations
134
268
 
135
- The client automatically respects AniList's rate limit (90 req/min) with a conservative default of **85 req/min**. If you hit a `429 Too Many Requests`, it retries automatically (up to 3 times with backoff).
269
+ Every media object includes a `relations` field with sequels, prequels, spin-offs, etc.
136
270
 
137
271
  ```ts
138
- const client = new AniListClient({
139
- rateLimit: {
140
- maxRequests: 60, // be extra conservative
141
- windowMs: 60_000, // 1 minute window
142
- maxRetries: 5, // retry up to 5 times on 429
143
- retryDelayMs: 3000, // wait 3s between retries
144
- enabled: true, // set to false to disable
145
- },
146
- });
272
+ const anime = await client.getMedia(1);
273
+ anime.relations?.edges.forEach((edge) =>
274
+ console.log(`${edge.relationType}: ${edge.node.title.romaji}`)
275
+ );
147
276
  ```
148
277
 
149
- ## Airing, Chapters & Planning
278
+ Available types: `ADAPTATION`, `PREQUEL`, `SEQUEL`, `PARENT`, `SIDE_STORY`, `CHARACTER`, `SUMMARY`, `ALTERNATIVE`, `SPIN_OFF`, `OTHER`, `SOURCE`, `COMPILATION`, `CONTAINS`.
150
279
 
151
- Fetch recently released content or see what's coming up next.
280
+ ### Studios
152
281
 
153
- ### `getAiredEpisodes(options?)`
282
+ | Method | Description |
283
+ | --- | --- |
284
+ | `getStudio(id)` | Fetch a studio with its productions |
285
+ | `searchStudios(options?)` | Search studios by name |
154
286
 
155
- Returns anime episodes that recently aired (defaults to the **last 24 hours**).
287
+ ```ts
288
+ const studio = await client.getStudio(44); // Bones
289
+ const results = await client.searchStudios({ query: "MAPPA" });
290
+ ```
156
291
 
157
- | Option | Type | Default | Description |
158
- | ----------------- | -------------- | -------------------- | -------------------------------------------- |
159
- | `airingAtGreater` | `number` | `now - 24h` (UNIX) | Only episodes aired after this timestamp |
160
- | `airingAtLesser` | `number` | `now` (UNIX) | Only episodes aired before this timestamp |
161
- | `sort` | `AiringSort[]` | `["TIME_DESC"]` | Sort order |
162
- | `page` | `number` | `1` | Page number |
163
- | `perPage` | `number` | `20` | Results per page (max 50) |
292
+ ### Genres & Tags
164
293
 
165
294
  ```ts
166
- import { AniListClient } from "ani-client";
295
+ const genres = await client.getGenres();
296
+ // ["Action", "Adventure", "Comedy", ...]
167
297
 
168
- const client = new AniListClient();
298
+ const tags = await client.getTags();
299
+ // [{ id, name, description, category, isAdult }, ...]
300
+ ```
301
+
302
+ ### Batch queries
303
+
304
+ Fetch multiple IDs in a single GraphQL request (up to 50 per call, auto-chunked).
305
+
306
+ | Method | Description |
307
+ | --- | --- |
308
+ | `getMediaBatch(ids)` | Fetch multiple anime / manga |
309
+ | `getCharacterBatch(ids)` | Fetch multiple characters |
310
+ | `getStaffBatch(ids)` | Fetch multiple staff members |
311
+
312
+ ```ts
313
+ const [bebop, naruto, aot] = await client.getMediaBatch([1, 20, 16498]);
314
+ ```
315
+
316
+ ### Raw queries
169
317
 
170
- // Episodes aired in the last 24 hours
171
- const recent = await client.getAiredEpisodes();
172
- recent.results.forEach((ep) =>
173
- console.log(`${ep.media.title.romaji} Episode ${ep.episode}`)
318
+ Execute any GraphQL query against the AniList API.
319
+
320
+ ```ts
321
+ const data = await client.raw<{ Media: { id: number; title: { romaji: string } } }>(
322
+ "query { Media(id: 1) { id title { romaji } } }",
174
323
  );
324
+ ```
175
325
 
176
- // Episodes aired in the last 7 days
177
- const week = await client.getAiredEpisodes({
178
- airingAtGreater: Math.floor(Date.now() / 1000) - 7 * 24 * 3600,
179
- perPage: 50,
326
+ ### Auto-pagination
327
+
328
+ `paginate()` returns an async iterator that fetches pages on demand.
329
+
330
+ ```ts
331
+ for await (const anime of client.paginate(
332
+ (page) => client.searchMedia({ query: "Gundam", page, perPage: 10 }),
333
+ 3, // max 3 pages
334
+ )) {
335
+ console.log(anime.title.romaji);
336
+ }
337
+ ```
338
+
339
+ ## Caching
340
+
341
+ ### Memory cache
342
+
343
+ The default in-memory cache uses **LRU eviction** (24 h TTL, 500 entries max).
344
+
345
+ ```ts
346
+ const client = new AniListClient({
347
+ cache: {
348
+ ttl: 1000 * 60 * 60, // 1 hour
349
+ maxSize: 200,
350
+ enabled: true, // false to disable
351
+ },
352
+ });
353
+ ```
354
+
355
+ ### Redis cache
356
+
357
+ For distributed or persistent caching, use the built-in Redis adapter. Compatible with [ioredis](https://github.com/redis/ioredis) and [node-redis](https://github.com/redis/node-redis) v4+.
358
+
359
+ ```ts
360
+ import Redis from "ioredis";
361
+ import { AniListClient, RedisCache } from "ani-client";
362
+
363
+ const client = new AniListClient({
364
+ cacheAdapter: new RedisCache({
365
+ client: new Redis(),
366
+ prefix: "ani:", // key prefix (default)
367
+ ttl: 86_400, // seconds (default: 24 h)
368
+ }),
180
369
  });
181
370
  ```
182
371
 
183
- ### `getAiredChapters(options?)`
372
+ ### Custom adapter
184
373
 
185
- Returns currently releasing manga, sorted by most recently updated — the closest proxy to "recently released chapters" (AniList does not expose individual chapter schedules).
374
+ Implement the `CacheAdapter` interface to bring your own storage:
375
+
376
+ ```ts
377
+ import type { CacheAdapter } from "ani-client";
378
+
379
+ class MyCache implements CacheAdapter {
380
+ get<T>(key: string): T | undefined | Promise<T | undefined> { /* ... */ }
381
+ set<T>(key: string, data: T): void | Promise<void> { /* ... */ }
382
+ delete(key: string): boolean | Promise<boolean> { /* ... */ }
383
+ clear(): void | Promise<void> { /* ... */ }
384
+ get size(): number { return -1; } // return -1 if unknown
385
+ keys(): string[] | Promise<string[]> { /* ... */ }
386
+ // Optional — the client provides a fallback if omitted
387
+ invalidate?(pattern: string | RegExp): number | Promise<number> { /* ... */ }
388
+ }
389
+ ```
186
390
 
187
- | Option | Type | Default | Description |
188
- | --------- | -------- | ------- | ----------------------- |
189
- | `page` | `number` | `1` | Page number |
190
- | `perPage` | `number` | `20` | Results per page (max 50) |
391
+ ### Cache invalidation
191
392
 
192
393
  ```ts
193
- const chapters = await client.getAiredChapters({ perPage: 10 });
194
- chapters.results.forEach((m) =>
195
- console.log(`${m.title.romaji} — ${m.chapters ?? "?"} chapters`)
196
- );
394
+ // Clear everything
395
+ await client.clearCache();
396
+
397
+ // Remove entries matching a pattern
398
+ const removed = await client.invalidateCache(/Media/);
399
+ console.log(`Removed ${removed} entries`);
400
+
401
+ // Current cache size
402
+ console.log(client.cacheSize);
197
403
  ```
198
404
 
199
- ### `getPlanning(options?)`
405
+ ## Rate limiting
200
406
 
201
- Returns anime and/or manga that are **not yet released**, sorted by popularity.
407
+ The client respects AniList's rate limit (90 req/min) with a conservative default of **85 req/min**. On HTTP 429, it retries automatically with progressive backoff.
202
408
 
203
- | Option | Type | Default | Description |
204
- | --------- | ------------- | -------------------- | ---------------------------------- |
205
- | `type` | `MediaType` | — | Filter by ANIME or MANGA (both if omitted) |
206
- | `sort` | `MediaSort[]` | `["POPULARITY_DESC"]` | Sort order |
207
- | `page` | `number` | `1` | Page number |
208
- | `perPage` | `number` | `20` | Results per page (max 50) |
409
+ ```ts
410
+ const client = new AniListClient({
411
+ rateLimit: {
412
+ maxRequests: 60,
413
+ windowMs: 60_000,
414
+ maxRetries: 5,
415
+ retryDelayMs: 3_000,
416
+ timeoutMs: 30_000, // abort after 30 s
417
+ retryOnNetworkError: true, // retry ECONNRESET, ETIMEDOUT, etc.
418
+ enabled: true,
419
+ },
420
+ });
421
+ ```
422
+
423
+ ## Request deduplication
424
+
425
+ When multiple callers request the same data at the same time, only one API call is made. All callers receive the same response.
209
426
 
210
427
  ```ts
211
- import { MediaType } from "ani-client";
428
+ // Only 1 HTTP request is sent
429
+ const [a, b] = await Promise.all([
430
+ client.getMedia(1),
431
+ client.getMedia(1),
432
+ ]);
433
+ ```
212
434
 
213
- // Most anticipated upcoming anime
214
- const upcoming = await client.getPlanning({ type: MediaType.ANIME, perPage: 10 });
215
- upcoming.results.forEach((m) => console.log(m.title.romaji));
435
+ ## Event hooks
216
436
 
217
- // All upcoming media (anime + manga)
218
- const all = await client.getPlanning();
437
+ Monitor every request lifecycle event for logging, metrics, or debugging.
438
+
439
+ ```ts
440
+ const client = new AniListClient({
441
+ hooks: {
442
+ onRequest: (query, variables) => console.log("→", query.slice(0, 40)),
443
+ onResponse: (query, durationMs, fromCache) => console.log(`← ${durationMs}ms (cache: ${fromCache})`),
444
+ onCacheHit: (key) => console.log("Cache hit:", key.slice(0, 30)),
445
+ onRateLimit: (retryAfterMs) => console.warn(`Rate limited, waiting ${retryAfterMs}ms`),
446
+ onRetry: (attempt, reason, delayMs) => console.warn(`Retry #${attempt}: ${reason}`),
447
+ },
448
+ });
219
449
  ```
220
450
 
221
451
  ## Error handling
222
452
 
223
- All API errors throw an `AniListError` with:
224
-
225
- - `message` — Human-readable error message
226
- - `status` — HTTP status code
227
- - `errors` — Raw error array from the API
453
+ All API errors throw an `AniListError` with `message`, `status` and `errors`:
228
454
 
229
455
  ```ts
230
- import { AniListClient, AniListError } from "ani-client";
456
+ import { AniListError } from "ani-client";
231
457
 
232
458
  try {
233
459
  await client.getMedia(999999999);
234
460
  } catch (err) {
235
461
  if (err instanceof AniListError) {
236
- console.error(err.message, err.status);
462
+ console.error(err.message); // "Not Found."
463
+ console.error(err.status); // 404
464
+ console.error(err.errors); // raw API error array
237
465
  }
238
466
  }
239
467
  ```
240
468
 
241
469
  ## Types
242
470
 
243
- All types are exported and documented:
471
+ All types and enums are exported:
244
472
 
245
473
  ```ts
246
474
  import type {
247
- Media,
248
- Character,
249
- Staff,
250
- User,
251
- AiringSchedule,
252
- PagedResult,
253
- SearchMediaOptions,
254
- GetAiringOptions,
255
- GetRecentChaptersOptions,
256
- GetPlanningOptions,
475
+ Media, Character, Staff, User,
476
+ AiringSchedule, MediaListEntry, Recommendation, StudioDetail,
477
+ MediaEdge, MediaConnection, PageInfo, PagedResult,
478
+ CacheAdapter, AniListHooks, AniListClientOptions,
479
+ SearchMediaOptions, SearchCharacterOptions, SearchStaffOptions,
480
+ SearchStudioOptions, GetAiringOptions, GetRecentChaptersOptions,
481
+ GetPlanningOptions, GetSeasonOptions, GetUserMediaListOptions,
482
+ GetRecommendationsOptions,
483
+ } from "ani-client";
484
+
485
+ import {
486
+ MediaType, MediaFormat, MediaStatus, MediaSeason, MediaSort,
487
+ CharacterSort, AiringSort, RecommendationSort,
488
+ MediaRelationType, MediaListStatus, MediaListSort,
257
489
  } from "ani-client";
258
490
  ```
259
491
 
492
+ ## Contributing
493
+
494
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding standards, and how to submit changes.
495
+
260
496
  ## License
261
497
 
262
498
  [MIT](LICENSE) © gonzyui