npm package: @irithell-js/yt-play
Description: YouTube search + download engine (audio/video) with optional caching.
Latest version: 0.3.3

# @irithell-js/yt-play

High-performance YouTube audio/video download engine with intelligent caching, built-in yt-dlp and aria2c binaries for blazing fast downloads.

## Features

- **Bundled Binaries** - yt-dlp and aria2c included (no system dependencies)
- **Auto-Update System** - yt-dlp updates automatically when needed
- **Ultra Fast Downloads** - aria2c acceleration (up to 5x faster)
- **Playlist Support** - Search, extract, and resolve direct media URLs from entire playlists in batches
- **Shorts Support** - Download and convertion for more compatibility
- **Metadata Stalking** - Deep extraction of channels, videos, and live streams (real-time viewers, hidden tags, etc).
- **Native Streaming** - Get raw `Readable` streams for audio or video without saving to disk first, perfect for Discord bots or streaming directly to HTTP responses.
- **Intelligent Caching** - TTL-based cache with automatic cleanup
- **Smart Quality** - Auto-reduces quality for long videos (>1h)
- **Container Ready** - Works in Docker/isolated environments with cookie support
- **Auto-Configuration** - Creates yt-dlp.conf with optimal settings
- **Cross-Platform** - Linux (x64/arm64), macOS, Windows
- **Zero Config** - Auto-detects binaries and optimizes settings
- **TypeScript** - Full type definitions included
- **Dual Format** - ESM and CommonJS support

## Installation

```bash
npm install @irithell-js/yt-play
```

Binaries (yt-dlp + aria2c) are automatically downloaded during installation (this may take a few seconds during the first use).

## Quick Start

### Basic Usage (ESM)

```typescript
import { PlayEngine } from "@irithell-js/yt-play";

const engine = new PlayEngine();

// Search and download
const metadata = await engine.search("linkin park numb");
if (!metadata) throw new Error("Not found");

const requestId = engine.generateRequestId();
await engine.preload(metadata, requestId);

// Get audio file
const { file: audioFile } = await engine.getOrDownload(requestId, "audio");
console.log("Audio:", audioFile.path);

// Get video file
const { file: videoFile } = await engine.getOrDownload(requestId, "video");
console.log("Video:", videoFile.path);

// Cleanup cache
engine.cleanup(requestId);
```

### Basic Usage (CommonJS)

```javascript
const { PlayEngine } = require("@irithell-js/yt-play");

const engine = new PlayEngine();

async function download() {
  const metadata = await engine.search("song name");
  const requestId = engine.generateRequestId();
  await engine.preload(metadata, requestId);

  const { file } = await engine.getOrDownload(requestId, "audio");
  console.log("Downloaded:", file.path);

  engine.cleanup(requestId);
}

download();
```

### Direct URL Support

```typescript
// Search by name
const metadata1 = await engine.search("never gonna give you up");

// Or use direct YouTube URL
const metadata2 = await engine.search(
  "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
);

// Both return the same metadata format
console.log(metadata2.title); // "Rick Astley - Never Gonna Give You Up"
```

### Playlist Support

You can search and extract direct media URLs from playlists with automatic batch processing:

```typescript
import { YtDlpClient, searchPlaylistBest } from "@irithell-js/yt-play";

// 1. Find a playlist (by search term or direct URL)
const metadata = await searchPlaylistBest("Lofi hip hop");

if (metadata) {
  const ytdlp = new YtDlpClient();

  // 2. Extract and resolve direct streaming URLs
  const playlistInfo = await ytdlp.getPlaylistInfo(metadata.url, {
    limit: 5, // Get exactly 5 valid tracks
    resolveLinks: true, // Automatically resolve direct media URLs
    type: "audio", // "audio", "video", or "both"
    batchSize: 5, // Process 5 tracks concurrently for maximum speed
  });

  playlistInfo.entries.forEach((track: any) => {
    console.log(`Title: ${track.title}`);
    console.log(`Direct URL: ${track.directUrl}\n`);
  });
}
```

**Example Output:**

