anipub 1.0.4
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/LICENSE +21 -0
- package/README.md +686 -0
- package/package.json +58 -0
- package/src/index.cjs +385 -0
- package/src/index.d.ts +154 -0
- package/src/index.js +368 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AniPub
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# anipub
|
|
4
|
+
|
|
5
|
+
**A full-featured JavaScript/TypeScript client for the [AniPub Anime API](https://api.anipub.xyz)**
|
|
6
|
+
|
|
7
|
+
Search · Browse · Stream · MAL Data · Characters · Voice Actors
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/anipub)
|
|
10
|
+
[](https://www.npmjs.com/package/anipub)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](https://nodejs.org)
|
|
13
|
+
[](src/index.d.ts)
|
|
14
|
+
[](#)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What is this?
|
|
21
|
+
|
|
22
|
+
`anipub` is a zero-dependency, isomorphic JavaScript wrapper for the [AniPub API](https://api.anipub.xyz) — a free, open anime metadata service with MAL integration. It covers **all 10 endpoints** in one clean package with full TypeScript support.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install anipub
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { search, getInfo, getTopRated } from 'anipub';
|
|
30
|
+
|
|
31
|
+
const results = await search('One Piece');
|
|
32
|
+
const anime = await getInfo('black-clover');
|
|
33
|
+
const top = await getTopRated();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> **No API key. No account. No rate limits enforced by this wrapper.**
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Table of Contents
|
|
41
|
+
|
|
42
|
+
- [Installation](#installation)
|
|
43
|
+
- [Quick Start](#quick-start)
|
|
44
|
+
- [API Reference](#api-reference)
|
|
45
|
+
- [getInfo](#getinfoidorslug)
|
|
46
|
+
- [getTotal](#gettotal)
|
|
47
|
+
- [findByName](#findbynamename)
|
|
48
|
+
- [search](#searchquery)
|
|
49
|
+
- [searchAll](#searchallquery-page)
|
|
50
|
+
- [findByGenre](#findbygenregenre-page)
|
|
51
|
+
- [checkAnime](#checkanimename-genre)
|
|
52
|
+
- [getTopRated](#gettopratedpage)
|
|
53
|
+
- [getStreamingLinks](#getstreaminglinksid-options)
|
|
54
|
+
- [getFullDetails](#getfulldetailsid)
|
|
55
|
+
- [Class-based Usage](#class-based-usage)
|
|
56
|
+
- [TypeScript](#typescript)
|
|
57
|
+
- [Error Handling](#error-handling)
|
|
58
|
+
- [Real-World Patterns](#real-world-patterns)
|
|
59
|
+
- [Publishing to NPM & GitHub](#publishing-to-npm--github)
|
|
60
|
+
- [License](#license)
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# npm
|
|
68
|
+
npm install anipub
|
|
69
|
+
|
|
70
|
+
# pnpm
|
|
71
|
+
pnpm add anipub
|
|
72
|
+
|
|
73
|
+
# yarn
|
|
74
|
+
yarn add anipub
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Requirements:** Node.js 18+ (uses native `fetch`). Works in Deno and modern browsers too.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { search, getInfo, getTopRated, getFullDetails } from 'anipub';
|
|
85
|
+
|
|
86
|
+
// 1. Search for anime
|
|
87
|
+
const results = await search('attack on titan');
|
|
88
|
+
console.log(results[0].Name); // "Attack on Titan"
|
|
89
|
+
console.log(results[0].Id); // 7
|
|
90
|
+
|
|
91
|
+
// 2. Get full metadata by ID or slug
|
|
92
|
+
const anime = await getInfo(7);
|
|
93
|
+
const same = await getInfo('attack-on-titan'); // same result
|
|
94
|
+
|
|
95
|
+
console.log(anime.Name); // "Attack on Titan"
|
|
96
|
+
console.log(anime.MALScore); // "9.00"
|
|
97
|
+
console.log(anime.Genres); // ["action", "drama", "fantasy"]
|
|
98
|
+
console.log(anime.epCount); // 75
|
|
99
|
+
console.log(anime.ImagePath); // "https://anipub.xyz/..." (always absolute)
|
|
100
|
+
|
|
101
|
+
// 3. Top rated
|
|
102
|
+
const { AniData } = await getTopRated();
|
|
103
|
+
AniData.forEach(a => console.log(`${a.MALScore} — ${a.Name}`));
|
|
104
|
+
|
|
105
|
+
// 4. Characters + MAL synopsis
|
|
106
|
+
const { local, jikan, characters } = await getFullDetails(7);
|
|
107
|
+
console.log(jikan.synopsis);
|
|
108
|
+
characters.filter(c => c.role === 'Main').forEach(c => {
|
|
109
|
+
const va = c.voice_actors.find(v => v.language === 'Japanese');
|
|
110
|
+
console.log(`${c.character.name} — VA: ${va?.person.name}`);
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
All functions are `async` and return parsed JSON. Image paths (`ImagePath`, `Cover`, `Image`) are **automatically resolved to absolute URLs** — no manual string manipulation needed.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### `getInfo(idOrSlug)`
|
|
123
|
+
|
|
124
|
+
> `GET /api/info/:id`
|
|
125
|
+
|
|
126
|
+
Full metadata for one anime. Accepts a numeric **ID** or a **kebab-case slug**.
|
|
127
|
+
|
|
128
|
+
**Slug rules:** lowercase, spaces → hyphens, strip special characters.
|
|
129
|
+
`"One Piece"` → `"one-piece"` · `"High School DxD"` → `"high-school-dxd"`
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import { getInfo } from 'anipub';
|
|
133
|
+
|
|
134
|
+
// By integer ID
|
|
135
|
+
const anime = await getInfo(61);
|
|
136
|
+
|
|
137
|
+
// By slug — spaces become hyphens, lowercase, no special chars
|
|
138
|
+
const anime = await getInfo('black-clover');
|
|
139
|
+
const anime = await getInfo('one-piece');
|
|
140
|
+
const anime = await getInfo('high-school-dxd');
|
|
141
|
+
const anime = await getInfo('date-a-live-iv');
|
|
142
|
+
|
|
143
|
+
console.log(anime.Name); // "Black Clover"
|
|
144
|
+
console.log(anime.MALScore); // "8.88"
|
|
145
|
+
console.log(anime.epCount); // 170
|
|
146
|
+
console.log(anime.Status); // "Finished Airing"
|
|
147
|
+
console.log(anime.Genres); // ["action", "fantasy", "magic"]
|
|
148
|
+
console.log(anime.Aired); // "Oct 3, 2017 to Mar 30, 2021"
|
|
149
|
+
console.log(anime.Studios); // "Pierrot"
|
|
150
|
+
console.log(anime.ImagePath); // "https://anipub.xyz/..." (always absolute)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Returns:** `AnimeInfo`
|
|
154
|
+
|
|
155
|
+
| Field | Type | Description |
|
|
156
|
+
|-------|------|-------------|
|
|
157
|
+
| `_id` | `number` | Numeric ID |
|
|
158
|
+
| `Name` | `string` | Anime title |
|
|
159
|
+
| `ImagePath` | `string` | Poster image (absolute URL) |
|
|
160
|
+
| `Cover` | `string` | Banner image (absolute URL) |
|
|
161
|
+
| `MALScore` | `string` | MyAnimeList score |
|
|
162
|
+
| `Genres` | `string[]` | Genre tags |
|
|
163
|
+
| `Status` | `string` | Airing status |
|
|
164
|
+
| `epCount` | `number` | Episode count |
|
|
165
|
+
| `Aired` | `string` | Airing date range |
|
|
166
|
+
| `Studios` | `string` | Production studios |
|
|
167
|
+
| `DescripTion` | `string` | Synopsis |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### `getTotal()`
|
|
172
|
+
|
|
173
|
+
> `GET /api/getAll`
|
|
174
|
+
|
|
175
|
+
Returns the **total number of anime** in the database. Use this to determine the valid integer ID range.
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
import { getTotal } from 'anipub';
|
|
179
|
+
|
|
180
|
+
const total = await getTotal();
|
|
181
|
+
console.log(`${total} anime available (IDs 1 to ${total})`);
|
|
182
|
+
// → 153 anime available (IDs 1 to 153)
|
|
183
|
+
|
|
184
|
+
// Use it to fetch a random anime
|
|
185
|
+
const randomId = Math.ceil(Math.random() * total);
|
|
186
|
+
const random = await getInfo(randomId);
|
|
187
|
+
console.log(`Random: ${random.Name}`);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Returns:** `number`
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### `findByName(name)`
|
|
195
|
+
|
|
196
|
+
> `GET /api/find/:name`
|
|
197
|
+
|
|
198
|
+
Check if an anime **exists by exact title**. Returns existence status, ID, and episode count.
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
import { findByName, getInfo } from 'anipub';
|
|
202
|
+
|
|
203
|
+
const result = await findByName('One Piece');
|
|
204
|
+
// → { exist: true, id: 10, ep: 1155 }
|
|
205
|
+
|
|
206
|
+
if (result.exist) {
|
|
207
|
+
console.log(`Found! ID: ${result.id}, Episodes: ${result.ep}`);
|
|
208
|
+
const anime = await getInfo(result.id); // fetch full data
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Non-existent
|
|
212
|
+
const none = await findByName('FakeAnime99999');
|
|
213
|
+
// → { exist: false }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Returns:** `FindResult`
|
|
217
|
+
|
|
218
|
+
| Field | Type | Description |
|
|
219
|
+
|-------|------|-------------|
|
|
220
|
+
| `exist` | `boolean` | Whether the anime was found |
|
|
221
|
+
| `id` | `number?` | Numeric ID (if found) |
|
|
222
|
+
| `ep` | `number?` | Episode count (if found) |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### `search(query)`
|
|
227
|
+
|
|
228
|
+
> `GET /api/search/:name`
|
|
229
|
+
|
|
230
|
+
**Quick search** — returns a flat array of results. No pagination. Fastest endpoint; ideal for autocomplete and live search inputs.
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
import { search } from 'anipub';
|
|
234
|
+
|
|
235
|
+
const results = await search('naruto');
|
|
236
|
+
// → [{ Name, Id, Image, finder }, ...]
|
|
237
|
+
|
|
238
|
+
results.forEach(r => {
|
|
239
|
+
console.log(`[${r.Id}] ${r.Name}`);
|
|
240
|
+
// → [1] Naruto
|
|
241
|
+
// → [2] Naruto: Shippuden
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Autocomplete example
|
|
245
|
+
input.addEventListener('input', async (e) => {
|
|
246
|
+
if (e.target.value.length < 2) return;
|
|
247
|
+
const hits = await search(e.target.value);
|
|
248
|
+
renderDropdown(hits.slice(0, 8));
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Returns:** `SearchResult[]`
|
|
253
|
+
|
|
254
|
+
| Field | Type | Description |
|
|
255
|
+
|-------|------|-------------|
|
|
256
|
+
| `Name` | `string` | Anime title |
|
|
257
|
+
| `Id` | `number` | Numeric ID |
|
|
258
|
+
| `Image` | `string` | Poster image (absolute URL) |
|
|
259
|
+
| `finder` | `string` | Kebab-case slug |
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### `searchAll(query, page?)`
|
|
264
|
+
|
|
265
|
+
> `GET /api/searchall/:name?page=1`
|
|
266
|
+
|
|
267
|
+
**Full paginated search** with complete anime objects. Returns more results than `search()`.
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
import { searchAll } from 'anipub';
|
|
271
|
+
|
|
272
|
+
const { AniData, currentPage } = await searchAll('sword art online', 1);
|
|
273
|
+
console.log(`Page ${currentPage}, ${AniData.length} results`);
|
|
274
|
+
|
|
275
|
+
AniData.forEach(a => {
|
|
276
|
+
console.log(`[${a._id}] ${a.Name} — Score: ${a.MALScore}`);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Load page 2
|
|
280
|
+
const page2 = await searchAll('sword art online', 2);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Returns:** `SearchAllResult`
|
|
284
|
+
|
|
285
|
+
| Field | Type | Description |
|
|
286
|
+
|-------|------|-------------|
|
|
287
|
+
| `currentPage` | `number` | Current page number |
|
|
288
|
+
| `AniData` | `AnimeInfo[]` | Array of full anime objects |
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### `findByGenre(genre, page?)`
|
|
293
|
+
|
|
294
|
+
> `GET /api/findbyGenre/:genre?Page=1`
|
|
295
|
+
|
|
296
|
+
Paginated anime list filtered by genre.
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
import { findByGenre } from 'anipub';
|
|
300
|
+
|
|
301
|
+
const { currentPage, wholePage } = await findByGenre('action', 1);
|
|
302
|
+
|
|
303
|
+
wholePage.forEach(a => {
|
|
304
|
+
console.log(`${a.Name} — ${a.MALScore}`);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Page 2
|
|
308
|
+
const next = await findByGenre('harem', 2);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Common genres:**
|
|
312
|
+
|
|
313
|
+
| | | | |
|
|
314
|
+
|--|--|--|--|
|
|
315
|
+
| `action` | `romance` | `harem` | `ecchi` |
|
|
316
|
+
| `fantasy` | `school` | `drama` | `supernatural` |
|
|
317
|
+
| `comedy` | `adventure` | `shounen` | `magic` |
|
|
318
|
+
|
|
319
|
+
**Returns:** `GenreResult`
|
|
320
|
+
|
|
321
|
+
| Field | Type | Description |
|
|
322
|
+
|-------|------|-------------|
|
|
323
|
+
| `currentPage` | `number` | Current page number |
|
|
324
|
+
| `wholePage` | `AnimeInfo[]` | Array of anime objects |
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
### `checkAnime(name, genre)`
|
|
329
|
+
|
|
330
|
+
> `POST /api/check`
|
|
331
|
+
|
|
332
|
+
Verify an anime exists with a specific **name and genre**. Genre accepts a string or an array.
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
import { checkAnime } from 'anipub';
|
|
336
|
+
|
|
337
|
+
// Single genre
|
|
338
|
+
const result = await checkAnime('Black Clover', 'Action');
|
|
339
|
+
|
|
340
|
+
// Multiple genres
|
|
341
|
+
const result = await checkAnime('Jujutsu Kaisen', ['Action', 'Drama']);
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
### `getTopRated(page?)`
|
|
347
|
+
|
|
348
|
+
> `GET /api/findbyrating?page=1`
|
|
349
|
+
|
|
350
|
+
Top-rated anime sorted by **MAL score descending**, paginated.
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
import { getTopRated } from 'anipub';
|
|
354
|
+
|
|
355
|
+
const { AniData, currentPage } = await getTopRated(1);
|
|
356
|
+
|
|
357
|
+
AniData.forEach((a, i) => {
|
|
358
|
+
console.log(`${i + 1}. ${a.MALScore} — ${a.Name}`);
|
|
359
|
+
// 1. 9.36 — Frieren: Beyond Journey's End
|
|
360
|
+
// 2. 9.21 — Fullmetal Alchemist: Brotherhood
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Page 2
|
|
364
|
+
const more = await getTopRated(2);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Returns:** `RatingResult`
|
|
368
|
+
|
|
369
|
+
| Field | Type | Description |
|
|
370
|
+
|-------|------|-------------|
|
|
371
|
+
| `currentPage` | `number` | Current page number |
|
|
372
|
+
| `AniData` | `AnimeInfo[]` | Anime sorted by score desc |
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### `getStreamingLinks(id, options?)`
|
|
377
|
+
|
|
378
|
+
> `GET /v1/api/details/:id`
|
|
379
|
+
|
|
380
|
+
Returns **streaming iframe links** organized by episode number. The `src=` prefix is stripped automatically.
|
|
381
|
+
|
|
382
|
+
> **Note on episode numbering:** The raw API has an offset quirk — `local.link` = EP1, `local.ep[0]` = EP2. This wrapper normalizes everything into a clean `episodes` array starting at episode 1. No manual offset needed.
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
import { getStreamingLinks } from 'anipub';
|
|
386
|
+
|
|
387
|
+
const { episodes } = await getStreamingLinks(119);
|
|
388
|
+
// episodes = [
|
|
389
|
+
// { ep: 1, src: 'https://...' },
|
|
390
|
+
// { ep: 2, src: 'https://...' },
|
|
391
|
+
// { ep: 3, src: 'https://...' },
|
|
392
|
+
// ]
|
|
393
|
+
|
|
394
|
+
console.log(`${episodes.length} episodes available`);
|
|
395
|
+
|
|
396
|
+
// Jump to specific episode
|
|
397
|
+
const ep5 = episodes.find(e => e.ep === 5);
|
|
398
|
+
iframe.src = ep5.src;
|
|
399
|
+
|
|
400
|
+
// Keep raw src= prefix
|
|
401
|
+
const raw = await getStreamingLinks(119, { stripSrc: false });
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Options:**
|
|
405
|
+
|
|
406
|
+
| Option | Type | Default | Description |
|
|
407
|
+
|--------|------|---------|-------------|
|
|
408
|
+
| `stripSrc` | `boolean` | `true` | Strip the `src=` prefix from links |
|
|
409
|
+
|
|
410
|
+
**Returns:** `StreamingDetails`
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### `getFullDetails(id)`
|
|
415
|
+
|
|
416
|
+
> `GET /anime/api/details/:id`
|
|
417
|
+
|
|
418
|
+
The most **complete single-anime endpoint**. Returns local metadata + MAL/Jikan data + full cast with voice actors.
|
|
419
|
+
|
|
420
|
+
```js
|
|
421
|
+
import { getFullDetails } from 'anipub';
|
|
422
|
+
|
|
423
|
+
const { local, jikan, characters } = await getFullDetails(119);
|
|
424
|
+
|
|
425
|
+
// Local metadata
|
|
426
|
+
console.log(local.Name); // "Black Clover"
|
|
427
|
+
console.log(local.MALScore); // "8.88"
|
|
428
|
+
|
|
429
|
+
// MAL/Jikan enrichment
|
|
430
|
+
console.log(jikan.synopsis); // full synopsis text
|
|
431
|
+
|
|
432
|
+
// Characters & voice actors
|
|
433
|
+
characters.forEach(c => {
|
|
434
|
+
console.log(`${c.character.name} — ${c.role}`);
|
|
435
|
+
// → "Asta — Main"
|
|
436
|
+
|
|
437
|
+
c.voice_actors.forEach(va => {
|
|
438
|
+
console.log(` VA: ${va.person.name} (${va.language})`);
|
|
439
|
+
// → "VA: Gakuto Kajiwara (Japanese)"
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Filter main characters only
|
|
444
|
+
const mainCast = characters.filter(c => c.role === 'Main');
|
|
445
|
+
|
|
446
|
+
// Get the Japanese VA for a character
|
|
447
|
+
const jpVA = c.voice_actors.find(va => va.language === 'Japanese');
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Returns:** `FullDetails`
|
|
451
|
+
|
|
452
|
+
| Field | Type | Description |
|
|
453
|
+
|-------|------|-------------|
|
|
454
|
+
| `local` | `AnimeInfo` | Full local metadata |
|
|
455
|
+
| `jikan` | `object` | MAL/Jikan data (synopsis, etc.) |
|
|
456
|
+
| `characters` | `Character[]` | Full cast + voice actors |
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Class-based Usage
|
|
461
|
+
|
|
462
|
+
Use the `AniPub` class for an OOP-style API or when you want a single import.
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
import AniPub from 'anipub';
|
|
466
|
+
|
|
467
|
+
const client = new AniPub();
|
|
468
|
+
|
|
469
|
+
// All 10 endpoints as instance methods
|
|
470
|
+
const total = await client.getTotal();
|
|
471
|
+
const anime = await client.getInfo('one-piece');
|
|
472
|
+
const results = await client.search('bleach');
|
|
473
|
+
const top = await client.getTopRated(1);
|
|
474
|
+
const genre = await client.findByGenre('action', 1);
|
|
475
|
+
const found = await client.findByName('Naruto');
|
|
476
|
+
const full = await client.getFullDetails(119);
|
|
477
|
+
const stream = await client.getStreamingLinks(119);
|
|
478
|
+
const check = await client.checkAnime('One Piece', 'Adventure');
|
|
479
|
+
const paged = await client.searchAll('dragon ball', 1);
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## TypeScript
|
|
485
|
+
|
|
486
|
+
Full type declarations are bundled. No `@types/` package needed.
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import {
|
|
490
|
+
getInfo,
|
|
491
|
+
getFullDetails,
|
|
492
|
+
AniPub,
|
|
493
|
+
type AnimeInfo,
|
|
494
|
+
type FullDetails,
|
|
495
|
+
type Character,
|
|
496
|
+
type SearchResult,
|
|
497
|
+
} from 'anipub';
|
|
498
|
+
|
|
499
|
+
// Typed anime object
|
|
500
|
+
const anime: AnimeInfo = await getInfo('demon-slayer');
|
|
501
|
+
|
|
502
|
+
// Typed full details
|
|
503
|
+
const full: FullDetails = await getFullDetails(61);
|
|
504
|
+
const mainChars: Character[] = full.characters.filter(c => c.role === 'Main');
|
|
505
|
+
|
|
506
|
+
// Typed class usage
|
|
507
|
+
const client = new AniPub();
|
|
508
|
+
const results: SearchResult[] = await client.search('bleach');
|
|
509
|
+
|
|
510
|
+
// Custom typed helper
|
|
511
|
+
async function getTopInGenre(genre: string, minScore: number): Promise<AnimeInfo[]> {
|
|
512
|
+
const { wholePage } = await client.findByGenre(genre);
|
|
513
|
+
return wholePage.filter(a => parseFloat(a.MALScore) >= minScore);
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Error Handling
|
|
520
|
+
|
|
521
|
+
All endpoints throw `AniPubError` on HTTP errors (404, 500, etc.).
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
import { getInfo, AniPubError } from 'anipub';
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const anime = await getInfo(99999999);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
if (err instanceof AniPubError) {
|
|
530
|
+
console.error(`API error ${err.statusCode}: ${err.message}`);
|
|
531
|
+
// → API error 404: AniPub API error [404]: Not Found.
|
|
532
|
+
} else {
|
|
533
|
+
console.error('Network error:', err.message);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Graceful fallback
|
|
538
|
+
async function safeGetInfo(id) {
|
|
539
|
+
try { return await getInfo(id); }
|
|
540
|
+
catch { return null; }
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**`AniPubError` properties:**
|
|
545
|
+
|
|
546
|
+
| Property | Type | Description |
|
|
547
|
+
|----------|------|-------------|
|
|
548
|
+
| `message` | `string` | Human-readable error |
|
|
549
|
+
| `statusCode` | `number` | HTTP status (404, 500…) |
|
|
550
|
+
| `name` | `string` | Always `"AniPubError"` |
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Real-World Patterns
|
|
555
|
+
|
|
556
|
+
### Autocomplete search input
|
|
557
|
+
|
|
558
|
+
```js
|
|
559
|
+
import { search } from 'anipub';
|
|
560
|
+
|
|
561
|
+
function debounce(fn, ms) {
|
|
562
|
+
let t;
|
|
563
|
+
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const handleSearch = debounce(async (query) => {
|
|
567
|
+
if (query.length < 2) return clearDropdown();
|
|
568
|
+
const hits = await search(query);
|
|
569
|
+
renderDropdown(hits.slice(0, 8));
|
|
570
|
+
}, 300);
|
|
571
|
+
|
|
572
|
+
searchInput.addEventListener('input', e => handleSearch(e.target.value));
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
### Smart lookup (exact → fuzzy fallback)
|
|
578
|
+
|
|
579
|
+
```js
|
|
580
|
+
import { findByName, search, getInfo } from 'anipub';
|
|
581
|
+
|
|
582
|
+
async function smartLookup(query) {
|
|
583
|
+
const found = await findByName(query);
|
|
584
|
+
if (found.exist) return getInfo(found.id); // exact match
|
|
585
|
+
|
|
586
|
+
const results = await search(query); // fuzzy fallback
|
|
587
|
+
if (!results.length) return null;
|
|
588
|
+
return getInfo(results[0].Id);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const anime = await smartLookup('Demon Slayer');
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
### Episode player builder
|
|
597
|
+
|
|
598
|
+
```js
|
|
599
|
+
import { getInfo, getStreamingLinks } from 'anipub';
|
|
600
|
+
|
|
601
|
+
async function buildPlayer(animeId) {
|
|
602
|
+
const [info, { episodes }] = await Promise.all([
|
|
603
|
+
getInfo(animeId),
|
|
604
|
+
getStreamingLinks(animeId),
|
|
605
|
+
]);
|
|
606
|
+
return { title: info.Name, cover: info.Cover, episodes, current: episodes[0] };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const player = await buildPlayer(61);
|
|
610
|
+
iframe.src = player.current.src;
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
### Fetch all pages in a genre
|
|
616
|
+
|
|
617
|
+
```js
|
|
618
|
+
import { findByGenre } from 'anipub';
|
|
619
|
+
|
|
620
|
+
async function getAllInGenre(genre, maxPages = 5) {
|
|
621
|
+
const all = [];
|
|
622
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
623
|
+
const { wholePage } = await findByGenre(genre, page);
|
|
624
|
+
if (!wholePage.length) break;
|
|
625
|
+
all.push(...wholePage);
|
|
626
|
+
}
|
|
627
|
+
return all;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const allRomance = await getAllInGenre('romance');
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
### Parallel batch fetch
|
|
636
|
+
|
|
637
|
+
```js
|
|
638
|
+
import { getInfo } from 'anipub';
|
|
639
|
+
|
|
640
|
+
const ids = [61, 10, 119, 7, 3];
|
|
641
|
+
const batch = await Promise.all(ids.map(id => getInfo(id)));
|
|
642
|
+
batch.forEach(a => console.log(`${a.Name} — ${a.MALScore}`));
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
### Random anime picker
|
|
648
|
+
|
|
649
|
+
```js
|
|
650
|
+
import { getTotal, getInfo } from 'anipub';
|
|
651
|
+
|
|
652
|
+
async function randomAnime() {
|
|
653
|
+
const total = await getTotal();
|
|
654
|
+
return getInfo(Math.ceil(Math.random() * total));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const surprise = await randomAnime();
|
|
658
|
+
console.log(`Today's pick: ${surprise.Name}`);
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
### Search with score filter
|
|
664
|
+
|
|
665
|
+
```js
|
|
666
|
+
import { searchAll } from 'anipub';
|
|
667
|
+
|
|
668
|
+
async function searchHighRated(query, minScore = 8.0) {
|
|
669
|
+
const { AniData } = await searchAll(query);
|
|
670
|
+
return AniData.filter(a => parseFloat(a.MALScore) >= minScore);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const picks = await searchHighRated('fantasy', 8.5);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## License
|
|
679
|
+
|
|
680
|
+
[MIT](LICENSE) © Abdullah AL Adnan
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
<div align="center">
|
|
685
|
+
<sub>Built with ❤️ for the anime community · <a href="https://api.anipub.xyz">AniPub API Docs</a></sub>
|
|
686
|
+
</div>
|