Skip to content

Getting Started

anime-sdk is a Typescript SDK that searches anime/manga catalogues (AniList, MAL, Kitsu), resolves direct video stream URLs and manga page URLs across multiple content providers, and lets you swap content providers without changing call sites.

It has four layers: HTTP transport (with per-host rate limiting, retry, AbortSignal, and a pluggable curl fallback), extractors (embed parsers), content providers (site-specific scraping logic), and metadata providers (catalogue-level data with cross-source mapping). Everything connects through HttpClient.

  • Node 20+ or Bun: native fetch and crypto.subtle are required
  • ffmpeg: only needed to run the live E2E test suite for anime streams
Terminal window
npm install anime-sdk
# -- or --
pnpm add anime-sdk
# -- or --
bun add anime-sdk

Every provider exposes the same three methods: searchfetchContentUnitsresolveStream.

import { HttpClient, GogoanimeProvider, MangadexProvider } from 'anime-sdk';
const client = new HttpClient({ timeoutMs: 10_000 });
// Anime example
const anime = new GogoanimeProvider(client);
const shows = await anime.search('Frieren');
const eps = await anime.fetchContentUnits(shows[0].id);
const stream = await anime.resolveStream(eps[0].id, 'sub');
// Manga example
const manga = new MangadexProvider(client);
const books = await manga.search('Frieren');
const chapters = await manga.fetchContentUnits(books[0].id);
const pages = await manga.resolveStream(chapters[0].id);

resolveStream returns ResolvedMediaStream: a discriminated union on type:

type ResolvedMediaStream =
| { type: 'video'; streams: IVideoPayload[] }
| { type: 'manga'; pages: IMangaPayload };

Manga providers return type: 'manga', while anime providers return type: 'video'. Always check result.type before accessing the payload.

Each IMangaPayload carries:

interface IMangaPayload {
imageUrls: string[]; // high-resolution image URLs for each page
headers?: Record<string, string>; // Referer required by some sites
}

Each IVideoPayload in the streams array has:

interface IVideoPayload {
sourceUrl: string; // direct URL or HLS manifest
isHLS: boolean; // true → feed to an HLS player
quality: '1080p' | '720p' | '480p' | '360p' | 'auto';
language?: ContentLanguage;
headers?: Record<string, string>; // Referer/User-Agent required by some CDNs
subtitles?: ISubtitleTrack[]; // external VTT tracks, when the provider has them
}
interface ISubtitleTrack {
url: string;
language: string; // BCP-47 (e.g. 'en', 'pt-BR')
label: string; // human-readable ("English", "Português")
format?: 'vtt' | 'srt' | 'ass';
}

fetchContentUnits returns one unified episode list: each IContentUnit advertises its translations via availableLanguages: ContentLanguage[]. The translation is picked at resolveStream time:

type ContentLanguage = 'sub' | 'dub' | 'raw';
const eps = await provider.fetchContentUnits(shows[0].id);
const ep = eps[0]; // ep.availableLanguages tells you what's playable
const result = await provider.resolveStream(ep.id, 'dub');

Providers that don’t support the requested language fall back to 'sub'.

Inspecting tracks without resolving the stream

Section titled “Inspecting tracks without resolving the stream”

Some providers expose a cheap metadata endpoint so a UI can show the subtitle/quality selector before playback. Implement (or call) fetchUnitTracks when available:

if (provider.fetchUnitTracks) {
const { subtitles, qualities } = await provider.fetchUnitTracks(ep.id, 'sub');
// subtitles: ISubtitleTrack[], qualities: IVideoPayload['quality'][]
}

Providers without a cheap path leave the method undefined: read IVideoPayload.subtitles straight off the resolved stream instead.

Pass sourceUrl directly to any HLS player or downloader. Many CDNs require the headers object to be forwarded:

hls.js
const hls = new Hls();
hls.loadSource(stream.sourceUrl);
// hls.js doesn't support custom headers on fetch: serve through your HTTP server instead
// ffmpeg
// ffmpeg -i "url" -headers "Referer: ..." output.mp4
// React Native / Video
<Video source={{ uri: stream.sourceUrl, headers: stream.headers }} />

The SDK includes built-in download utilities. HLS anime streams are downloaded by fetching segments and muxing with ffmpeg; direct MP4s stream straight to disk. Manga chapters are packaged as .zip archives.

import { downloadVideo, downloadMangaChapter } from 'anime-sdk';
// Anime: resolveStream → downloadVideo
const animeResult = await animeProvider.resolveStream(eps[0].id, 'sub');
if (animeResult.type === 'video') {
const { outputPath, fileSize } = await downloadVideo(animeResult.streams, './episode-1.mp4', {
onProgress: ({ phase, detail }) => console.log(`[${phase}] ${detail ?? ''}`),
});
console.log(`Saved ${fileSize} bytes → ${outputPath}`);
}
// Manga: resolveStream → downloadMangaChapter
const mangaResult = await mangaProvider.resolveStream(chapters[0].id);
if (mangaResult.type === 'manga') {
const { pageCount, outputPath } = await downloadMangaChapter(mangaResult.pages, './ch-1.zip', {
onProgress: ({ downloaded, total }) => console.log(`${downloaded}/${total}`),
});
console.log(`Packed ${pageCount} pages → ${outputPath}`);
}

Pass the full streams array to downloadVideo and it tries each candidate in order, falling back automatically on failure. See Downloads for the full API including batch downloads and low-level HLS helpers.

The three calls are identical across providers. Swap the import and constructor, nothing else changes:

import { AllmangaProvider } from 'anime-sdk';
const provider = new AllmangaProvider(new HttpClient());
// same: search → fetchContentUnits → resolveStream

Every ID returned by the SDK is a URN — ${providerId}:${rawId}:

allmanga:5jzpRTJWnubrgHm5G → media URN
allmanga:5jzpRTJWnubrgHm5G/1 → content unit URN
anilist:1 → metadata URN (single namespace)
mal:anime:21 / kitsu:manga:13 → typed catalogue URN (anime/manga distinction)

The first colon is the separator; raw IDs may themselves contain colons or slashes. The metadata layer’s MappingClient translates between catalogue and content-provider namespaces automatically, so you can swap content providers without changing your data layer.

Use AniList / MAL / Kitsu as a unified catalogue front-end, then pipe to any content provider for actual playback. The mapping (anilist id → site raw id) is resolved through a cache → provider native lookup → MALSync/Anify/arm-server → fuzzy waterfall.

import { HttpClient, AnilistMeta, AllmangaProvider } from 'anime-sdk';
const http = new HttpClient();
const meta = new AnilistMeta(http);
const allmanga = new AllmangaProvider(http);
const info = await meta.fetchMediaInfo('anilist:1'); // full IMediaMetadata
const stream = await meta.resolveStream('anilist:1', 1, allmanga, 'sub');

The metadata object includes relations (sequel/prequel), characters with voice actors, staff, recommendations, external links (Crunchyroll, …) and per-episode streamingEpisodes with titles + thumbnails (and filler/recap flags when sourced from MAL).

Every method accepts an optional CallOptions.signal:

const ac = new AbortController();
setTimeout(() => ac.abort(), 1500);
await meta.search('frieren', { signal: ac.signal });

The signal is threaded all the way down to fetch, the rate-limiter queue, and the retry loop.