```json
{
  "id": "PLjNlQ2vXx1xbt30X8TcUfNzw_akVISXEu",
  "title": "BEST Anime Openings and Endings Songs *Playlist*",
  "uploader": "by - AnimeEnfermos -",
  "entries": [
    {
      "id": "jIfogFtgV-o",
      "title": "Noragami / Opening 2",
      "url": "https://www.youtube.com/watch?v=jIfogFtgV-o",
      "duration": 103,
      "uploader": "reeaaas",
      "directUrl": "https://rr2---sn-103oxu-bg0l.go..."
    },
    {
      "id": "0YF8vecQWYs",
      "title": "Crying for Rain - 美波 (Minami) MV",
      "url": "https://www.youtube.com/watch?v=0YF8vecQWYs",
      "duration": 254,
      "uploader": "Minami",
      "directUrl": "https://rr1---sn-103oxu-bg0l.go..."
    },
    {
      "id": "pmanD_s7G3U",
      "title": "Demon Slayer | OP | Gurenge by LiSA HD",
      "url": "https://www.youtube.com/watch?v=pmanD_s7G3U",
      "duration": 90,
      "uploader": "animelab",
      "directUrl": "https://rr1---sn-103oxu-bg0l.go"
    },
    {
      "id": "atxYe-nOa9w",
      "title": "One Punch Man - Official Opening - The Hero!! Set Fire to the Furious Fist",
      "url": "https://www.youtube.com/watch?v=atxYe-nOa9w",
      "duration": 90,
      "uploader": "animelab",
      "directUrl": "https://rr1---sn-103oxu-bg0l.go..."
    },
    {
      "id": "792vg0amsuQ",
      "title": "Steins;Gate 0 - op pt-br legendado",
      "url": "https://www.youtube.com/watch?v=792vg0amsuQ",
      "duration": 243,
      "uploader": "Animes Nii-san",
      "directUrl": "https://rr1---sn-103oxu-bg0l.go..."
    }
  ]
}
```

## Configuration

### Constructor Options

```typescript
const engine = new PlayEngine({
  // Cache settings
  cacheDir: "./cache", // Cache directory (default: OS temp)
  ttlMs: 5 * 60_000, // Cache TTL in ms (default: 3min)
  cleanupIntervalMs: 30_000, // Cleanup interval (default: 30s)
  preloadBuffer: true, // Load files into RAM (default: true)

  // Quality settings
  preferredAudioKbps: 128, // Audio quality: 320|256|192|128|96|64
  preferredVideoP: 720, // Video quality: 1080|720|480|360
  maxPreloadDurationSeconds: 1200, // Max duration for preload (default: 20min)

  // Performance settings (auto-optimized)
  useAria2c: true, // Use aria2c for downloads (default: auto)
  concurrentFragments: 8, // Parallel fragments (default: 5)
  ytdlpTimeoutMs: 300_000, // yt-dlp timeout (default: 5min)

  // Binary paths (optional - auto-detected)
  ytdlpBinaryPath: "./bin/yt-dlp",
  aria2cPath: "./bin/aria2c",
  ffmpegPath: "/usr/bin/ffmpeg", // Optional

  // Cookies (required for VPS/Docker environments)
  cookiesPath: "./cookies.txt", // Path to Netscape cookies file
  // OR
  cookiesFromBrowser: "firefox", // Extract from browser: chrome, firefox, edge, safari

  // Logging
  logger: console, // Logger instance (optional)
});
```

### Cookie Configuration (VPS/Docker)

For headless environments (VPS, Docker), YouTube authentication is required:

**Option 1: Export cookies from browser**

1. Install browser extension: "Get cookies.txt LOCALLY"
2. Visit youtube.com and login
3. Export cookies to `cookies.txt`
4. Use in PlayEngine:

```typescript
const engine = new PlayEngine({
  cookiesPath: "/path/to/cookies.txt",
});
```

**Option 2: Extract directly from browser** (requires browser installed)

```typescript
const engine = new PlayEngine({
  cookiesFromBrowser: "firefox", // chrome, firefox, edge, safari
});
```

The engine automatically creates `yt-dlp.conf` with:

- Node.js runtime detection
- EJS remote components
- Cookie authentication
- Optimal download settings

### Quality Presets

```typescript
// High quality (larger files)
const hq = new PlayEngine({
  preferredAudioKbps: 320,
  preferredVideoP: 1080,
});

// Balanced (recommended)
const balanced = new PlayEngine({
  preferredAudioKbps: 128,
  preferredVideoP: 720,
});

// Low quality (faster, smaller)
const lq = new PlayEngine({
  preferredAudioKbps: 96,
  preferredVideoP: 480,
});
```

