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 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
+ [![npm version](https://img.shields.io/npm/v/anipub?color=ef4444&labelColor=18181b&style=flat-square)](https://www.npmjs.com/package/anipub)
10
+ [![npm downloads](https://img.shields.io/npm/dm/anipub?color=f97316&labelColor=18181b&style=flat-square)](https://www.npmjs.com/package/anipub)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-22c55e?labelColor=18181b&style=flat-square)](LICENSE)
12
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-3b82f6?labelColor=18181b&style=flat-square)](https://nodejs.org)
13
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-3b82f6?labelColor=18181b&style=flat-square)](src/index.d.ts)
14
+ [![ESM + CJS](https://img.shields.io/badge/ESM%20%2B%20CJS-supported-a855f7?labelColor=18181b&style=flat-square)](#)
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>