npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@irithell-js/multi-dl

v1.0.0

Published

Universal media downloader and stream extractor.

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

  • 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.js EventEmitter.
  • 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-playlist extraction with automated URL slug parsing for missing titles.
  • YouTube Engine Injection — Dependency injection support for @irithell-js/yt-play to 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:

  1. IPv6 Hang Prevention: Binary verification network calls are bypassed when the PREFIX environment variable indicates Termux.
  2. FUSE Filesystem Safety: Creation of the internal bin directory avoids fs.mkdir crashes in the Android /sdcard/ emulated storage layer.
  3. Shell Contexts: CLI spawning is securely piped without assuming terminal TTY, preventing infinite stdin blocking.

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