@mks2508/bundlp
v0.1.36
Published
Bun-native YouTube video/music resolver library
Maintainers
Readme
bundlp
Bun-native TypeScript library for extracting YouTube video and audio streams
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
bun add bundlpQuick Start
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.
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_IDhttps://youtu.be/VIDEO_IDhttps://youtube.com/shorts/VIDEO_IDhttps://youtube.com/embed/VIDEO_IDhttps://youtube.com/live/VIDEO_IDhttps://music.youtube.com/watch?v=VIDEO_ID- Direct video ID:
dQw4w9WgXcQ
VideoInfo
Complete video information returned by extract().
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.
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.
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
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
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
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
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
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
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.
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.
# 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 benchmarkBenchmarking
Compare bundlp performance against other libraries (yt-dlp, YouTube.js).
Compare Libraries
# 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 3Options:
-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.
# 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.jsonThe 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, parsersDocumentation
- API Reference - Complete API documentation
- Examples - More usage examples
- Architecture - Technical architecture
- Status - Current status and comparisons
Development
# 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 cliLicense
MIT
