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.
Requirements
Section titled “Requirements”- Node 20+ or Bun: native
fetchandcrypto.subtleare required - ffmpeg: only needed to run the live E2E test suite for anime streams
Install
Section titled “Install”npm install anime-sdk# -- or --pnpm add anime-sdk# -- or --bun add anime-sdkHow it works
Section titled “How it works”Every provider exposes the same three methods: search → fetchContentUnits → resolveStream.
import { HttpClient, GogoanimeProvider, MangadexProvider } from 'anime-sdk';
const client = new HttpClient({ timeoutMs: 10_000 });
// Anime exampleconst 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 exampleconst 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);The resolved stream shape
Section titled “The resolved stream shape”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';}Sub / dub / raw
Section titled “Sub / dub / raw”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 playableconst 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.
Using the resolved URL
Section titled “Using the resolved URL”Pass sourceUrl directly to any HLS player or downloader. Many CDNs require the headers object to be forwarded:
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 }} />Downloading to disk
Section titled “Downloading to disk”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 → downloadVideoconst 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 → downloadMangaChapterconst 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.
Swapping providers
Section titled “Swapping providers”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 → resolveStreamUnified URN IDs
Section titled “Unified URN IDs”Every ID returned by the SDK is a URN — ${providerId}:${rawId}:
allmanga:5jzpRTJWnubrgHm5G → media URNallmanga:5jzpRTJWnubrgHm5G/1 → content unit URNanilist: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.
Metadata layer
Section titled “Metadata 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 IMediaMetadataconst 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).
Cancellation
Section titled “Cancellation”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.
Next steps
Section titled “Next steps”- Browse providers: AllManga, Gogoanime, Anikoto, Goyabu
- Downloads: save episodes as MP4 and chapters as ZIP
- Run the HTTP server: call anime-sdk from any language
- Full API reference
- Add a provider