## API Reference

### Exported Functions

#### `searchBest(query: string): Promise<PlayMetadata | null>`

Search for a video on YouTube or get metadata from a URL.

#### `searchPlaylistBest(query: string): Promise<PlayMetadata | null>`

Search for a playlist on YouTube or get metadata from a direct playlist URL. Returns the list ID and base URL.

### PlayEngine Methods

#### `search(query: string): Promise<PlayMetadata | null>`

Search for a video on YouTube or get metadata from URL.

```typescript
// Search by name
const metadata = await engine.search("artist - song name");

// Or use direct URL
const metadata = await engine.search(
  "https://www.youtube.com/watch?v=VIDEO_ID",
);

// Returns: { title, author, duration, durationSeconds, thumb, videoId, url }
```

#### `generateRequestId(prefix?: string): string`

Generate unique request ID for caching.

```typescript
const requestId = engine.generateRequestId("audio"); // "audio_1234567890_abc123"
```

#### `preload(metadata: PlayMetadata, requestId: string): Promise<void>`

Pre-download audio and video in parallel (cached for TTL duration).

```typescript
await engine.preload(metadata, requestId);
// Downloads audio + video if <1h, only audio if >1h (96kbps)
```

#### `getOrDownload(requestId: string, type: 'audio' | 'video'): Promise<Result>`

Get file from cache or download directly.

```typescript
const result = await engine.getOrDownload(requestId, "audio");
// Returns: { metadata, file: { path, size, info, buffer? }, direct: boolean }

console.log(result.file.path); // "/tmp/cache/audio_xxx.m4a"
console.log(result.file.size); // 8457234 (bytes)
console.log(result.file.info.quality); // "128kbps m4a"
console.log(result.direct); // false if from cache, true if direct download
```

#### `waitCache(requestId: string, type: 'audio' | 'video', timeoutMs?: number, intervalMs?: number): Promise<CachedFile | null>`

Wait for cache to be ready (useful for checking preload status).

```typescript
const cached = await engine.waitCache(requestId, "audio", 8000, 500);
if (cached) {
  console.log("Cache ready:", cached.path);
} else {
  console.log("Timeout - falling back to direct download");
}
```

#### `cleanup(requestId: string): void`

Remove cached files for a request.

```typescript
engine.cleanup(requestId); // Deletes audio + video from cache
```

#### `getFromCache(requestId: string): CacheEntry | undefined`

Get cache entry metadata (without downloading).

```typescript
const entry = engine.getFromCache(requestId);
if (entry) {
  console.log(entry.metadata.title);
  console.log(entry.audio?.path);
  console.log(entry.loading); // true if preload in progress
}
```

### YtDlpClient Methods

#### `getPlaylistInfo(url: string, opts?: YtDlpPlaylistOptions): Promise<YtDlpPlaylistInfo>`

Fetches all entries from a playlist efficiently. If `resolveLinks: true` is provided in options, it will batch-resolve the direct playable streaming URLs for each valid track up to the provided `limit`.

### StalkerEngine Methods

The `StalkerEngine` allows you to deeply inspect channels, videos, and live streams to extract valuable metadata like hidden tags, exact subscriber counts, and real-time live viewers.

```typescript
import { YtDlpClient, StalkerEngine } from "@irithell-js/yt-play";

const client = new YtDlpClient({ cookiesFromBrowser: "firefox" });
const stalker = new StalkerEngine(client);
```

#### `stalkVideoOrLive(url: string): Promise<YtDlpVideoMetadata>`

Extracts complete metadata from a single video or live stream.

```typescript
const live = await stalker.stalkVideoOrLive(
  "https://youtube.com/watch?v=LIVE_ID",
);

console.log(live.title);
console.log(live.view_count);

if (live.is_live) {
  console.log(`Live right now with ${live.concurrent_view_count} viewers!`);
}
```

#### `stalkChannel(url: string, opts?: StalkChannelOptions): Promise<YtDlpChannelMetadata>`

Scrapes channel data. You can target specific tabs (videos, shorts, streams, popular) and set specific ranges.

