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