Downloads
The SDK ships built-in download utilities for both anime and manga. No extra dependencies beyond ffmpeg on your PATH (anime only).
Anime: downloadVideo
Section titled “Anime: downloadVideo”downloadVideo accepts one or more IVideoPayload candidates from resolveStream and saves a playable .mp4 file.
- HLS streams: the playlist is walked (master → variant → segments), each segment is downloaded and concatenated into a temporary
.tsfile, thenffmpeg -c copymuxes it to MP4. - Direct MP4 streams: downloaded via
fetchstreaming directly to disk.
import { HttpClient, AllmangaProvider, downloadVideo } from 'anime-sdk';
const client = new HttpClient();const provider = new AllmangaProvider(client);
const shows = await provider.search('Frieren');const eps = await provider.fetchContentUnits(shows[0].id);const result = await provider.resolveStream(eps[0].id, 'sub');
if (result.type === 'video') { const download = await downloadVideo(result.streams, './episode-1.mp4', { onProgress: ({ phase, detail }) => console.log(`[${phase}] ${detail ?? ''}`), });
console.log(`Saved ${download.fileSize} bytes → ${download.outputPath}`);}Fallback behaviour
Section titled “Fallback behaviour”Pass the full streams array and downloadVideo tries each candidate in order until one succeeds. If all fail, it throws with a combined error message listing every failure.
// Single candidate: throws immediately on failureawait downloadVideo(result.streams[0], './episode-1.mp4');
// All candidates: tries each in orderawait downloadVideo(result.streams, './episode-1.mp4');Progress reporting
Section titled “Progress reporting”await downloadVideo(streams, './episode-1.mp4', { onProgress: ({ phase, detail }) => { // phase: 'resolving' | 'downloading' | 'muxing' | 'complete' // detail: human-readable string (segment count, output path, etc.) console.log(`[${phase}] ${detail ?? ''}`); }, timeoutMs: 600_000, // 10 min; default 300_000 (5 min)});Phases in order:
| Phase | When it fires |
|---|---|
resolving | Before each candidate is attempted |
downloading | As HLS segments or MP4 bytes are fetched |
muxing | While ffmpeg is converting the .ts concat to .mp4 |
complete | After the file is written and verified (size > 1 KB check) |
Requirements
Section titled “Requirements”ffmpegmust be on yourPATH(only needed for HLS streams; direct MP4s usefetchonly).- The output directory is created automatically if it doesn’t exist.
- A minimum file size of 1 KB is asserted after writing; candidates that produce a smaller file are retried.
Manga: single page
Section titled “Manga: single page”import { MangadexProvider, HttpClient, downloadMangaPage } from 'anime-sdk';
const client = new HttpClient();const provider = new MangadexProvider(client);
const books = await provider.search('Frieren');const chapters = await provider.fetchContentUnits(books[0].id);const result = await provider.resolveStream(chapters[0].id);
if (result.type === 'manga') { const page = await downloadMangaPage(result.pages, 0, './chapter-1/', { timeoutMs: 30_000, }); // Saves ./chapter-1/page_001.jpg (extension is auto-detected from Content-Type) console.log(page.outputPath, page.fileSize);}The filename is page_<NNN><ext> where <ext> is inferred from the Content-Type header (.jpg, .png, .webp, .gif, .avif). Pages are 1-indexed and zero-padded to three digits.
Manga: full chapter as ZIP
Section titled “Manga: full chapter as ZIP”import { MangadexProvider, HttpClient, downloadMangaChapter } from 'anime-sdk';
const client = new HttpClient();const provider = new MangadexProvider(client);
const books = await provider.search('Frieren');const chapters = await provider.fetchContentUnits(books[0].id);const result = await provider.resolveStream(chapters[0].id);
if (result.type === 'manga') { const zip = await downloadMangaChapter(result.pages, './chapter-1.zip', { onProgress: ({ downloaded, total }) => { process.stdout.write(`\r${downloaded}/${total} pages`); }, });
console.log(`\nSaved ${zip.pageCount} pages (${zip.fileSize} bytes) → ${zip.outputPath}`);}Pages are downloaded in order and stored inside the ZIP as 001.jpg, 002.png, etc. The archive uses STORE (no compression): images are already compressed, so deflation would only add CPU cost with no size benefit.
The ZIP writer is self-contained with no external dependencies and implements the ZIP specification directly using Node’s Buffer APIs.
Batch download
Section titled “Batch download”downloadVideo is safe to run concurrently per episode. Use Promise.allSettled to continue even if individual downloads fail:
const episodes = await provider.fetchContentUnits(shows[0].id);
const results = await Promise.allSettled( episodes.slice(0, 5).map(async (ep) => { const stream = await provider.resolveStream(ep.id, 'sub'); if (stream.type !== 'video') return; return downloadVideo(stream.streams, `./ep-${ep.number}.mp4`, { onProgress: ({ phase }) => console.log(`ep${ep.number} [${phase}]`), }); }),);
for (const r of results) { if (r.status === 'fulfilled') console.log('ok:', r.value?.outputPath); else console.error('failed:', r.reason);}API types
Section titled “API types”interface DownloadVideoOptions { onProgress?: (info: { phase: string; detail?: string }) => void; timeoutMs?: number; // default 300_000 (5 min)}
interface DownloadVideoResult { outputPath: string; stream: IVideoPayload; // the candidate that succeeded fileSize: number; // bytes}
interface DownloadMangaPageOptions { headers?: Record<string, string>; // override the headers on IMangaPayload timeoutMs?: number; // default 30_000}
interface DownloadMangaPageResult { outputPath: string; pageIndex: number; fileSize: number; contentType: string;}
interface DownloadMangaChapterOptions { onProgress?: (info: { downloaded: number; total: number }) => void; timeoutMs?: number; // per page; default 30_000}
interface DownloadMangaChapterResult { outputPath: string; pageCount: number; fileSize: number; // total ZIP file size in bytes}Low-level helpers
Section titled “Low-level helpers”These are exported for custom download pipelines that need to work with HLS playlists directly.
// Parse variant URLs from an HLS master playlist (lowest → highest quality order)parseHlsMaster(content: string, baseUrl: string): string[]
// Parse segment URLs + durations from an HLS media playlistparseHlsSegments(content: string, baseUrl: string): Array<{ url: string; duration: number }>
// Infer image extension from a Content-Type headerdetectImageExtension(contentType: string): '.jpg' | '.png' | '.webp' | '.gif' | '.bmp' | '.avif'
// Compute CRC-32 for a Buffer (used by the ZIP writer)crc32(buf: Buffer): number
// Create an uncompressed ZIP buffer from an array of { filename, data } entriescreateZipBuffer(entries: Array<{ filename: string; data: Buffer }>): Buffer