```typescript
// Get the 5 most recent videos from a channel
const recentVideos = await stalker.stalkChannel(
  "https://youtube.com/@MrBeast",
  {
    flat: true, // Faster extraction (basic info only)
    tab: "videos", // "videos" | "shorts" | "streams" | "popular" | "featured"
    endItem: 5, // Get items 1 to 5
  },
);

// Get videos from index 500 to 510
const olderVideos = await stalker.stalkChannel("https://youtube.com/@MrBeast", {
  flat: true,
  tab: "videos",
  startItem: 500,
  endItem: 510,
});
```

### StreamEngine Methods

The `StreamEngine` bypasses disk caching completely and returns a Node.js `Readable` stream. This is ideal for Discord music bots (passing the stream directly to the voice connection) or proxying the download via an HTTP API.

It is automatically available inside the `PlayEngine` or can be initialized separately.

#### `getAudioStream(url: string, qualityKbps?: number): Promise<StreamInfo>`

Returns a readable audio stream (defaults to 128kbps M4A).

```typescript
import fs from "node:fs";

const { stream, quality, format } = await engine.stream.getAudioStream(
  "https://youtube.com/watch?v=VIDEO_ID",
  128, // preferred kbps
);

console.log(`Streaming ${quality} in ${format} format...`);

// Pipe directly to a file, an HTTP response, or Discord voice
stream.pipe(fs.createWriteStream("output.m4a"));
```

#### `getVideoStream(url: string, qualityP?: number): Promise<StreamInfo>`

Returns a readable video stream (defaults to 720p MP4).

```typescript
const { stream, quality, format } = await engine.stream.getVideoStream(
  "https://youtube.com/watch?v=VIDEO_ID",
  1080, // preferred height (1080p, 720p, etc)
);

stream.pipe(fs.createWriteStream("output.mp4"));
```

## Auto-Update System

The engine includes an intelligent auto-update system for yt-dlp:

### How it works

1. **Constant Background Checks** - Checks GitHub for new yt-dlp releases
2. **Instant Updates on Failure** - If a download fails, immediately checks and updates yt-dlp
3. **Automatic Retry** - After updating, automatically retries the failed download
4. **Version Detection** - Compares installed binary version with latest GitHub release
5. **Zero Downtime** - Updates happen in background without blocking your application

### Update Behavior

```typescript
const engine = new PlayEngine();
// ✓ Checks for updates in background

// If download fails
await engine.getOrDownload(requestId, "audio");
// <!> Download failed. Forcing yt-dlp update check...
// >> Updating yt-dlp to 2026.02.04...
// ✓ Updated to 2026.02.04
// >> Retrying download after update...
// ✓ Success
```

### Manual Update Check

To force an update check, simply trigger a download. The system will automatically update if needed.

## Advanced Usage

### Handle Long Videos (>1h)

```typescript
const metadata = await engine.search("2h music mix");

// Automatically uses:
// - Audio: 96kbps (reduced quality)
// - Video: skipped (audio only)
await engine.preload(metadata, requestId);

const { file } = await engine.getOrDownload(requestId, "audio");
// Fast download with reduced quality
```

### Custom Cache Directory

```typescript
import path from "path";

const engine = new PlayEngine({
  cacheDir: path.join(process.cwd(), "downloads"),
  ttlMs: 10 * 60_000, // 10 minutes
});
```

### Performance Monitoring

```typescript
const startTime = Date.now();
await engine.preload(metadata, requestId);
const preloadTime = Date.now() - startTime;

console.log(`Preload took ${(preloadTime / 1000).toFixed(2)}s`);
```

### Error Handling

```typescript
try {
  const metadata = await engine.search("non-existent-video");
  if (!metadata) {
    console.error("Video not found");
    return;
  }

  await engine.preload(metadata, requestId);
  const { file } = await engine.getOrDownload(requestId, "audio");
  console.log("Success:", file.path);
} catch (error) {
  console.error("Download failed:", error.message);
  // Engine automatically tries to update yt-dlp and retry
}
```

## Performance

With aria2c enabled (default):

| Video Length | Audio Download | Video Download | Total Time |
| ------------ | -------------- | -------------- | ---------- |
| 5 min        | ~3-5s          | ~6-8s          | ~8s        |
| 1 hour       | ~15-20s        | Audio only     | ~20s       |
| 2 hours      | ~25-30s        | Audio only     | ~30s       |

_Times may vary based on network speed and YouTube throttling_

