npm package: @mks2508/bundlp
Description: Bun-native YouTube video/music resolver library
Latest version: 0.1.36

# bundlp

> Bun-native TypeScript library for extracting YouTube video and audio streams

[![E2E Tests](https://img.shields.io/badge/E2E%20Tests-100%25%20PASS-brightgreen)]()
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue)]()
[![Bun](https://img.shields.io/badge/Bun-1.0+-black)]()
[![License](https://img.shields.io/badge/License-MIT-yellow)]()

## Features

- **Complete Extraction**: Video, audio, and combined formats with full metadata
- **Multi-Client Support**: ANDROID_SDKLESS, TV, WEB, IOS with automatic fallback
- **PO Token System**: Superior implementation with SQLite caching (12h TTL)
- **AST-Based Cipher**: Signature decryption using meriyah parser
- **Result Pattern**: Type-safe error handling with `Result<T, E>`
- **ArkType Validation**: Runtime type validation for API responses
- **HLS/DASH Support**: Manifest parsing for streaming formats
- **Zero Python**: Pure TypeScript, optimized for Bun runtime

## Installation

```bash
bun add bundlp
```

## Quick Start

```typescript
import { YouTubeExtractor, isOk } from 'bundlp';

const extractor = new YouTubeExtractor();

// Extract from URL
const result = await extractor.extract('https://youtube.com/watch?v=dQw4w9WgXcQ');

if (isOk(result)) {
  const video = result.value;

  console.log('Title:', video.title);
  console.log('Duration:', video.duration, 'seconds');
  console.log('Channel:', video.channel.name);

  // Get best audio
  const bestAudio = video.formats.audio[0];
  console.log('Best Audio URL:', bestAudio.url);
  console.log('Audio Quality:', bestAudio.audioQuality);
  console.log('Sample Rate:', bestAudio.audioSampleRate);

  // Get best video
  const bestVideo = video.formats.video[0];
  console.log('Best Video URL:', bestVideo.url);
  console.log('Resolution:', bestVideo.qualityLabel);
}
```

## API Reference

### YouTubeExtractor

Main class for extracting video information.

```typescript
import { YouTubeExtractor } from 'bundlp';

const extractor = new YouTubeExtractor({
  cacheDir: '.cache',                  // Optional: cache directory for player.js
  preferredClient: 'ANDROID_SDKLESS',  // Optional: preferred InnerTube client
  poToken: 'your-token'                // Optional: static PO token
});
```

#### Methods

##### `extract(url: string): Promise<Result<VideoInfo, BundlpError>>`

Extracts complete video information from a YouTube URL or video ID.

**Supported URL formats:**
- `https://youtube.com/watch?v=VIDEO_ID`
- `https://youtu.be/VIDEO_ID`
- `https://youtube.com/shorts/VIDEO_ID`
- `https://youtube.com/embed/VIDEO_ID`
- `https://youtube.com/live/VIDEO_ID`
- `https://music.youtube.com/watch?v=VIDEO_ID`
- Direct video ID: `dQw4w9WgXcQ`

### VideoInfo

Complete video information returned by `extract()`.

```typescript
interface VideoInfo {
  id: string;                    // Video ID
  title: string;                 // Video title
  description: string;           // Video description
  duration: number;              // Duration in seconds
  uploadDate?: string;           // ISO date string
  channel: ChannelInfo;          // Channel information
  viewCount: number;             // View count
  thumbnails: Thumbnail[];       // Available thumbnails
  formats: FormatCollection;     // All available formats
  subtitles: Map<string, Subtitle[]>;  // Subtitles by language
  isLive: boolean;               // Is live stream
  isPrivate: boolean;            // Is private video
}
```

### FormatCollection

Categorized formats for easy access.

```typescript
interface FormatCollection {
  combined: Format[];    // Video+Audio (progressive downloads)
  video: Format[];       // Video-only (adaptive streaming)
  audio: Format[];       // Audio-only (adaptive streaming)
  hls?: HlsInfo;         // HLS manifest info
  dash?: DashInfo;       // DASH manifest info
}
```

### Format

Individual format details.

```typescript
interface Format {
  itag: number;              // YouTube format identifier
  url: string;               // Direct playback URL
  mimeType: string;          // MIME type (e.g., 'audio/webm')
  codecs: string[];          // Codec list ['opus']
  bitrate?: number;          // Bitrate in bps

  // Video properties
  width?: number;            // Video width
  height?: number;           // Video height
  fps?: number;              // Frames per second
  qualityLabel?: string;     // e.g., '1080p60'

  // Audio properties
  audioQuality?: string;     // 'AUDIO_QUALITY_LOW/MEDIUM/HIGH'
  audioSampleRate?: number;  // Sample rate in Hz
  audioChannels?: number;    // Number of audio channels

  // Metadata
  contentLength?: number;    // File size in bytes
  approxDurationMs?: number; // Duration in milliseconds
  hasDrm: boolean;           // Has DRM protection
  isAdaptive: boolean;       // Is adaptive format
}
```

## Usage Examples

### Get Best Audio URL

```typescript
import { YouTubeExtractor, isOk } from 'bundlp';

async function getBestAudioUrl(videoUrl: string): Promise<string | null> {
  const extractor = new YouTubeExtractor();
  const result = await extractor.extract(videoUrl);

  if (!isOk(result)) {
    console.error('Extraction failed:', result.error.message);
    return null;
  }

  const { audio } = result.value.formats;

  if (audio.length === 0) {
    console.error('No audio formats available');
    return null;
  }

  // Formats are sorted by quality (best first)
  return audio[0].url;
}

const audioUrl = await getBestAudioUrl('https://youtube.com/watch?v=dQw4w9WgXcQ');
console.log('Audio URL:', audioUrl);
```

### Get All Audio Formats with Details

```typescript
import { YouTubeExtractor, isOk } from 'bundlp';

async function getAudioFormats(videoUrl: string) {
  const extractor = new YouTubeExtractor();
  const result = await extractor.extract(videoUrl);

  if (!isOk(result)) return [];

  return result.value.formats.audio.map(format => ({
    itag: format.itag,
    url: format.url,
    mimeType: format.mimeType,
    codecs: format.codecs.join(', '),
    bitrate: format.bitrate ? `${Math.round(format.bitrate / 1000)} kbps` : 'unknown',
    sampleRate: format.audioSampleRate ? `${format.audioSampleRate} Hz` : 'unknown',
    channels: format.audioChannels || 2,
    quality: format.audioQuality || 'unknown',
    size: format.contentLength
      ? `${(format.contentLength / 1024 / 1024).toFixed(2)} MB`
      : 'unknown'
  }));
}

const formats = await getAudioFormats('https://youtube.com/watch?v=dQw4w9WgXcQ');
console.table(formats);
```

### Get Video Metadata

```typescript
import { YouTubeExtractor, isOk } from 'bundlp';

async function getVideoMetadata(videoUrl: string) {
  const extractor = new YouTubeExtractor();
  const result = await extractor.extract(videoUrl);

  if (!isOk(result)) {
    throw new Error(result.error.message);
  }

  const video = result.value;

  return {
    id: video.id,
    title: video.title,
    description: video.description,
    duration: {
      seconds: video.duration,
      formatted: formatDuration(video.duration)
    },
    channel: {
      name: video.channel.name,
      id: video.channel.id,
      url: video.channel.url
    },
    statistics: {
      views: video.viewCount,
      viewsFormatted: formatNumber(video.viewCount)
    },
    thumbnails: video.thumbnails.map(t => ({
      url: t.url,
      resolution: `${t.width}x${t.height}`
    })),
    uploadDate: video.uploadDate,
    isLive: video.isLive,
    availableFormats: {
      video: video.formats.video.length,
      audio: video.formats.audio.length,
      combined: video.formats.combined.length
    }
  };
}

function formatDuration(seconds: number): string {
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = seconds % 60;
  if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
  return `${m}:${s.toString().padStart(2, '0')}`;
}

function formatNumber(num: number): string {
  if (num >= 1e9) return `${(num / 1e9).toFixed(1)}B`;
  if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
  if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
  return num.toString();
}

const metadata = await getVideoMetadata('https://youtube.com/watch?v=dQw4w9WgXcQ');
console.log(JSON.stringify(metadata, null, 2));
```

### Download Best Quality Audio

```typescript
import { YouTubeExtractor, isOk } from 'bundlp';

async function downloadAudio(videoUrl: string, outputPath: string) {
  const extractor = new YouTubeExtractor();
  const result = await extractor.extract(videoUrl);

  if (!isOk(result)) {
    throw new Error(`Extraction failed: ${result.error.message}`);
  }

  const bestAudio = result.value.formats.audio[0];

  if (!bestAudio) {
    throw new Error('No audio formats available');
  }

  console.log(`Downloading: ${result.value.title}`);
  console.log(`Format: ${bestAudio.mimeType} (${bestAudio.codecs.join(', ')})`);
  console.log(`Bitrate: ${Math.round((bestAudio.bitrate || 0) / 1000)} kbps`);

  const response = await fetch(bestAudio.url);
  const buffer = await response.arrayBuffer();

  await Bun.write(outputPath, buffer);
  console.log(`Saved to: ${outputPath}`);
}

await downloadAudio(
  'https://youtube.com/watch?v=dQw4w9WgXcQ',
  'audio.webm'
);
```

### Get HLS/DASH Streaming URLs

```typescript
import { YouTubeExtractor, isOk } from 'bundlp';

async function getStreamingManifests(videoUrl: string) {
  const extractor = new YouTubeExtractor();
  const result = await extractor.extract(videoUrl);

  if (!isOk(result)) return null;

  const { hls, dash } = result.value.formats;

  return {
    hls: hls ? {
      manifestUrl: hls.manifestUrl,
      variants: hls.variants.map(v => ({
        url: v.url,
        bandwidth: `${Math.round(v.bandwidth / 1000)} kbps`,
        resolution: v.resolution,
        codecs: v.codecs
      }))
    } : null,
    dash: dash ? {
      manifestUrl: dash.manifestUrl,
      duration: dash.duration
    } : null
  };
}

const manifests = await getStreamingManifests('https://youtube.com/watch?v=dQw4w9WgXcQ');
console.log(JSON.stringify(manifests, null, 2));
```

### Filter Formats by Criteria

```typescript
import { YouTubeExtractor, isOk, type Format } from 'bundlp';

async function getFormatsFiltered(videoUrl: string, options: {
  maxBitrate?: number;
  codec?: string;
  minQuality?: string;
}) {
  const extractor = new YouTubeExtractor();
  const result = await extractor.extract(videoUrl);

  if (!isOk(result)) return { audio: [], video: [] };

  const { audio, video } = result.value.formats;

  const filterFormat = (f: Format) => {
    if (options.maxBitrate && f.bitrate && f.bitrate > options.maxBitrate) return false;
    if (options.codec && !f.codecs.some(c => c.includes(options.codec!))) return false;
    return true;
  };

  return {
    audio: audio.filter(filterFormat),
    video: video.filter(filterFormat)
  };
}

// Get only opus audio under 128kbps
const formats = await getFormatsFiltered('https://youtube.com/watch?v=dQw4w9WgXcQ', {
  codec: 'opus',
  maxBitrate: 128000
});
```

## Result Pattern

bundlp uses the Result pattern for type-safe error handling.

```typescript
import { isOk, isErr, match, unwrapOr } from 'bundlp';

// Check result type
if (isOk(result)) {
  console.log(result.value);
} else {
  console.error(result.error);
}

// Pattern matching
match(result, {
  ok: (video) => console.log('Success:', video.title),
  err: (error) => console.error('Error:', error.message)
});

// Default value on error
const video = unwrapOr(result, defaultVideoInfo);
```

## Error Codes

| Code | Description |
|------|-------------|
| `INVALID_URL` | Could not parse video ID from URL |
| `VIDEO_UNAVAILABLE` | Video is unavailable or deleted |
| `NETWORK_ERROR` | Network request failed |
| `PARSE_ERROR` | Failed to parse response |
| `CIPHER_ERROR` | Signature decryption failed |
| `ALL_CLIENTS_FAILED` | All InnerTube clients failed |

## CLI

bundlp includes a CLI for testing and debugging.

```bash
# Extract video info
bun run cli extract https://youtube.com/watch?v=dQw4w9WgXcQ

# List formats
bun run cli formats https://youtube.com/watch?v=dQw4w9WgXcQ

# Debug extraction
bun run cli debug https://youtube.com/watch?v=dQw4w9WgXcQ

# Run benchmarks (bundlp only)
bun run cli benchmark
```

## Benchmarking

Compare bundlp performance against other libraries (yt-dlp, YouTube.js).

### Compare Libraries

```bash
# Compare all three libraries on a single video
bun run cli benchmark compare <url>

# Compare specific libraries
bun run cli benchmark compare <url> -l bundlp,yt-dlp

# Specify output format
bun run cli benchmark compare <url> -o json -f results.json

# With warmup and multiple iterations
bun run cli benchmark compare <url> -w 2 -i 3
```

**Options:**
- `-l, --libraries <libs>` - Libraries to compare (default: bundlp,yt-dlp,YouTube.js)
- `-w, --warmup <count>` - Warmup iterations (default: 1)
- `-i, --iterations <count>` - Measurement iterations (default: 1)
- `--no-validate` - Skip URL playability validation
- `-o, --output <format>` - Output format: console|json|all (default: console)
- `-f, --file <path>` - Output file path for JSON

### Export JSON

Export video information in yt-dlp compatible JSON format.

```bash
# Export to JSON with auto-generated filename
bun run cli benchmark export-json <url>

# Specify output file
bun run cli benchmark export-json <url> -o my-video.json
```

The JSON includes:
- Basic video info (id, title, duration, view count)
- Format count and breakdown (video/audio/combined)
- Highest quality available
- URL playability rate
- bundlp metadata (version, extraction time, comparison results)

## Performance

| Operation | Time (cached) | Time (fresh) |
|-----------|---------------|--------------|
| Full Extraction | ~130ms | ~5s |
| Player.js Parse | ~20ms | ~5s |
| PO Token | ~10ms | ~800ms |
| InnerTube Request | ~120ms | ~120ms |

## Architecture

```
src/
├── core/           # YouTubeExtractor
├── innertube/      # InnerTube API client
├── player/         # Player.js + AST cipher extraction
├── streaming/      # HLS/DASH processing
├── po-token/       # PO Token system
├── http/           # HTTP client with cookies
├── result/         # Result<T,E> pattern
├── types/          # TypeScript types
├── validation/     # ArkType schemas
└── utils/          # Constants, parsers
```

## Documentation

- [API Reference](./docs/API-REFERENCE.md) - Complete API documentation
- [Examples](./docs/EXAMPLES.md) - More usage examples
- [Architecture](./ARCHITECTURE.md) - Technical architecture
- [Status](./docs/STATUS.md) - Current status and comparisons

## Development

```bash
# Install dependencies
bun install

# Run E2E tests
bun test:e2e

# Run unit tests
bun test

# Type check
bunx tsc --noEmit

# Lint
bun run lint

# CLI
bun run cli
```

## License

MIT