@irithell-js/multi-dl
v1.0.0
Published
Universal media downloader and stream extractor.
Maintainers
Readme
@irithell-js/multi-dl
High-performance, universal media extraction and download engine. Agnostic support for SoundCloud, Vimeo, X (Twitter), XVideos, and hundreds of other platforms via yt-dlp. Features event-driven downloads, on-the-fly FFmpeg media conversion, playlist mapping, and seamless integration with @irithell-js/yt-play. Built with native Termux (Android) compatibility.
Table of Contents
- Features
- Installation
- Quick Start
- Configuration
- API Reference
- Type Reference
- Termux and Android Support
- Cookie Management
- License
Features
- Universal Extraction — Agnostic engine that standardizes metadata, formats, and playlists across hundreds of platforms.
- Event-Driven Downloads — Real-time telemetry (
progress,speed,eta,fragment_retry) via Node.jsEventEmitter. - Media Conversion — Built-in FFmpeg wrapper for on-the-fly muxing, format conversion, and container swapping.
- Lazy Initialization — Binaries and filesystem operations are strictly deferred until the first method call.
- Master Cookie Jar — Centralized cookie management that persists Cloudflare bypass tokens and session data natively.
- Playlist — High-speed
--flat-playlistextraction with automated URL slug parsing for missing titles. - YouTube Engine Injection — Dependency injection support for
@irithell-js/yt-playto keep specialized caching and live stream chunking fully isolated. - Termux Native — Zero-configuration IPv6 hang prevention and FUSE filesystem safety for Android environments.
Installation
npm install @irithell-js/multi-dl
Note: If you plan to use the YouTube injection feature, you must also install @irithell-js/yt-play.
Quick Start
ESM
import { MultiEngine } from "@irithell-js/multi-dl";
import path from "node:path";
const engine = new MultiEngine({ concurrentFragments: 4 });
const url = "https://soundcloud.com/hate_music/stf23-caiva-final";
// 1. Search metadata
const metadata = await engine.search(url);
console.log(`Found: ${metadata.title} on ${metadata.platform}`);
// 2. Download with real-time telemetry
const task = engine.download(url, {
type: "audio",
quality: "better",
outputPath: path.join(process.cwd(), "audio.m4a"),
});
task.on("progress", (data) => {
const mb = (data.downloadedBytes / 1024 / 1024).toFixed(2);
process.stdout.write(
`\rProgress: ${data.percent.toFixed(1)}% | Downloaded: ${mb}MB`,
);
});
const result = await task.done();
console.log(`\nSaved to: ${result.path}`);CommonJS
const { MultiEngine } = require("@irithell-js/multi-dl");
const path = require("node:path");
const engine = new MultiEngine();
async function run() {
const url = "https://vimeo.com/76979871";
const directUrl = await engine.resolve(url);
console.log("Direct Stream URL:", directUrl);
const result = await engine
.download(url, {
type: "video",
quality: "1080",
outputPath: path.join(__dirname, "video.mp4"),
})
.done();
await engine.convert({
inputPath: result.path,
outputPath: path.join(__dirname, "video.mkv"),
ffmpegArgs: ["-c", "copy"],
});
}
run();Configuration
MultiDlOptions
Pass options to the MultiEngine constructor. All fields are optional.
const engine = new MultiEngine({
// ── Injection ─────────────────────────────────────────────────────────────
ytModule: false, // Pass a PlayEngine instance here to enable the .youtube namespace
// ── Performance ───────────────────────────────────────────────────────────
concurrentFragments: 8, // Global parallel download fragments (default: fallback to engine internal)
// ── Authentication ────────────────────────────────────────────────────────
cookieJarPath: "./master_cookies.txt", // Custom path for the persistent cookie jar
// ── Binaries (auto-detected if omitted) ───────────────────────────────────
ytdlpBinaryPath: "/data/data/com.termux/files/usr/bin/yt-dlp",
ffmpegPath: "/usr/bin/ffmpeg",
aria2cPath: "/usr/bin/aria2c",
// ── Logging ───────────────────────────────────────────────────────────────
logger: {
debug: (...args) => console.debug("\x1b[36m[DEBUG]\x1b[0m", ...args),
info: (...args) => console.info("\x1b[32m[INFO]\x1b[0m", ...args),
warn: (...args) => console.warn("\x1b[33m[WARN]\x1b[0m", ...args),
error: (...args) => console.error("\x1b[31m[ERROR]\x1b[0m", ...args),
},
});API Reference
MultiEngine
The central Facade.
engine.init(): Promise<void>
Forces the lazy initialization process to run immediately (downloads/checks binaries and creates directories). Useful if you need to guarantee readiness before the first method call.
await engine.init();engine.search(url, options?): Promise<UniversalMetadata>
Extracts normalized metadata, available media formats, and playlist entries (if applicable).
const metadata = await engine.search(
"https://soundcloud.com/hate_music/sets/hate-x-the-third-room-x-1",
{
limit: 2,
saveMetadataPath: "./playlist_meta.json",
},
);
console.log(metadata.isPlaylist); // true
console.log(metadata.entries[0].title); // "Hitam B2b Windfuhr..."engine.resolve(url, options?): Promise<string>
Resolves the raw, direct CDN streaming URL for a given media link.
const directUrl = await engine.resolve("https://vimeo.com/76979871");engine.download(url, options): DownloadTaskContract
Initiates a media download. Returns an EventEmitter that exposes telemetry and a .done() promise.
const task = engine.download("https://vimeo.com/76979871", {
type: "video",
quality: "720",
outputPath: "./output.mp4",
autoRetry: true,
concurrentFragments: 4,
});
task.on("progress", (data) =>
console.log(data.percent, data.speedBytesPerSecond, data.etaSeconds),
);
task.on("fragment_retry", (attempt) =>
console.warn(`Fragment failed, retrying...`),
);
task.on("error", (err) => console.error(err));
const file = await task.done();
console.log(file.path, file.size);engine.stream(url, options): Promise<Readable>
Bypasses disk storage and pipes the raw output from yt-dlp into a Node.js Readable stream.
import fs from "node:fs";
const stream = await engine.stream(
"https://soundcloud.com/hate_music/stf23-caiva-final",
{
type: "audio",
quality: "better",
},
);
stream.pipe(fs.createWriteStream("output.m4a"));engine.convert(options): Promise<{ path, size }>
Wraps FFmpeg to execute post-processing tasks on local files.
const result = await engine.convert({
inputPath: "./video_teste.mp4",
outputPath: "./video_convertido.mkv",
ffmpegArgs: ["-c", "copy"], // Fast container swap
});YouTube Integration
To maintain peak performance and avoid translating complex interfaces, @irithell-js/yt-play can be injected directly into the engine.
engine.youtube (Getter)
Returns the injected PlayEngine instance. Throws an EngineInitializationError if not configured or if init() hasn't been called.
const { MultiEngine } = require("@irithell-js/multi-dl");
const { PlayEngine } = require("@irithell-js/yt-play");
// 1. Instantiate the specialized YouTube engine
const ytEngine = new PlayEngine({
preferredAudioKbps: 128,
preferredVideoP: 1080,
cacheDir: "./tmp",
});
// 2. Inject it into the MultiEngine
const engine = new MultiEngine({
ytModule: ytEngine,
});
// 3. Use specialized methods safely isolated in the .youtube namespace
const metadata = await engine.youtube.search("https://youtu.be/OCc8LKU-hwg");
const requestId = engine.youtube.generateRequestId();
await engine.youtube.preload(metadata, requestId, "audio");
const { file } = await engine.youtube.getOrDownload(
requestId,
"audio",
metadata,
);
console.log(`YouTube Audio cached at: ${file.path}`);Type Reference
Core Types
QualityFilter
type QualityFilter = "better" | "lower" | string; // Pass exact height/abr (e.g., "1080", "128")MediaType
type MediaType = "audio" | "video" | "muxed";Metadata Interfaces
UniversalMetadata
Returned by engine.search().
| Field | Type | Description |
| ----------------- | --------------------------- | --------------------------------------- |
| id | string | Unique media ID |
| title | string | Media or playlist title |
| author | string | Uploader or creator |
| durationSeconds | number | Duration in seconds (0 for playlists) |
| thumbnail | string | Thumbnail URL |
| platform | string | Extractor key (e.g., "SoundcloudSet") |
| isPlaylist | boolean | True if the URL points to a playlist |
| webpageUrl | string | Canonical URL |
| formats | UniversalMediaFormat[] | Available resolutions and bitrates |
| entries | PlaylistEntry[]/undefined | Populated if isPlaylist is true |
UniversalMediaFormat
| Field | Type | Description |
| ---------------- | ------------------ | --------------------------------------------- |
| formatId | string | yt-dlp internal format ID |
| url | string | Direct stream URL |
| quality | string | Description (e.g., "1080p", "audio only") |
| ext | string | File extension |
| protocol | string | https, m3u8_native, etc. |
| hasDrm | boolean | True if DRM protected |
| isMuxed | boolean | Contains both audio and video tracks |
| isAudioOnly | boolean | Audio track only |
| isVideoOnly | boolean | Video track only |
| filesizeApprox | number/undefined | Estimated file size in bytes |
| tbr | number/undefined | Total bitrate |
PlaylistEntry
| Field | Type | Description |
| ----------------- | -------- | -------------------------------- |
| id | string | Item ID |
| title | string | Extracted title or fallback slug |
| url | string | Watch URL |
| durationSeconds | number | Duration in seconds |
Extraction Interfaces
ExtractionOptions
Base options inherited by Search, Download, and Stream methods.
| Field | Type | Description |
| -------------------- | ----------------- | ---------------------------------------------------------- |
| cookiesPaths | string/string[] | Paths to additional cookie files to merge into the session |
| limit | number | Max downloads or playlist parse limit |
| startItem | number | Playlist start index |
| endItem | number | Playlist end index |
| reverse | boolean | Reverse playlist order |
| maxDurationSeconds | number | Filter out media longer than this |
| minDurationSeconds | number | Filter out media shorter than this |
| saveMetadataPath | string | Dumps the parsed JSON to this path |
DownloadOptions (Extends ExtractionOptions)
| Field | Type | Description |
| --------------------- | --------------- | ------------------------------------------- |
| type | MediaType | Required. "audio"/"video"/"muxed" |
| quality | QualityFilter | Required. "better"/"lower"/"1080" |
| outputPath | string | Destination path on disk |
| ffmpegArgs | string[] | Post-processor arguments |
| useAria2c | boolean | Use aria2c for acceleration (default: true) |
| concurrentFragments | number | Parallel fragment downloads |
| autoRetry | boolean | Infinite fragment retries (default: true) |
| maxRetries | number | Total file retries (default: 10) |
StreamOptions (Extends ExtractionOptions)
| Field | Type | Description |
| ------------ | --------------- | ----------------------------------- |
| type | MediaType | Required. "audio"/"video"/"muxed" |
| quality | QualityFilter | Required. "better"/"lower"/"1080" |
| ffmpegArgs | string[] | Post-processor arguments |
MediaConversionOptions
| Field | Type | Description |
| ------------ | ---------- | --------------------------------------------------- |
| inputPath | string | Required. Source file |
| outputPath | string | Required. Destination file |
| ffmpegArgs | string[] | Required. FFmpeg arguments (e.g., ["-c", "copy"]) |
Task Interfaces
DownloadProgress
| Field | Type | Description |
| --------------------- | -------- | --------------------------- |
| percent | number | Download percentage (0-100) |
| downloadedBytes | number | Bytes written |
| totalBytes | number | Total file size in bytes |
| speedBytesPerSecond | number | Current download speed |
| etaSeconds | number | Estimated time remaining |
DownloadTaskContract (Extends EventEmitter)
Methods:
abort(): void- Kills the child process immediately.done(): Promise<{ path: string; size: number }>- Awaits completion.
Events:
"progress" (data: DownloadProgress)"fragment_retry" (attempt: number)"end" (file: { path: string; size: number })"error" (error: Error)
Termux & Android Support
This engine is natively built to circumvent Node.js architecture limitations inside Termux:
- IPv6 Hang Prevention: Binary verification network calls are bypassed when the
PREFIXenvironment variable indicates Termux. - FUSE Filesystem Safety: Creation of the internal
bindirectory avoidsfs.mkdircrashes in the Android/sdcard/emulated storage layer. - Shell Contexts: CLI spawning is securely piped without assuming terminal TTY, preventing infinite
stdinblocking.
When running on Termux, ensure yt-dlp and aria2c are installed via pkg or pip, and provide their absolute paths in MultiDlOptions:
const engine = new MultiEngine({
ytdlpBinaryPath: "/data/data/com.termux/files/usr/bin/yt-dlp",
aria2cPath: "/data/data/com.termux/files/usr/bin/aria2c",
});Cookie Management
The engine maintains a centralized "Master Cookie Jar" (master_cookies.txt).
When a platform (like Vimeo or X) issues a Cloudflare clearance token (__cf_bm) or authentication token during extraction, yt-dlp writes it back to this jar. Subsequent requests automatically utilize these tokens to prevent 403 Forbidden errors and bot-detection blocks.
You can inject external cookies for specific requests without overwriting the master jar:
await engine.download(url, {
type: "video",
quality: "better",
cookiesPaths: ["./my_browser_cookies.txt"], // Merged into the master session safely
});License
MIT