_The values are based on local tests with optimized caching, for downloading long videos use direct download_

## File Formats

- **Audio**: M4A (native format, no conversion needed)
- **Video**: MP4 (with audio merged)

M4A provides better quality-to-size ratio and downloads 2-5x faster (no re-encoding).

## Requirements

- Node.js >= 18.0.0
- ~50MB disk space for binaries (auto-downloaded)
- Optional: ffmpeg for advanced features

## Binaries

The package automatically downloads and manages:

- **yt-dlp** (auto-updates to latest version) (~35 MB)
- **aria2c** v1.37.0 (12 MB)

Binaries are platform-specific and downloaded on first `npm install`.

### Auto-Update Schedule

- **Background Check**: Constant version check
- **On-Demand**: Immediately when download fails
- **Version Source**: GitHub Releases API

### Supported Platforms

- Linux x64 / arm64
- macOS x64 / arm64 (Apple Silicon)
- Windows x64

### Manual Binary Paths

```typescript
const engine = new PlayEngine({
  ytdlpBinaryPath: "/custom/path/yt-dlp",
  aria2cPath: "/custom/path/aria2c",
});
```

## Troubleshooting

### Slow Downloads

```typescript
// Enable aria2c explicitly
const engine = new PlayEngine({
  useAria2c: true,
  concurrentFragments: 10, // Increase parallelism
});
```

### Cache Issues

```typescript
// Clear cache directory manually
import fs from "fs";
fs.rmSync("./cache", { recursive: true, force: true });
```

### Binary Not Found

Binaries are auto-downloaded to `node_modules/@irithell-js/yt-play/bin/`. If missing:

```bash
npm rebuild @irithell-js/yt-play
```

### Cookie Authentication Issues

If downloads fail with "Sign in to confirm you're not a bot":

1. Export fresh cookies from your browser
2. Ensure cookies.txt is in Netscape format
3. Verify cookie file path is correct
4. Try `cookiesFromBrowser` option if browser is installed

### Update Check Failures

If auto-update fails:

```bash
# Manually update binaries
cd node_modules/@irithell-js/yt-play
node scripts/setup-binaries.mjs
```

## License

MIT

## Contributing

Issues and PRs welcome!

## Changelog

Deprecated versions have been removed to prevent errors during use.

### 0.3.3 (Latest)

- Added `StalkerEngine` for deep metadata extraction from videos, live streams, and entire channels.
- Added advanced channel scraping with support for specific tabs (`videos`, `shorts`, `streams`, `popular`) and precise item ranges.
- Added real-time live stream metadata detection (`is_live`, `concurrent_view_count`, etc.).
- Added `StreamEngine` to return raw Node.js `Readable` streams for audio and video, bypassing disk storage entirely (ideal for Discord bots and HTTP streaming).
- Improved error handling to gracefully return empty arrays when a requested channel tab does not exist instead of crashing.

### 0.3.2

- Added full support for YouTube Playlists (search by name or URL via `searchPlaylistBest`)
- Added fast batch processing to resolve direct media URLs from playlists (`resolveLinks`) with auto-replenishment
- Fixed race conditions in the `PlayEngine` auto-update system during concurrent downloads
- Fixed binary setup scripts to use atomic downloads (temp files), preventing missing or corrupted binaries on connection drops
- Added automatic cleanup of leftover `.part` and `.ytdl` temporary files on download failure or timeout

### 0.2.8

- Fixed Codec for shorts to work in all plataforms

### 0.2.7

- Fixed parse for shorts

### 0.2.6

- Added auto-update system for yt-dlp
- Direct YouTube URL support in search()
- Auto-configuration system (creates yt-dlp.conf)
- Cookie authentication support (cookiesPath/cookiesFromBrowser)
- Fixed URL search returning wrong video
- Improved error handling with automatic retry after update

### 0.2.5

- Added support to direct cookies extraction in pre-built browsers

### 0.2.4

- Added support to cookies.txt

### 0.2.3

- Updated documentation
- Improved error messages

### 0.2.2

- Many syntax errors fixed

### 0.2.1

- Added auto-detection for yt-dlp and aria2c binaries
- Fixed CommonJS compatibility
- Improved error handling for long videos

### 0.2.0

- Initial release with bundled binaries
- aria2c acceleration support
- Intelligent caching system