@mks2508/bundlp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +495 -0
- package/dist/core/extractor.d.ts +30 -0
- package/dist/core/extractor.d.ts.map +1 -0
- package/dist/http/client.d.ts +50 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/retry.d.ts +22 -0
- package/dist/http/retry.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19021 -0
- package/dist/innertube/client.d.ts +62 -0
- package/dist/innertube/client.d.ts.map +1 -0
- package/dist/player/ast/analyzer.d.ts +16 -0
- package/dist/player/ast/analyzer.d.ts.map +1 -0
- package/dist/player/ast/extractor.d.ts +35 -0
- package/dist/player/ast/extractor.d.ts.map +1 -0
- package/dist/player/ast/matchers.d.ts +40 -0
- package/dist/player/ast/matchers.d.ts.map +1 -0
- package/dist/player/cache.d.ts +60 -0
- package/dist/player/cache.d.ts.map +1 -0
- package/dist/player/player.d.ts +49 -0
- package/dist/player/player.d.ts.map +1 -0
- package/dist/po-token/botguard/challenge.d.ts +22 -0
- package/dist/po-token/botguard/challenge.d.ts.map +1 -0
- package/dist/po-token/botguard/client.d.ts +25 -0
- package/dist/po-token/botguard/client.d.ts.map +1 -0
- package/dist/po-token/cache/token-cache.d.ts +24 -0
- package/dist/po-token/cache/token-cache.d.ts.map +1 -0
- package/dist/po-token/index.d.ts +14 -0
- package/dist/po-token/index.d.ts.map +1 -0
- package/dist/po-token/manager.d.ts +34 -0
- package/dist/po-token/manager.d.ts.map +1 -0
- package/dist/po-token/minter/web-minter.d.ts +20 -0
- package/dist/po-token/minter/web-minter.d.ts.map +1 -0
- package/dist/po-token/policies.d.ts +18 -0
- package/dist/po-token/policies.d.ts.map +1 -0
- package/dist/po-token/providers/local.provider.d.ts +26 -0
- package/dist/po-token/providers/local.provider.d.ts.map +1 -0
- package/dist/po-token/providers/provider.interface.d.ts +15 -0
- package/dist/po-token/providers/provider.interface.d.ts.map +1 -0
- package/dist/po-token/types.d.ts +160 -0
- package/dist/po-token/types.d.ts.map +1 -0
- package/dist/result/index.d.ts +6 -0
- package/dist/result/index.d.ts.map +1 -0
- package/dist/result/result.types.d.ts +14 -0
- package/dist/result/result.types.d.ts.map +1 -0
- package/dist/result/result.utils.d.ts +32 -0
- package/dist/result/result.utils.d.ts.map +1 -0
- package/dist/streaming/dash/parser.d.ts +37 -0
- package/dist/streaming/dash/parser.d.ts.map +1 -0
- package/dist/streaming/dash/segments.d.ts +58 -0
- package/dist/streaming/dash/segments.d.ts.map +1 -0
- package/dist/streaming/decipher.d.ts +24 -0
- package/dist/streaming/decipher.d.ts.map +1 -0
- package/dist/streaming/drm.d.ts +26 -0
- package/dist/streaming/drm.d.ts.map +1 -0
- package/dist/streaming/formats.d.ts +20 -0
- package/dist/streaming/formats.d.ts.map +1 -0
- package/dist/streaming/hls/parser.d.ts +10 -0
- package/dist/streaming/hls/parser.d.ts.map +1 -0
- package/dist/streaming/hls/segments.d.ts +37 -0
- package/dist/streaming/hls/segments.d.ts.map +1 -0
- package/dist/streaming/processor.d.ts +20 -0
- package/dist/streaming/processor.d.ts.map +1 -0
- package/dist/types/error.types.d.ts +12 -0
- package/dist/types/error.types.d.ts.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/innertube.types.d.ts +155 -0
- package/dist/types/innertube.types.d.ts.map +1 -0
- package/dist/types/player.types.d.ts +30 -0
- package/dist/types/player.types.d.ts.map +1 -0
- package/dist/types/video.types.d.ts +112 -0
- package/dist/types/video.types.d.ts.map +1 -0
- package/dist/utils/constants.d.ts +129 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/m3u8.d.ts +31 -0
- package/dist/utils/m3u8.d.ts.map +1 -0
- package/dist/utils/xml.d.ts +41 -0
- package/dist/utils/xml.d.ts.map +1 -0
- package/dist/validation/schemas.d.ts +290 -0
- package/dist/validation/schemas.d.ts.map +1 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# bundlp
|
|
2
|
+
|
|
3
|
+
> Bun-native TypeScript library for extracting YouTube video and audio streams
|
|
4
|
+
|
|
5
|
+
[]()
|
|
6
|
+
[]()
|
|
7
|
+
[]()
|
|
8
|
+
[]()
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Complete Extraction**: Video, audio, and combined formats with full metadata
|
|
13
|
+
- **Multi-Client Support**: ANDROID_SDKLESS, TV, WEB, IOS with automatic fallback
|
|
14
|
+
- **PO Token System**: Superior implementation with SQLite caching (12h TTL)
|
|
15
|
+
- **AST-Based Cipher**: Signature decryption using meriyah parser
|
|
16
|
+
- **Result Pattern**: Type-safe error handling with `Result<T, E>`
|
|
17
|
+
- **ArkType Validation**: Runtime type validation for API responses
|
|
18
|
+
- **HLS/DASH Support**: Manifest parsing for streaming formats
|
|
19
|
+
- **Zero Python**: Pure TypeScript, optimized for Bun runtime
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun add bundlp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { YouTubeExtractor, isOk } from 'bundlp';
|
|
31
|
+
|
|
32
|
+
const extractor = new YouTubeExtractor();
|
|
33
|
+
|
|
34
|
+
// Extract from URL
|
|
35
|
+
const result = await extractor.extract('https://youtube.com/watch?v=dQw4w9WgXcQ');
|
|
36
|
+
|
|
37
|
+
if (isOk(result)) {
|
|
38
|
+
const video = result.value;
|
|
39
|
+
|
|
40
|
+
console.log('Title:', video.title);
|
|
41
|
+
console.log('Duration:', video.duration, 'seconds');
|
|
42
|
+
console.log('Channel:', video.channel.name);
|
|
43
|
+
|
|
44
|
+
// Get best audio
|
|
45
|
+
const bestAudio = video.formats.audio[0];
|
|
46
|
+
console.log('Best Audio URL:', bestAudio.url);
|
|
47
|
+
console.log('Audio Quality:', bestAudio.audioQuality);
|
|
48
|
+
console.log('Sample Rate:', bestAudio.audioSampleRate);
|
|
49
|
+
|
|
50
|
+
// Get best video
|
|
51
|
+
const bestVideo = video.formats.video[0];
|
|
52
|
+
console.log('Best Video URL:', bestVideo.url);
|
|
53
|
+
console.log('Resolution:', bestVideo.qualityLabel);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### YouTubeExtractor
|
|
60
|
+
|
|
61
|
+
Main class for extracting video information.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { YouTubeExtractor } from 'bundlp';
|
|
65
|
+
|
|
66
|
+
const extractor = new YouTubeExtractor({
|
|
67
|
+
cacheDir: '.cache', // Optional: cache directory for player.js
|
|
68
|
+
preferredClient: 'ANDROID_SDKLESS', // Optional: preferred InnerTube client
|
|
69
|
+
poToken: 'your-token' // Optional: static PO token
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Methods
|
|
74
|
+
|
|
75
|
+
##### `extract(url: string): Promise<Result<VideoInfo, BundlpError>>`
|
|
76
|
+
|
|
77
|
+
Extracts complete video information from a YouTube URL or video ID.
|
|
78
|
+
|
|
79
|
+
**Supported URL formats:**
|
|
80
|
+
- `https://youtube.com/watch?v=VIDEO_ID`
|
|
81
|
+
- `https://youtu.be/VIDEO_ID`
|
|
82
|
+
- `https://youtube.com/shorts/VIDEO_ID`
|
|
83
|
+
- `https://youtube.com/embed/VIDEO_ID`
|
|
84
|
+
- `https://youtube.com/live/VIDEO_ID`
|
|
85
|
+
- `https://music.youtube.com/watch?v=VIDEO_ID`
|
|
86
|
+
- Direct video ID: `dQw4w9WgXcQ`
|
|
87
|
+
|
|
88
|
+
### VideoInfo
|
|
89
|
+
|
|
90
|
+
Complete video information returned by `extract()`.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface VideoInfo {
|
|
94
|
+
id: string; // Video ID
|
|
95
|
+
title: string; // Video title
|
|
96
|
+
description: string; // Video description
|
|
97
|
+
duration: number; // Duration in seconds
|
|
98
|
+
uploadDate?: string; // ISO date string
|
|
99
|
+
channel: ChannelInfo; // Channel information
|
|
100
|
+
viewCount: number; // View count
|
|
101
|
+
thumbnails: Thumbnail[]; // Available thumbnails
|
|
102
|
+
formats: FormatCollection; // All available formats
|
|
103
|
+
subtitles: Map<string, Subtitle[]>; // Subtitles by language
|
|
104
|
+
isLive: boolean; // Is live stream
|
|
105
|
+
isPrivate: boolean; // Is private video
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### FormatCollection
|
|
110
|
+
|
|
111
|
+
Categorized formats for easy access.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
interface FormatCollection {
|
|
115
|
+
combined: Format[]; // Video+Audio (progressive downloads)
|
|
116
|
+
video: Format[]; // Video-only (adaptive streaming)
|
|
117
|
+
audio: Format[]; // Audio-only (adaptive streaming)
|
|
118
|
+
hls?: HlsInfo; // HLS manifest info
|
|
119
|
+
dash?: DashInfo; // DASH manifest info
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Format
|
|
124
|
+
|
|
125
|
+
Individual format details.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
interface Format {
|
|
129
|
+
itag: number; // YouTube format identifier
|
|
130
|
+
url: string; // Direct playback URL
|
|
131
|
+
mimeType: string; // MIME type (e.g., 'audio/webm')
|
|
132
|
+
codecs: string[]; // Codec list ['opus']
|
|
133
|
+
bitrate?: number; // Bitrate in bps
|
|
134
|
+
|
|
135
|
+
// Video properties
|
|
136
|
+
width?: number; // Video width
|
|
137
|
+
height?: number; // Video height
|
|
138
|
+
fps?: number; // Frames per second
|
|
139
|
+
qualityLabel?: string; // e.g., '1080p60'
|
|
140
|
+
|
|
141
|
+
// Audio properties
|
|
142
|
+
audioQuality?: string; // 'AUDIO_QUALITY_LOW/MEDIUM/HIGH'
|
|
143
|
+
audioSampleRate?: number; // Sample rate in Hz
|
|
144
|
+
audioChannels?: number; // Number of audio channels
|
|
145
|
+
|
|
146
|
+
// Metadata
|
|
147
|
+
contentLength?: number; // File size in bytes
|
|
148
|
+
approxDurationMs?: number; // Duration in milliseconds
|
|
149
|
+
hasDrm: boolean; // Has DRM protection
|
|
150
|
+
isAdaptive: boolean; // Is adaptive format
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Usage Examples
|
|
155
|
+
|
|
156
|
+
### Get Best Audio URL
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { YouTubeExtractor, isOk } from 'bundlp';
|
|
160
|
+
|
|
161
|
+
async function getBestAudioUrl(videoUrl: string): Promise<string | null> {
|
|
162
|
+
const extractor = new YouTubeExtractor();
|
|
163
|
+
const result = await extractor.extract(videoUrl);
|
|
164
|
+
|
|
165
|
+
if (!isOk(result)) {
|
|
166
|
+
console.error('Extraction failed:', result.error.message);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { audio } = result.value.formats;
|
|
171
|
+
|
|
172
|
+
if (audio.length === 0) {
|
|
173
|
+
console.error('No audio formats available');
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Formats are sorted by quality (best first)
|
|
178
|
+
return audio[0].url;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const audioUrl = await getBestAudioUrl('https://youtube.com/watch?v=dQw4w9WgXcQ');
|
|
182
|
+
console.log('Audio URL:', audioUrl);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Get All Audio Formats with Details
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { YouTubeExtractor, isOk } from 'bundlp';
|
|
189
|
+
|
|
190
|
+
async function getAudioFormats(videoUrl: string) {
|
|
191
|
+
const extractor = new YouTubeExtractor();
|
|
192
|
+
const result = await extractor.extract(videoUrl);
|
|
193
|
+
|
|
194
|
+
if (!isOk(result)) return [];
|
|
195
|
+
|
|
196
|
+
return result.value.formats.audio.map(format => ({
|
|
197
|
+
itag: format.itag,
|
|
198
|
+
url: format.url,
|
|
199
|
+
mimeType: format.mimeType,
|
|
200
|
+
codecs: format.codecs.join(', '),
|
|
201
|
+
bitrate: format.bitrate ? `${Math.round(format.bitrate / 1000)} kbps` : 'unknown',
|
|
202
|
+
sampleRate: format.audioSampleRate ? `${format.audioSampleRate} Hz` : 'unknown',
|
|
203
|
+
channels: format.audioChannels || 2,
|
|
204
|
+
quality: format.audioQuality || 'unknown',
|
|
205
|
+
size: format.contentLength
|
|
206
|
+
? `${(format.contentLength / 1024 / 1024).toFixed(2)} MB`
|
|
207
|
+
: 'unknown'
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const formats = await getAudioFormats('https://youtube.com/watch?v=dQw4w9WgXcQ');
|
|
212
|
+
console.table(formats);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Get Video Metadata
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { YouTubeExtractor, isOk } from 'bundlp';
|
|
219
|
+
|
|
220
|
+
async function getVideoMetadata(videoUrl: string) {
|
|
221
|
+
const extractor = new YouTubeExtractor();
|
|
222
|
+
const result = await extractor.extract(videoUrl);
|
|
223
|
+
|
|
224
|
+
if (!isOk(result)) {
|
|
225
|
+
throw new Error(result.error.message);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const video = result.value;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
id: video.id,
|
|
232
|
+
title: video.title,
|
|
233
|
+
description: video.description,
|
|
234
|
+
duration: {
|
|
235
|
+
seconds: video.duration,
|
|
236
|
+
formatted: formatDuration(video.duration)
|
|
237
|
+
},
|
|
238
|
+
channel: {
|
|
239
|
+
name: video.channel.name,
|
|
240
|
+
id: video.channel.id,
|
|
241
|
+
url: video.channel.url
|
|
242
|
+
},
|
|
243
|
+
statistics: {
|
|
244
|
+
views: video.viewCount,
|
|
245
|
+
viewsFormatted: formatNumber(video.viewCount)
|
|
246
|
+
},
|
|
247
|
+
thumbnails: video.thumbnails.map(t => ({
|
|
248
|
+
url: t.url,
|
|
249
|
+
resolution: `${t.width}x${t.height}`
|
|
250
|
+
})),
|
|
251
|
+
uploadDate: video.uploadDate,
|
|
252
|
+
isLive: video.isLive,
|
|
253
|
+
availableFormats: {
|
|
254
|
+
video: video.formats.video.length,
|
|
255
|
+
audio: video.formats.audio.length,
|
|
256
|
+
combined: video.formats.combined.length
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatDuration(seconds: number): string {
|
|
262
|
+
const h = Math.floor(seconds / 3600);
|
|
263
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
264
|
+
const s = seconds % 60;
|
|
265
|
+
if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
266
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatNumber(num: number): string {
|
|
270
|
+
if (num >= 1e9) return `${(num / 1e9).toFixed(1)}B`;
|
|
271
|
+
if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
|
|
272
|
+
if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
|
|
273
|
+
return num.toString();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const metadata = await getVideoMetadata('https://youtube.com/watch?v=dQw4w9WgXcQ');
|
|
277
|
+
console.log(JSON.stringify(metadata, null, 2));
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Download Best Quality Audio
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { YouTubeExtractor, isOk } from 'bundlp';
|
|
284
|
+
|
|
285
|
+
async function downloadAudio(videoUrl: string, outputPath: string) {
|
|
286
|
+
const extractor = new YouTubeExtractor();
|
|
287
|
+
const result = await extractor.extract(videoUrl);
|
|
288
|
+
|
|
289
|
+
if (!isOk(result)) {
|
|
290
|
+
throw new Error(`Extraction failed: ${result.error.message}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const bestAudio = result.value.formats.audio[0];
|
|
294
|
+
|
|
295
|
+
if (!bestAudio) {
|
|
296
|
+
throw new Error('No audio formats available');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
console.log(`Downloading: ${result.value.title}`);
|
|
300
|
+
console.log(`Format: ${bestAudio.mimeType} (${bestAudio.codecs.join(', ')})`);
|
|
301
|
+
console.log(`Bitrate: ${Math.round((bestAudio.bitrate || 0) / 1000)} kbps`);
|
|
302
|
+
|
|
303
|
+
const response = await fetch(bestAudio.url);
|
|
304
|
+
const buffer = await response.arrayBuffer();
|
|
305
|
+
|
|
306
|
+
await Bun.write(outputPath, buffer);
|
|
307
|
+
console.log(`Saved to: ${outputPath}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await downloadAudio(
|
|
311
|
+
'https://youtube.com/watch?v=dQw4w9WgXcQ',
|
|
312
|
+
'audio.webm'
|
|
313
|
+
);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Get HLS/DASH Streaming URLs
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { YouTubeExtractor, isOk } from 'bundlp';
|
|
320
|
+
|
|
321
|
+
async function getStreamingManifests(videoUrl: string) {
|
|
322
|
+
const extractor = new YouTubeExtractor();
|
|
323
|
+
const result = await extractor.extract(videoUrl);
|
|
324
|
+
|
|
325
|
+
if (!isOk(result)) return null;
|
|
326
|
+
|
|
327
|
+
const { hls, dash } = result.value.formats;
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
hls: hls ? {
|
|
331
|
+
manifestUrl: hls.manifestUrl,
|
|
332
|
+
variants: hls.variants.map(v => ({
|
|
333
|
+
url: v.url,
|
|
334
|
+
bandwidth: `${Math.round(v.bandwidth / 1000)} kbps`,
|
|
335
|
+
resolution: v.resolution,
|
|
336
|
+
codecs: v.codecs
|
|
337
|
+
}))
|
|
338
|
+
} : null,
|
|
339
|
+
dash: dash ? {
|
|
340
|
+
manifestUrl: dash.manifestUrl,
|
|
341
|
+
duration: dash.duration
|
|
342
|
+
} : null
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const manifests = await getStreamingManifests('https://youtube.com/watch?v=dQw4w9WgXcQ');
|
|
347
|
+
console.log(JSON.stringify(manifests, null, 2));
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Filter Formats by Criteria
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { YouTubeExtractor, isOk, type Format } from 'bundlp';
|
|
354
|
+
|
|
355
|
+
async function getFormatsFiltered(videoUrl: string, options: {
|
|
356
|
+
maxBitrate?: number;
|
|
357
|
+
codec?: string;
|
|
358
|
+
minQuality?: string;
|
|
359
|
+
}) {
|
|
360
|
+
const extractor = new YouTubeExtractor();
|
|
361
|
+
const result = await extractor.extract(videoUrl);
|
|
362
|
+
|
|
363
|
+
if (!isOk(result)) return { audio: [], video: [] };
|
|
364
|
+
|
|
365
|
+
const { audio, video } = result.value.formats;
|
|
366
|
+
|
|
367
|
+
const filterFormat = (f: Format) => {
|
|
368
|
+
if (options.maxBitrate && f.bitrate && f.bitrate > options.maxBitrate) return false;
|
|
369
|
+
if (options.codec && !f.codecs.some(c => c.includes(options.codec!))) return false;
|
|
370
|
+
return true;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
audio: audio.filter(filterFormat),
|
|
375
|
+
video: video.filter(filterFormat)
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Get only opus audio under 128kbps
|
|
380
|
+
const formats = await getFormatsFiltered('https://youtube.com/watch?v=dQw4w9WgXcQ', {
|
|
381
|
+
codec: 'opus',
|
|
382
|
+
maxBitrate: 128000
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Result Pattern
|
|
387
|
+
|
|
388
|
+
bundlp uses the Result pattern for type-safe error handling.
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import { isOk, isErr, match, unwrapOr } from 'bundlp';
|
|
392
|
+
|
|
393
|
+
// Check result type
|
|
394
|
+
if (isOk(result)) {
|
|
395
|
+
console.log(result.value);
|
|
396
|
+
} else {
|
|
397
|
+
console.error(result.error);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Pattern matching
|
|
401
|
+
match(result, {
|
|
402
|
+
ok: (video) => console.log('Success:', video.title),
|
|
403
|
+
err: (error) => console.error('Error:', error.message)
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Default value on error
|
|
407
|
+
const video = unwrapOr(result, defaultVideoInfo);
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Error Codes
|
|
411
|
+
|
|
412
|
+
| Code | Description |
|
|
413
|
+
|------|-------------|
|
|
414
|
+
| `INVALID_URL` | Could not parse video ID from URL |
|
|
415
|
+
| `VIDEO_UNAVAILABLE` | Video is unavailable or deleted |
|
|
416
|
+
| `NETWORK_ERROR` | Network request failed |
|
|
417
|
+
| `PARSE_ERROR` | Failed to parse response |
|
|
418
|
+
| `CIPHER_ERROR` | Signature decryption failed |
|
|
419
|
+
| `ALL_CLIENTS_FAILED` | All InnerTube clients failed |
|
|
420
|
+
|
|
421
|
+
## CLI
|
|
422
|
+
|
|
423
|
+
bundlp includes a CLI for testing and debugging.
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
# Extract video info
|
|
427
|
+
bun run cli extract https://youtube.com/watch?v=dQw4w9WgXcQ
|
|
428
|
+
|
|
429
|
+
# List formats
|
|
430
|
+
bun run cli formats https://youtube.com/watch?v=dQw4w9WgXcQ
|
|
431
|
+
|
|
432
|
+
# Debug extraction
|
|
433
|
+
bun run cli debug https://youtube.com/watch?v=dQw4w9WgXcQ
|
|
434
|
+
|
|
435
|
+
# Run benchmarks
|
|
436
|
+
bun run cli benchmark
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Performance
|
|
440
|
+
|
|
441
|
+
| Operation | Time (cached) | Time (fresh) |
|
|
442
|
+
|-----------|---------------|--------------|
|
|
443
|
+
| Full Extraction | ~130ms | ~5s |
|
|
444
|
+
| Player.js Parse | ~20ms | ~5s |
|
|
445
|
+
| PO Token | ~10ms | ~800ms |
|
|
446
|
+
| InnerTube Request | ~120ms | ~120ms |
|
|
447
|
+
|
|
448
|
+
## Architecture
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
src/
|
|
452
|
+
├── core/ # YouTubeExtractor
|
|
453
|
+
├── innertube/ # InnerTube API client
|
|
454
|
+
├── player/ # Player.js + AST cipher extraction
|
|
455
|
+
├── streaming/ # HLS/DASH processing
|
|
456
|
+
├── po-token/ # PO Token system
|
|
457
|
+
├── http/ # HTTP client with cookies
|
|
458
|
+
├── result/ # Result<T,E> pattern
|
|
459
|
+
├── types/ # TypeScript types
|
|
460
|
+
├── validation/ # ArkType schemas
|
|
461
|
+
└── utils/ # Constants, parsers
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Documentation
|
|
465
|
+
|
|
466
|
+
- [API Reference](./docs/API-REFERENCE.md) - Complete API documentation
|
|
467
|
+
- [Examples](./docs/EXAMPLES.md) - More usage examples
|
|
468
|
+
- [Architecture](./ARCHITECTURE.md) - Technical architecture
|
|
469
|
+
- [Status](./docs/STATUS.md) - Current status and comparisons
|
|
470
|
+
|
|
471
|
+
## Development
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
# Install dependencies
|
|
475
|
+
bun install
|
|
476
|
+
|
|
477
|
+
# Run E2E tests
|
|
478
|
+
bun test:e2e
|
|
479
|
+
|
|
480
|
+
# Run unit tests
|
|
481
|
+
bun test
|
|
482
|
+
|
|
483
|
+
# Type check
|
|
484
|
+
bunx tsc --noEmit
|
|
485
|
+
|
|
486
|
+
# Lint
|
|
487
|
+
bun run lint
|
|
488
|
+
|
|
489
|
+
# CLI
|
|
490
|
+
bun run cli
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## License
|
|
494
|
+
|
|
495
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Result } from '../result';
|
|
2
|
+
import type { VideoInfo } from '../types/video.types';
|
|
3
|
+
import { type BundlpError } from '../types/error.types';
|
|
4
|
+
import type { ClientName } from '../utils/constants';
|
|
5
|
+
export interface ExtractorConfig {
|
|
6
|
+
poToken?: string;
|
|
7
|
+
cacheDir?: string;
|
|
8
|
+
preferredClient?: ClientName;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Main YouTube video extractor class.
|
|
12
|
+
* Handles video information extraction using InnerTube API.
|
|
13
|
+
*/
|
|
14
|
+
export declare class YouTubeExtractor {
|
|
15
|
+
private client;
|
|
16
|
+
private player;
|
|
17
|
+
private config;
|
|
18
|
+
constructor(config?: ExtractorConfig);
|
|
19
|
+
/**
|
|
20
|
+
* Extracts video information from a YouTube URL or video ID.
|
|
21
|
+
* @param url - YouTube URL or video ID
|
|
22
|
+
* @returns Result containing VideoInfo or BundlpError
|
|
23
|
+
*/
|
|
24
|
+
extract(url: string): Promise<Result<VideoInfo, BundlpError>>;
|
|
25
|
+
private parseVideoId;
|
|
26
|
+
private realExtract;
|
|
27
|
+
private processFormats;
|
|
28
|
+
private assembleVideoInfo;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../src/core/extractor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,MAAM,EAAS,MAAM,WAAW,CAAC;AAIxD,OAAO,KAAK,EAAE,SAAS,EAAoB,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAKrD,MAAM,WAAW,eAAe;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,UAAU,CAAC;CAChC;AAaD;;;GAGG;AACH,qBAAa,gBAAgB;IACzB,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAkB;gBAEpB,MAAM,GAAE,eAAoB;IAMxC;;;;OAIG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAgBnE,OAAO,CAAC,YAAY;YAcN,WAAW;YAyBX,cAAc;IAK5B,OAAO,CAAC,iBAAiB;CA2B5B"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Result } from '../result';
|
|
2
|
+
import type { BundlpError } from '../types/error.types';
|
|
3
|
+
export interface HttpClientOptions extends RequestInit {
|
|
4
|
+
timeout?: number;
|
|
5
|
+
maxRedirects?: number;
|
|
6
|
+
manualRedirects?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface Cookie {
|
|
9
|
+
name: string;
|
|
10
|
+
value: string;
|
|
11
|
+
domain?: string;
|
|
12
|
+
path?: string;
|
|
13
|
+
expires?: Date;
|
|
14
|
+
secure?: boolean;
|
|
15
|
+
httpOnly?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* HTTP client with cookie jar and controlled redirect handling.
|
|
19
|
+
* Superior to YouTube.js: tracks cookies across redirects, prevents loops.
|
|
20
|
+
*/
|
|
21
|
+
export declare class HttpClient {
|
|
22
|
+
private cookies;
|
|
23
|
+
/**
|
|
24
|
+
* Performs a fetch request with automatic cookie and redirect handling.
|
|
25
|
+
* @param url - The URL to fetch
|
|
26
|
+
* @param options - Fetch options. Use manualRedirects=true for controlled redirects
|
|
27
|
+
* @returns Result containing the Response or a BundlpError
|
|
28
|
+
*/
|
|
29
|
+
request(url: string, options?: HttpClientOptions): Promise<Result<Response, BundlpError>>;
|
|
30
|
+
clearCookies(): void;
|
|
31
|
+
getCookies(): Map<string, Cookie>;
|
|
32
|
+
/**
|
|
33
|
+
* Performs a GET request.
|
|
34
|
+
* @param url - The URL to fetch
|
|
35
|
+
* @param options - Optional fetch options
|
|
36
|
+
* @returns Result containing the Response or a BundlpError
|
|
37
|
+
*/
|
|
38
|
+
get(url: string, options?: HttpClientOptions): Promise<Result<Response, BundlpError>>;
|
|
39
|
+
/**
|
|
40
|
+
* Performs a POST request with JSON body.
|
|
41
|
+
* @param url - The URL to post to
|
|
42
|
+
* @param body - The request body (will be JSON stringified)
|
|
43
|
+
* @param options - Optional fetch options
|
|
44
|
+
* @returns Result containing the Response or a BundlpError
|
|
45
|
+
*/
|
|
46
|
+
post(url: string, body: unknown, options?: HttpClientOptions): Promise<Result<Response, BundlpError>>;
|
|
47
|
+
}
|
|
48
|
+
export declare const httpClient: HttpClient;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/http/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGxD,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,UAAU,MAAM;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AA8BD;;;GAGG;AACH,qBAAa,UAAU;IACnB,OAAO,CAAC,OAAO,CAAkC;IAEjD;;;;;OAKG;IACG,OAAO,CACT,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,iBAAsB,GAChC,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IAkFzC,YAAY,IAAI,IAAI;IAIpB,UAAU,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAIjC;;;;;OAKG;IACG,GAAG,CACL,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,iBAAsB,GAChC,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IAIzC;;;;;;OAMG;IACG,IAAI,CACN,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,iBAAsB,GAChC,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;CAW5C;AAED,eAAO,MAAM,UAAU,YAAmB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utilities with exponential backoff.
|
|
3
|
+
* Provides automatic retry logic for transient failures.
|
|
4
|
+
* @module http/retry
|
|
5
|
+
*/
|
|
6
|
+
export interface RetryOptions {
|
|
7
|
+
retries?: number;
|
|
8
|
+
delay?: number;
|
|
9
|
+
backoff?: number;
|
|
10
|
+
maxDelay?: number;
|
|
11
|
+
shouldRetry?: (error: Error) => boolean;
|
|
12
|
+
onRetry?: (error: Error, attempt: number) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Executes a function with automatic retry on failure.
|
|
16
|
+
* @param fn - The async function to execute
|
|
17
|
+
* @param options - Retry configuration options
|
|
18
|
+
* @returns The result of the function if successful
|
|
19
|
+
* @throws The last error if all retries are exhausted
|
|
20
|
+
*/
|
|
21
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
22
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/http/retry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,WAAW,YAAY;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACxC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAID;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC7B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE,YAAiB,GAC3B,OAAO,CAAC,CAAC,CAAC,CA2BZ"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bundlp - Bun-native YouTube video/music resolver
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { YouTubeExtractor, isOk } from 'bundlp';
|
|
7
|
+
*
|
|
8
|
+
* const extractor = new YouTubeExtractor();
|
|
9
|
+
* const result = await extractor.extract('https://youtube.com/watch?v=dQw4w9WgXcQ');
|
|
10
|
+
*
|
|
11
|
+
* if (isOk(result)) {
|
|
12
|
+
* console.log(result.value.title);
|
|
13
|
+
* console.log(result.value.formats.video);
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { type Result, type ResultOk, type ResultErr, ok, err, isOk, isErr, match, map, mapErr, andThen, unwrapOr, unwrap, tryCatch, tryCatchAsync, } from './result';
|
|
18
|
+
export type { VideoInfo, ChannelInfo, Thumbnail, FormatCollection, Format, AudioFormat, VideoFormat, HlsInfo, HlsVariant, DashInfo, Subtitle, Chapter, Storyboard, ErrorCode, BundlpError, ClientName, ClientConfig, InnerTubeContext, PlayerResponse, StreamingData, PlayerInfo, SignatureCipher, } from './types';
|
|
19
|
+
export { YouTubeExtractor, type ExtractorConfig } from './core/extractor';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EACL,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,EAAE,EACF,GAAG,EACH,IAAI,EACJ,KAAK,EACL,KAAK,EACL,GAAG,EACH,MAAM,EACN,OAAO,EACP,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,aAAa,GACd,MAAM,UAAU,CAAC;AAGlB,YAAY,EAEV,SAAS,EACT,WAAW,EACX,SAAS,EACT,gBAAgB,EAChB,MAAM,EACN,WAAW,EACX,WAAW,EACX,OAAO,EACP,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,UAAU,EAEV,SAAS,EACT,WAAW,EAEX,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,aAAa,EAEb,UAAU,EACV,eAAe,GAChB,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
|