yt-direct 1.0.0 → 1.0.1

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/dist/index.mjs ADDED
@@ -0,0 +1,1281 @@
1
+ /*! yt-direct v1.0.1 | MIT */
2
+
3
+ (function(){
4
+ 'use strict';
5
+
6
+ const __m = {
7
+ "index":[function(module,exports,__r__){
8
+ const { fetch } = __r__('core/InnerTube');
9
+ const { YouTubeError, FormatError, ValidationError, QualityError, MergeError, NetworkError } = __r__('core/Errors');
10
+ const { FormatSelector } = __r__('formats/Selector');
11
+ const { resolveContainer, requiresConversion, QUALITY_TIERS, CONTAINER_MAP } = __r__('formats/Registry');
12
+ const { validateOptions } = __r__('utils/validators');
13
+ const { extractVideoId } = __r__('utils/url');
14
+ const { merge } = __r__('download/Merge');
15
+ const { downloadToFile, createReadStream, verify } = __r__('download/Downloader');
16
+ const { createStream } = __r__('download/Stream');
17
+ const { VERSION } = __r__('utils/constants');
18
+
19
+ function ytdl(input, options = {}) {
20
+ const videoId = extractVideoId(input);
21
+ if (!videoId) {
22
+ return Promise.reject(new ValidationError(`Invalid YouTube URL or video ID: "${input}"`));
23
+ }
24
+
25
+ const normalized = validateOptions(options);
26
+
27
+ return (async () => {
28
+ const info = await getInfo(videoId);
29
+ const selector = new FormatSelector(info.streamingData);
30
+ const selected = selector.select(normalized);
31
+
32
+ const format = selected.format;
33
+ const audio = selected.audio || null;
34
+
35
+ const response = {
36
+ videoId,
37
+ title: info.title || 'video',
38
+ url: format.url,
39
+ format,
40
+ audio,
41
+ type: selected.type,
42
+ stream: () => createStream(format.url),
43
+ pipe: (writable) => createStream(format.url).pipe(writable),
44
+ download: async (filePath) => {
45
+ if (!filePath) {
46
+ const ext = normalized.format || format.container || 'mp4';
47
+ filePath = `${sanitize(info.title || 'video')}.${ext}`;
48
+ }
49
+ return downloadToFile(format.url, filePath, {
50
+ concurrency: normalized.concurrency,
51
+ onProgress: normalized.onProgress,
52
+ });
53
+ },
54
+ };
55
+
56
+ if (normalized.merge && audio) {
57
+ response.merge = async (outputPath) => {
58
+ if (!outputPath) {
59
+ const ext = normalized.format || 'mp4';
60
+ outputPath = `${sanitize(info.title || 'video')}.${ext}`;
61
+ }
62
+ const fs = require('node:fs');
63
+ const tmpVideo = `/tmp/yt-direct-${format.itag}-video`;
64
+ const tmpAudio = `/tmp/yt-direct-${audio.itag}-audio`;
65
+ try {
66
+ await Promise.all([
67
+ downloadToFile(format.url, tmpVideo),
68
+ downloadToFile(audio.url, tmpAudio),
69
+ ]);
70
+ await merge(tmpVideo, tmpAudio, outputPath, normalized.merge);
71
+ return outputPath;
72
+ } finally {
73
+ try { fs.unlinkSync(tmpVideo); } catch {}
74
+ try { fs.unlinkSync(tmpAudio); } catch {}
75
+ }
76
+ };
77
+ }
78
+
79
+ return response;
80
+ })();
81
+ }
82
+
83
+ async function getInfo(input) {
84
+ const videoId = extractVideoId(input);
85
+ if (!videoId) throw new ValidationError(`Invalid YouTube URL or video ID: "${input}"`);
86
+
87
+ const result = await fetch(videoId);
88
+ const selector = new FormatSelector(result.streamingData);
89
+
90
+ return {
91
+ id: videoId,
92
+ title: result.videoDetails.title || 'Unknown',
93
+ author: result.videoDetails.author || result.videoDetails.channelId || null,
94
+ duration: parseInt(result.videoDetails.lengthSeconds || '0', 10),
95
+ thumbnails: result.videoDetails.thumbnail?.thumbnails || [],
96
+ description: result.videoDetails.shortDescription || '',
97
+ viewCount: parseInt(result.videoDetails.viewCount || '0', 10),
98
+ isLive: result.videoDetails.isLive === true,
99
+ streamingData: result.streamingData,
100
+ formats: selector.list(),
101
+ combined: selector.combined.map((f) => f.toJSON()),
102
+ adaptive: selector.adaptive.map((f) => f.toJSON()),
103
+ clientUsed: result.clientUsed,
104
+ };
105
+ }
106
+
107
+ function sanitize(name) {
108
+ return String(name || 'video').replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, ' ').trim() || 'video';
109
+ }
110
+
111
+ ytdl.getInfo = getInfo;
112
+ ytdl.getFormats = (input) => getInfo(input).then((i) => i.formats);
113
+ ytdl.verifyURL = verify;
114
+ ytdl.createStream = createStream;
115
+ ytdl.version = VERSION;
116
+ ytdl.FORMATS = Object.keys(CONTAINER_MAP);
117
+ ytdl.QUALITIES = [...QUALITY_TIERS, 'auto', 'best', 'audio'];
118
+ ytdl.YouTubeError = YouTubeError;
119
+ ytdl.FormatError = FormatError;
120
+ ytdl.ValidationError = ValidationError;
121
+ ytdl.QualityError = QualityError;
122
+ ytdl.MergeError = MergeError;
123
+ ytdl.NetworkError = NetworkError;
124
+
125
+ module.exports = ytdl;
126
+ module.exports.default = ytdl;
127
+
128
+ },["core/InnerTube","core/Errors","formats/Selector","formats/Registry","utils/validators","utils/url","download/Merge","download/Downloader","download/Stream","utils/constants"]],
129
+ "core/InnerTube":[function(module,exports,__r__){
130
+ const { request } = __r__('core/Client');
131
+ const { YouTubeError } = __r__('core/Errors');
132
+
133
+ const CLIENT_PROFILES = [
134
+ {
135
+ name: 'ANDROID',
136
+ version: '20.10.38',
137
+ sdk: 30,
138
+ os: 'Android',
139
+ osVer: '11',
140
+ ua: 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip',
141
+ },
142
+ {
143
+ name: 'ANDROID_VR',
144
+ version: '1.71.26',
145
+ sdk: 32,
146
+ os: 'Android',
147
+ osVer: '12L',
148
+ ua: 'com.google.android.apps.youtube.vr.oculus/1.71.26 (Linux; U; Android 12L) gzip',
149
+ },
150
+ ];
151
+
152
+ function buildContext(client) {
153
+ return {
154
+ client: {
155
+ clientName: client.name,
156
+ clientVersion: client.version,
157
+ androidSdkVersion: client.sdk,
158
+ osName: client.os,
159
+ osVersion: client.osVer,
160
+ userAgent: client.ua,
161
+ hl: 'en',
162
+ gl: 'US',
163
+ timeZone: 'UTC',
164
+ utcOffsetMinutes: 0,
165
+ },
166
+ };
167
+ }
168
+
169
+ function playerPayload(videoId, client) {
170
+ return JSON.stringify({
171
+ context: buildContext(client),
172
+ videoId,
173
+ contentCheckOk: true,
174
+ racyCheckOk: true,
175
+ });
176
+ }
177
+
178
+ function parseStreamingData(raw) {
179
+ if (!raw || raw.error) {
180
+ const msg = raw?.error?.message || 'Unknown API error';
181
+ throw new YouTubeError(msg, 'API_ERROR', raw?.error);
182
+ }
183
+ const sd = raw.streamingData;
184
+ if (!sd) {
185
+ throw new YouTubeError('No streaming data in response', 'NO_STREAMING_DATA');
186
+ }
187
+ return sd;
188
+ }
189
+
190
+ function extractFormats(streamingData) {
191
+ return {
192
+ combined: (streamingData.formats || []).filter((f) => f.url),
193
+ adaptive: (streamingData.adaptiveFormats || []).filter((f) => f.url),
194
+ };
195
+ }
196
+
197
+ async function fetch(videoId) {
198
+ let lastError = null;
199
+
200
+ for (const client of CLIENT_PROFILES) {
201
+ try {
202
+ const payload = playerPayload(videoId, client);
203
+ const raw = await request(payload);
204
+ const sd = parseStreamingData(raw);
205
+ const { combined, adaptive } = extractFormats(sd);
206
+
207
+ if (combined.length > 0 || adaptive.length > 0) {
208
+ return {
209
+ raw,
210
+ videoDetails: raw.videoDetails || {},
211
+ streamingData: sd,
212
+ combined,
213
+ adaptive,
214
+ clientUsed: client.name,
215
+ };
216
+ }
217
+ } catch (err) {
218
+ lastError = err;
219
+ }
220
+ }
221
+
222
+ throw lastError || new YouTubeError('All InnerTube clients failed', 'ALL_CLIENTS_FAILED');
223
+ }
224
+
225
+ module.exports = { fetch, CLIENT_PROFILES };
226
+
227
+ },["core/Client","core/Errors"]],
228
+ "core/Errors":[function(module,exports,__r__){
229
+ class YouTubeError extends Error {
230
+ constructor(message, code = 'YOUTUBE_ERROR', details = null) {
231
+ super(message);
232
+ this.name = 'YouTubeError';
233
+ this.code = code;
234
+ this.details = details;
235
+ Error.captureStackTrace(this, this.constructor);
236
+ }
237
+ }
238
+
239
+ class FormatError extends YouTubeError {
240
+ constructor(message, details = null) {
241
+ super(message, 'FORMAT_ERROR', details);
242
+ this.name = 'FormatError';
243
+ }
244
+ }
245
+
246
+ class QualityError extends YouTubeError {
247
+ constructor(message, details = null) {
248
+ super(message, 'QUALITY_ERROR', details);
249
+ this.name = 'QualityError';
250
+ }
251
+ }
252
+
253
+ class MergeError extends YouTubeError {
254
+ constructor(message, details = null) {
255
+ super(message, 'MERGE_ERROR', details);
256
+ this.name = 'MergeError';
257
+ }
258
+ }
259
+
260
+ class NetworkError extends YouTubeError {
261
+ constructor(message, details = null) {
262
+ super(message, 'NETWORK_ERROR', details);
263
+ this.name = 'NetworkError';
264
+ }
265
+ }
266
+
267
+ class ValidationError extends YouTubeError {
268
+ constructor(message, details = null) {
269
+ super(message, 'VALIDATION_ERROR', details);
270
+ this.name = 'ValidationError';
271
+ }
272
+ }
273
+
274
+ module.exports = {
275
+ YouTubeError,
276
+ FormatError,
277
+ QualityError,
278
+ MergeError,
279
+ NetworkError,
280
+ ValidationError,
281
+ };
282
+
283
+ },[]],
284
+ "formats/Selector":[function(module,exports,__r__){
285
+ const { Format } = __r__('formats/Format');
286
+ const { getFallbackChain, matchQualityRank, toHeight, validateQuality } = __r__('formats/Qualities');
287
+ const { checkContainer, mimeTypeToContainer } = __r__('formats/mime');
288
+ const { FormatError } = __r__('core/Errors');
289
+
290
+ class FormatSelector {
291
+ #combined;
292
+ #adaptive;
293
+ #all;
294
+
295
+ constructor(streamingData) {
296
+ this.#combined = (streamingData.formats || []).map((f) => new Format({ ...f, _source: 'combined' }));
297
+ this.#adaptive = (streamingData.adaptiveFormats || []).map((f) => new Format({ ...f, _source: 'adaptive' }));
298
+ this.#all = [...this.#combined, ...this.#adaptive];
299
+ }
300
+
301
+ get formats() {
302
+ return [...this.#all];
303
+ }
304
+
305
+ get combined() {
306
+ return [...this.#combined];
307
+ }
308
+
309
+ get adaptive() {
310
+ return [...this.#adaptive];
311
+ }
312
+
313
+ list(options = {}) {
314
+ let list = [...this.#all];
315
+
316
+ if (options.type === 'video') list = list.filter((f) => f.isVideo);
317
+ else if (options.type === 'audio') list = list.filter((f) => f.isAudio);
318
+ else if (options.type === 'combined') list = list.filter((f) => f.isCombined);
319
+
320
+ if (options.minHeight) list = list.filter((f) => f.height >= options.minHeight);
321
+ if (options.minBitrate) list = list.filter((f) => f.bitrate >= options.minBitrate);
322
+ if (options.container) list = list.filter((f) => f.container === options.container);
323
+ if (options.codec) list = list.filter((f) => f.codec.toLowerCase().includes(options.codec.toLowerCase()));
324
+
325
+ return list.map((f) => f.toJSON());
326
+ }
327
+
328
+ select(options = {}) {
329
+ const quality = validateQuality(options.quality || 'auto');
330
+ const container = options.format || null;
331
+
332
+ if (quality === 'audio') {
333
+ return this.#selectAudio(options);
334
+ }
335
+
336
+ const chain = getFallbackChain(quality);
337
+
338
+ for (const q of chain) {
339
+ const targetH = toHeight(q);
340
+ const result = this.#tryQuality(targetH, container, options);
341
+ if (result) return result;
342
+ }
343
+
344
+ return this.#lastResort(container, options);
345
+ }
346
+
347
+ #tryQuality(targetH, container, options) {
348
+ const preferMp4 = options.preferMp4 !== false;
349
+
350
+ let candidates = this.#combined
351
+ .filter((f) => f.hasUrl && matchQualityRank(f, targetH))
352
+ .sort(this.#sortFn(preferMp4));
353
+
354
+ if (candidates.length) {
355
+ const picked = candidates[0];
356
+ const cc = container ? checkContainer(picked, container) : null;
357
+
358
+ if (container && cc && cc.needsConversion && !options.merge) {
359
+ throw new FormatError(
360
+ `Format "${container}" requires a merge/convert tool for itag ${picked.itag} (${picked.qualityLabel}, ${picked.container}). ` +
361
+ `Available source: ${picked.container}. Use { merge: { tool: 'ffmpeg', output: 'file.${container}' } } to convert.`,
362
+ {
363
+ itag: picked.itag,
364
+ sourceContainer: picked.container,
365
+ targetContainer: container,
366
+ requiresConversion: true,
367
+ suggestedTool: 'ffmpeg',
368
+ }
369
+ );
370
+ }
371
+
372
+ return { format: picked, type: 'combined' };
373
+ }
374
+
375
+ const videos = this.#adaptive
376
+ .filter((f) => f.isVideo && f.hasUrl && matchQualityRank(f, targetH))
377
+ .sort(this.#sortFn(preferMp4));
378
+
379
+ if (videos.length) {
380
+ const video = videos[0];
381
+ const audios = this.#adaptive
382
+ .filter((f) => f.isAudio && f.hasUrl)
383
+ .sort((a, b) => b.bitrate - a.bitrate);
384
+
385
+ const cc = container ? checkContainer(video, container) : null;
386
+
387
+ if (container && cc && cc.needsConversion && !options.merge) {
388
+ throw new FormatError(
389
+ `Format "${container}" requires a merge/convert tool. ` +
390
+ `Source: ${video.container}. Use { merge: { tool: 'ffmpeg', output: 'file.${container}' } }.`,
391
+ {
392
+ itag: video.itag,
393
+ sourceContainer: video.container,
394
+ targetContainer: container,
395
+ requiresConversion: true,
396
+ suggestedTool: 'ffmpeg',
397
+ }
398
+ );
399
+ }
400
+
401
+ const result = { format: video, type: 'video-only' };
402
+
403
+ if (audios.length) {
404
+ result.audio = audios[0];
405
+ result.type = 'separate';
406
+ }
407
+
408
+ return result;
409
+ }
410
+
411
+ return null;
412
+ }
413
+
414
+ #selectAudio(options) {
415
+ const container = options.format || null;
416
+ const audios = this.#all
417
+ .filter((f) => f.isAudio && f.hasUrl)
418
+ .sort((a, b) => b.bitrate - a.bitrate);
419
+
420
+ if (!audios.length) throw new FormatError('No audio formats available');
421
+
422
+ if (container) {
423
+ const match = audios.find((f) => f.container === container);
424
+ if (match) return { format: match, type: 'audio', audio: null };
425
+
426
+ const best = audios[0];
427
+ throw new FormatError(
428
+ `No audio format in "${container}" available. Best available: ${best.container} (${best.qualityLabel}). ` +
429
+ `Use { merge: { tool: 'ffmpeg', output: 'file.${container}' } } to convert.`,
430
+ {
431
+ sourceContainer: best.container,
432
+ targetContainer: container,
433
+ requiresConversion: true,
434
+ suggestedTool: 'ffmpeg',
435
+ }
436
+ );
437
+ }
438
+
439
+ return { format: audios[0], type: 'audio', audio: null };
440
+ }
441
+
442
+ #lastResort(container, options) {
443
+ if (this.#combined.length) {
444
+ return { format: this.#combined[0], type: 'combined' };
445
+ }
446
+ const video = this.#adaptive.find((f) => f.isVideo && f.hasUrl);
447
+ if (video) {
448
+ const audio = this.#adaptive.find((f) => f.isAudio && f.hasUrl);
449
+ return { format: video, type: audio ? 'separate' : 'video-only', audio };
450
+ }
451
+ throw new FormatError('No compatible formats found for this video');
452
+ }
453
+
454
+ #sortFn(preferMp4) {
455
+ return (a, b) => {
456
+ if (preferMp4) {
457
+ const aMp4 = a.container === 'mp4' ? 1 : 0;
458
+ const bMp4 = b.container === 'mp4' ? 1 : 0;
459
+ if (aMp4 !== bMp4) return bMp4 - aMp4;
460
+ }
461
+ return b.qualityRank - a.qualityRank;
462
+ };
463
+ }
464
+ }
465
+
466
+ module.exports = { FormatSelector };
467
+
468
+ },["formats/Format","formats/Qualities","formats/mime","core/Errors"]],
469
+ "formats/Registry":[function(module,exports,__r__){
470
+ const { FormatError } = __r__('core/Errors');
471
+
472
+ const ITAG_REGISTRY = {
473
+ '18': { quality: '360p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
474
+ '22': { quality: '720p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
475
+ '37': { quality: '1080p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
476
+ '38': { quality: '4K', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
477
+ '59': { quality: '480p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
478
+ '78': { quality: '480p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
479
+ '133': { quality: '240p', container: 'mp4', type: 'video', codec: 'H.264' },
480
+ '134': { quality: '360p', container: 'mp4', type: 'video', codec: 'H.264' },
481
+ '135': { quality: '480p', container: 'mp4', type: 'video', codec: 'H.264' },
482
+ '136': { quality: '720p', container: 'mp4', type: 'video', codec: 'H.264' },
483
+ '137': { quality: '1080p', container: 'mp4', type: 'video', codec: 'H.264' },
484
+ '138': { quality: '2160p', container: 'mp4', type: 'video', codec: 'H.264' },
485
+ '139': { quality: '48kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
486
+ '140': { quality: '128kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
487
+ '141': { quality: '256kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
488
+ '160': { quality: '144p', container: 'mp4', type: 'video', codec: 'H.264' },
489
+ '242': { quality: '240p', container: 'webm', type: 'video', codec: 'VP9' },
490
+ '243': { quality: '360p', container: 'webm', type: 'video', codec: 'VP9' },
491
+ '244': { quality: '480p', container: 'webm', type: 'video', codec: 'VP9' },
492
+ '247': { quality: '720p', container: 'webm', type: 'video', codec: 'VP9' },
493
+ '248': { quality: '1080p', container: 'webm', type: 'video', codec: 'VP9' },
494
+ '249': { quality: '50kbps',container: 'webm', type: 'audio', codec: 'Opus' },
495
+ '250': { quality: '70kbps',container: 'webm', type: 'audio', codec: 'Opus' },
496
+ '251': { quality: '160kbps',container: 'webm',type: 'audio', codec: 'Opus' },
497
+ '271': { quality: '1440p', container: 'webm', type: 'video', codec: 'VP9' },
498
+ '272': { quality: '2160p', container: 'webm', type: 'video', codec: 'VP9' },
499
+ '278': { quality: '144p', container: 'webm', type: 'video', codec: 'VP9' },
500
+ '298': { quality: '720p', container: 'mp4', type: 'video', codec: 'H.264' },
501
+ '299': { quality: '1080p', container: 'mp4', type: 'video', codec: 'H.264' },
502
+ '302': { quality: '720p', container: 'webm', type: 'video', codec: 'VP9' },
503
+ '303': { quality: '1080p', container: 'webm', type: 'video', codec: 'VP9' },
504
+ '308': { quality: '1440p', container: 'webm', type: 'video', codec: 'VP9' },
505
+ '313': { quality: '2160p', container: 'webm', type: 'video', codec: 'VP9' },
506
+ '315': { quality: '2160p', container: 'webm', type: 'video', codec: 'VP9' },
507
+ '394': { quality: '144p', container: 'mp4', type: 'video', codec: 'AV1' },
508
+ '395': { quality: '240p', container: 'mp4', type: 'video', codec: 'AV1' },
509
+ '396': { quality: '360p', container: 'mp4', type: 'video', codec: 'AV1' },
510
+ '397': { quality: '480p', container: 'mp4', type: 'video', codec: 'AV1' },
511
+ '398': { quality: '720p', container: 'mp4', type: 'video', codec: 'AV1' },
512
+ '399': { quality: '1080p', container: 'mp4', type: 'video', codec: 'AV1' },
513
+ '400': { quality: '1440p', container: 'mp4', type: 'video', codec: 'AV1' },
514
+ '401': { quality: '2160p', container: 'mp4', type: 'video', codec: 'AV1' },
515
+ '402': { quality: '4320p', container: 'mp4', type: 'video', codec: 'AV1' },
516
+ '571': { quality: '48kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
517
+ '597': { quality: '48kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
518
+ '598': { quality: '144p', container: 'webm', type: 'video', codec: 'VP9' },
519
+ '599': { quality: '32kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
520
+ '600': { quality: '32kbps',container: 'webm', type: 'audio', codec: 'Opus' },
521
+ };
522
+
523
+ const CONTAINER_MAP = {
524
+ mp4: { mime: 'video/mp4', extension: '.mp4', default: true },
525
+ webm: { mime: 'video/webm', extension: '.webm', default: false },
526
+ mkv: { mime: 'video/x-matroska', extension: '.mkv', default: false, requiresMerge: true },
527
+ avi: { mime: 'video/x-msvideo', extension: '.avi', default: false, requiresMerge: true },
528
+ mov: { mime: 'video/quicktime', extension: '.mov', default: false, requiresMerge: true },
529
+ m4a: { mime: 'audio/mp4', extension: '.m4a', default: false },
530
+ aac: { mime: 'audio/aac', extension: '.aac', default: false, requiresMerge: true },
531
+ flac: { mime: 'audio/flac', extension: '.flac', default: false, requiresMerge: true },
532
+ ogg: { mime: 'audio/ogg', extension: '.ogg', default: false, requiresMerge: true },
533
+ mp3: { mime: 'audio/mpeg', extension: '.mp3', default: false, requiresMerge: true },
534
+ wav: { mime: 'audio/wav', extension: '.wav', default: false, requiresMerge: true },
535
+ };
536
+
537
+ const QUALITY_TIERS = ['4320p', '2160p', '1440p', '1080p', '720p', '480p', '360p', '240p', '144p'];
538
+
539
+ function getItagMeta(itag) {
540
+ return ITAG_REGISTRY[String(itag)] || null;
541
+ }
542
+
543
+ function resolveContainer(name) {
544
+ const key = String(name).toLowerCase().replace(/^\./, '');
545
+ return CONTAINER_MAP[key] || null;
546
+ }
547
+
548
+ function requiresConversion(format, targetContainer) {
549
+ const container = resolveContainer(targetContainer);
550
+ if (!container) return false;
551
+ if (container.requiresMerge) return true;
552
+ const fmtContainer = resolveContainer(format.container || 'mp4');
553
+ if (!fmtContainer) return true;
554
+ return fmtContainer.extension !== container.extension;
555
+ }
556
+
557
+ function qualityIndex(label) {
558
+ const idx = QUALITY_TIERS.indexOf(label);
559
+ if (idx !== -1) return idx;
560
+ if (/^\d+p$/.test(label)) return QUALITY_TIERS.indexOf(label) !== -1 ? QUALITY_TIERS.indexOf(label) : -1;
561
+ return -1;
562
+ }
563
+
564
+ function isQualitySupported(label) {
565
+ return QUALITY_TIERS.includes(label) || ['audio', 'best', 'auto'].includes(label);
566
+ }
567
+
568
+ function isContainerSupported(name) {
569
+ return !!resolveContainer(name);
570
+ }
571
+
572
+ module.exports = {
573
+ ITAG_REGISTRY,
574
+ CONTAINER_MAP,
575
+ QUALITY_TIERS,
576
+ getItagMeta,
577
+ resolveContainer,
578
+ requiresConversion,
579
+ qualityIndex,
580
+ isQualitySupported,
581
+ isContainerSupported,
582
+ };
583
+
584
+ },["core/Errors"]],
585
+ "utils/validators":[function(module,exports,__r__){
586
+ const { ValidationError } = __r__('core/Errors');
587
+ const { QUALITY_TIERS } = __r__('formats/Qualities');
588
+ const { CONTAINER_MAP } = __r__('formats/Registry');
589
+
590
+ const VALID_QUALITIES = [...QUALITY_TIERS, 'auto', 'best', 'audio'];
591
+ const VALID_CONTAINERS = Object.keys(CONTAINER_MAP);
592
+
593
+ function validateOptions(options = {}) {
594
+ const errors = [];
595
+
596
+ if (options.quality && !VALID_QUALITIES.includes(String(options.quality).toLowerCase())) {
597
+ errors.push(`Invalid quality "${options.quality}". Valid: ${VALID_QUALITIES.join(', ')}`);
598
+ }
599
+
600
+ if (options.format && !VALID_CONTAINERS.includes(String(options.format).toLowerCase())) {
601
+ errors.push(`Invalid format "${options.format}". Valid: ${VALID_CONTAINERS.join(', ')}`);
602
+ }
603
+
604
+ if (options.filter && !['audioandvideo', 'videoonly', 'audioonly'].includes(options.filter)) {
605
+ errors.push('filter must be "audioandvideo", "videoonly", or "audioonly"');
606
+ }
607
+
608
+ if (options.merge) {
609
+ if (typeof options.merge !== 'object' || Array.isArray(options.merge)) {
610
+ errors.push('merge must be an object with optional { tool, path, output }');
611
+ }
612
+ }
613
+
614
+ if (errors.length) {
615
+ throw new ValidationError(`Invalid options:\n - ${errors.join('\n - ')}`, { errors });
616
+ }
617
+
618
+ return {
619
+ quality: String(options.quality || 'auto').toLowerCase(),
620
+ format: options.format ? String(options.format).toLowerCase() : null,
621
+ filter: options.filter || 'audioandvideo',
622
+ preferMp4: options.preferMp4 !== false,
623
+ merge: options.merge || null,
624
+ concurrency: Math.min(Math.max(parseInt(options.concurrency) || 6, 1), 12),
625
+ onProgress: typeof options.onProgress === 'function' ? options.onProgress : null,
626
+ };
627
+ }
628
+
629
+ module.exports = { validateOptions, VALID_QUALITIES, VALID_CONTAINERS };
630
+
631
+ },["core/Errors","formats/Qualities","formats/Registry"]],
632
+ "utils/url":[function(module,exports,__r__){
633
+ const { URL } = require('node:url');
634
+
635
+ function extractVideoId(input) {
636
+ if (!input) return null;
637
+
638
+ const trimmed = String(input).trim();
639
+
640
+ if (/^[a-zA-Z0-9_-]{11}$/.test(trimmed)) {
641
+ return trimmed;
642
+ }
643
+
644
+ try {
645
+ const u = new URL(trimmed);
646
+ const host = u.hostname.replace('www.', '').replace('m.', '');
647
+
648
+ if (host === 'youtu.be') {
649
+ return u.pathname.replace(/^\//, '').split(/[?&#]/)[0] || null;
650
+ }
651
+
652
+ if (host === 'youtube.com') {
653
+ if (u.pathname.startsWith('/embed/') || u.pathname.startsWith('/shorts/') || u.pathname.startsWith('/live/')) {
654
+ return u.pathname.split('/')[2]?.split(/[?&#]/)[0] || null;
655
+ }
656
+ return u.searchParams.get('v') || null;
657
+ }
658
+ } catch {}
659
+
660
+ return null;
661
+ }
662
+
663
+ function isValidVideoId(id) {
664
+ return /^[a-zA-Z0-9_-]{11}$/.test(id);
665
+ }
666
+
667
+ module.exports = { extractVideoId, isValidVideoId };
668
+
669
+ },[]],
670
+ "download/Merge":[function(module,exports,__r__){
671
+ const fs = require('node:fs');
672
+ const { spawn } = require('node:child_process');
673
+ const { MergeError } = __r__('core/Errors');
674
+
675
+ const TOOLS = {
676
+ ffmpeg: {
677
+ cmd: 'ffmpeg',
678
+ label: 'FFmpeg',
679
+ check: () => {
680
+ return new Promise((resolve) => {
681
+ const proc = spawn('ffmpeg', ['-version'], { stdio: 'ignore' });
682
+ proc.on('error', () => resolve(false));
683
+ proc.on('close', (code) => resolve(code === 0));
684
+ });
685
+ },
686
+ },
687
+ avconv: {
688
+ cmd: 'avconv',
689
+ label: 'avconv (Libav)',
690
+ check: () => {
691
+ return new Promise((resolve) => {
692
+ const proc = spawn('avconv', ['-version'], { stdio: 'ignore' });
693
+ proc.on('error', () => resolve(false));
694
+ proc.on('close', (code) => resolve(code === 0));
695
+ });
696
+ },
697
+ },
698
+ };
699
+
700
+ async function detectTool(name) {
701
+ if (name) {
702
+ const tool = TOOLS[name.toLowerCase()];
703
+ if (!tool) throw new MergeError(`Unknown merge tool "${name}". Supported: ${Object.keys(TOOLS).join(', ')}`);
704
+ const available = await tool.check();
705
+ if (!available) throw new MergeError(`"${tool.label}" not found in PATH. Install it or provide a custom path.`);
706
+ return tool;
707
+ }
708
+
709
+ for (const [key, tool] of Object.entries(TOOLS)) {
710
+ if (await tool.check()) return tool;
711
+ }
712
+
713
+ return null;
714
+ }
715
+
716
+ function buildArgs(tool, videoPath, audioPath, outputPath) {
717
+ const cmd = tool.cmd || 'ffmpeg';
718
+ return {
719
+ cmd,
720
+ args: [
721
+ '-i', videoPath,
722
+ '-i', audioPath,
723
+ '-c:v', 'copy',
724
+ '-c:a', 'aac',
725
+ '-shortest',
726
+ '-y',
727
+ outputPath,
728
+ ],
729
+ };
730
+ }
731
+
732
+ async function merge(videoPath, audioPath, outputPath, options = {}) {
733
+ const toolName = options.tool || null;
734
+ const customPath = options.path || null;
735
+
736
+ let tool;
737
+ if (customPath) {
738
+ tool = { cmd: customPath, label: customPath };
739
+ } else {
740
+ tool = await detectTool(toolName);
741
+ }
742
+
743
+ if (!tool) {
744
+ throw new MergeError(
745
+ 'No merge/conversion tool detected. Install FFmpeg (https://ffmpeg.org) or avconv. ' +
746
+ 'Alternatively, use { merge: { path: "/path/to/ffmpeg" } } to specify a custom binary.'
747
+ );
748
+ }
749
+
750
+ const { cmd, args } = buildArgs(tool, videoPath, audioPath, outputPath);
751
+
752
+ return new Promise((resolve, reject) => {
753
+ const proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
754
+ let stderr = '';
755
+
756
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
757
+ proc.on('close', (code) => {
758
+ if (code === 0) resolve(outputPath);
759
+ else reject(new MergeError(`Merge failed (exit ${code}): ${stderr.slice(-300)}`));
760
+ });
761
+ proc.on('error', (err) => reject(new MergeError(`Failed to start ${cmd}: ${err.message}`)));
762
+ });
763
+ }
764
+
765
+ async function checkAvailable(toolName) {
766
+ const tool = await detectTool(toolName);
767
+ return !!tool;
768
+ }
769
+
770
+ module.exports = {
771
+ merge,
772
+ detectTool,
773
+ checkAvailable,
774
+ TOOLS,
775
+ };
776
+
777
+ },["core/Errors"]],
778
+ "download/Downloader":[function(module,exports,__r__){
779
+ const fs = require('node:fs');
780
+ const https = require('node:https');
781
+ const { URL } = require('node:url');
782
+ const { head } = __r__('core/Client');
783
+ const { NetworkError } = __r__('core/Errors');
784
+
785
+ const CHUNK_SIZE = 10 * 1024 * 1024;
786
+ const MAX_CONCURRENCY = 6;
787
+
788
+ function verify(url) {
789
+ return head(url);
790
+ }
791
+
792
+ function createReadStream(url) {
793
+ return stream(url);
794
+ }
795
+
796
+ async function downloadToFile(url, filePath, options = {}) {
797
+ const concurrency = options.concurrency || MAX_CONCURRENCY;
798
+ const onProgress = options.onProgress || null;
799
+
800
+ const size = await getContentLength(url);
801
+
802
+ if (!size || size < CHUNK_SIZE) {
803
+ return simpleDownload(url, filePath, onProgress);
804
+ }
805
+
806
+ return parallelDownload(url, filePath, size, concurrency, onProgress);
807
+ }
808
+
809
+ function getContentLength(url) {
810
+ return new Promise((resolve) => {
811
+ let u;
812
+ try { u = new URL(url); } catch { resolve(0); return; }
813
+ const req = https.get({
814
+ hostname: u.hostname,
815
+ path: u.pathname + u.search,
816
+ headers: {
817
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
818
+ 'Referer': 'https://www.youtube.com/',
819
+ 'Range': 'bytes=0-0',
820
+ },
821
+ }, (res) => {
822
+ const cr = res.headers['content-range'];
823
+ if (cr) {
824
+ const m = cr.match(/\/(\d+)/);
825
+ if (m) { resolve(parseInt(m[1], 10)); res.resume(); return; }
826
+ }
827
+ resolve(parseInt(res.headers['content-length'] || '0', 10));
828
+ res.resume();
829
+ });
830
+ req.on('error', () => resolve(0));
831
+ req.setTimeout(10000, () => { req.destroy(); resolve(0); });
832
+ });
833
+ }
834
+
835
+ function simpleDownload(url, filePath, onProgress, redirects = 0) {
836
+ return new Promise((resolve, reject) => {
837
+ if (redirects > 5) return reject(new NetworkError('Too many redirects'));
838
+ let u;
839
+ try { u = new URL(url); } catch { return reject(new NetworkError('Invalid URL')); }
840
+ const req = https.get({
841
+ hostname: u.hostname,
842
+ path: u.pathname + u.search,
843
+ headers: {
844
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
845
+ 'Referer': 'https://www.youtube.com/',
846
+ },
847
+ }, (res) => {
848
+ if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
849
+ res.resume();
850
+ return resolve(simpleDownload(res.headers.location, filePath, onProgress, redirects + 1));
851
+ }
852
+ if (res.statusCode !== 200) {
853
+ return reject(new NetworkError(`Download failed with HTTP ${res.statusCode}`));
854
+ }
855
+ const total = parseInt(res.headers['content-length'] || '0', 10);
856
+ let downloaded = 0;
857
+ const out = fs.createWriteStream(filePath);
858
+
859
+ res.on('data', (chunk) => {
860
+ downloaded += chunk.length;
861
+ if (onProgress && total) onProgress(downloaded, total);
862
+ out.write(chunk);
863
+ });
864
+ res.on('end', () => { out.end(); });
865
+ out.on('finish', () => resolve(filePath));
866
+ out.on('error', reject);
867
+ });
868
+ req.on('error', reject);
869
+ req.setTimeout(300000, () => { req.destroy(new Error('Download timed out')); });
870
+ });
871
+ }
872
+
873
+ async function parallelDownload(url, filePath, totalSize, concurrency, onProgress) {
874
+ const count = Math.min(Math.min(concurrency, MAX_CONCURRENCY), Math.ceil(totalSize / (1024 * 1024)));
875
+ const actualCount = Math.max(1, count);
876
+ const chunkSize = Math.ceil(totalSize / actualCount);
877
+ const ranges = Array.from({ length: actualCount }, (_, i) => ({
878
+ start: i * chunkSize,
879
+ end: i === actualCount - 1 ? totalSize - 1 : (i + 1) * chunkSize - 1,
880
+ }));
881
+
882
+ try {
883
+ const buffers = await Promise.all(
884
+ ranges.map((r) => downloadChunk(url, r.start, r.end))
885
+ );
886
+ if (onProgress) onProgress(totalSize, totalSize);
887
+ const full = Buffer.concat(buffers);
888
+ fs.writeFileSync(filePath, full);
889
+ return filePath;
890
+ } catch (err) {
891
+ throw new NetworkError('Parallel download failed: ' + err.message);
892
+ }
893
+ }
894
+
895
+ function downloadChunk(url, start, end, redirects = 0) {
896
+ return new Promise((resolve, reject) => {
897
+ if (redirects > 5) return reject(new Error('Too many redirects'));
898
+ let u;
899
+ try { u = new URL(url); } catch { return reject(new Error('Invalid URL')); }
900
+ const req = https.get({
901
+ hostname: u.hostname,
902
+ path: u.pathname + u.search,
903
+ headers: {
904
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
905
+ 'Referer': 'https://www.youtube.com/',
906
+ 'Range': `bytes=${start}-${end}`,
907
+ },
908
+ }, (res) => {
909
+ if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
910
+ res.resume();
911
+ return resolve(downloadChunk(res.headers.location, start, end, redirects + 1));
912
+ }
913
+ if (res.statusCode !== 200 && res.statusCode !== 206) {
914
+ return reject(new Error(`Chunk HTTP ${res.statusCode}`));
915
+ }
916
+ const chunks = [];
917
+ res.on('data', (c) => chunks.push(c));
918
+ res.on('end', () => resolve(Buffer.concat(chunks)));
919
+ });
920
+ req.on('error', reject);
921
+ req.setTimeout(120000, () => { req.destroy(new Error('Chunk timed out')); });
922
+ });
923
+ }
924
+
925
+ module.exports = {
926
+ verify,
927
+ createReadStream,
928
+ downloadToFile,
929
+ getContentLength,
930
+ };
931
+
932
+ },["core/Client","core/Errors"]],
933
+ "download/Stream":[function(module,exports,__r__){
934
+ const { Transform } = require('node:stream');
935
+ const { createReadStream } = __r__('download/Downloader');
936
+
937
+ class DownloadStream extends Transform {
938
+ #url;
939
+ #bytesRead;
940
+ #startTime;
941
+ #onProgress;
942
+
943
+ constructor(url, options = {}) {
944
+ super({ highWaterMark: 1024 * 1024 });
945
+ this.#url = url;
946
+ this.#bytesRead = 0;
947
+ this.#startTime = Date.now();
948
+ this.#onProgress = options.onProgress || null;
949
+ }
950
+
951
+ _transform(chunk, encoding, callback) {
952
+ this.#bytesRead += chunk.length;
953
+ if (this.#onProgress) {
954
+ this.#onProgress({
955
+ bytes: this.#bytesRead,
956
+ elapsed: Date.now() - this.#startTime,
957
+ });
958
+ }
959
+ this.push(chunk);
960
+ callback();
961
+ }
962
+
963
+ _flush(callback) {
964
+ callback();
965
+ }
966
+
967
+ get bytesRead() {
968
+ return this.#bytesRead;
969
+ }
970
+
971
+ get elapsed() {
972
+ return Date.now() - this.#startTime;
973
+ }
974
+ }
975
+
976
+ function createStream(url, options = {}) {
977
+ const transform = new DownloadStream(url, options);
978
+ const source = createReadStream(url);
979
+
980
+ source.on('error', (err) => transform.destroy(err));
981
+ source.pipe(transform);
982
+
983
+ return transform;
984
+ }
985
+
986
+ module.exports = { DownloadStream, createStream };
987
+
988
+ },["download/Downloader"]],
989
+ "utils/constants":[function(module,exports,__r__){
990
+ const pkg = {"name":"yt-direct","version":"1.0.1","description":"Hello, I present to you a module to download YouTube videos directly","main":"dist/index.js","types":"dist/index.d.ts","files":["dist","README.md","LICENSE"],"scripts":{"build":"node build/build.js","prepublishOnly":"npm run build","test":"node test/basic.js","example":"node examples/basic.js"},"keywords":["youtube","download","video","yt-dlp","innertube","downloader","ytdl","mp4","stream","no-dependencies"],"license":"MIT","engines":{"node":">=18.0.0"},"repository":{"type":"git","url":"https://github.com/SoyMaycol/yt-direct.git"},"bugs":{"url":"https://github.com/SoyMaycol/yt-direct/issues"},"homepage":"https://github.com/SoyMaycol/yt-direct#readme","author":"SoyMaycol"};
991
+
992
+ module.exports = {
993
+ VERSION: pkg.version,
994
+ NAME: pkg.name,
995
+ HOMEPAGE: pkg.homepage,
996
+ MAX_CONCURRENCY: 6,
997
+ DEFAULT_TIMEOUT: 20000,
998
+ DOWNLOAD_TIMEOUT: 300000,
999
+ CHUNK_SIZE: 10 * 1024 * 1024,
1000
+ YOUTUBE_HOST: 'www.youtube.com',
1001
+ INNERTUBE_KEY: 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
1002
+ SUPPORTED_PROTOCOLS: ['http:', 'https:'],
1003
+ INTEGRITY_SEED: 'yt-direct-v1',
1004
+ };
1005
+
1006
+ },[]],
1007
+ "core/Client":[function(module,exports,__r__){
1008
+ const https = require('node:https');
1009
+ const zlib = require('node:zlib');
1010
+ const { URL } = require('node:url');
1011
+
1012
+ const UA = 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip';
1013
+ const API_KEY = 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w';
1014
+ const HOST = 'www.youtube.com';
1015
+ const PATH = '/youtubei/v1/player';
1016
+ const TIMEOUT = 20000;
1017
+
1018
+ function request(body) {
1019
+ return new Promise((resolve, reject) => {
1020
+ const opts = {
1021
+ hostname: HOST,
1022
+ path: `${PATH}?key=${API_KEY}`,
1023
+ method: 'POST',
1024
+ headers: {
1025
+ 'Content-Type': 'application/json',
1026
+ 'User-Agent': UA,
1027
+ 'Content-Length': Buffer.byteLength(body),
1028
+ 'Accept-Encoding': 'gzip',
1029
+ 'Origin': `https://${HOST}`,
1030
+ },
1031
+ };
1032
+ const req = https.request(opts, (res) => {
1033
+ const chunks = [];
1034
+ res.on('data', (c) => chunks.push(c));
1035
+ res.on('end', () => {
1036
+ let buf = Buffer.concat(chunks);
1037
+ if (res.headers['content-encoding'] === 'gzip') {
1038
+ try { buf = zlib.gunzipSync(buf); } catch {}
1039
+ }
1040
+ if (res.statusCode !== 200) {
1041
+ return reject(new Error(`YouTube API returned HTTP ${res.statusCode}`));
1042
+ }
1043
+ try {
1044
+ resolve(JSON.parse(buf.toString('utf8')));
1045
+ } catch (e) {
1046
+ reject(new Error('Invalid JSON from YouTube API: ' + e.message));
1047
+ }
1048
+ });
1049
+ });
1050
+ req.on('error', reject);
1051
+ req.setTimeout(TIMEOUT, () => { req.destroy(new Error('YouTube API request timed out')); });
1052
+ req.write(body);
1053
+ req.end();
1054
+ });
1055
+ }
1056
+
1057
+ function head(url) {
1058
+ return new Promise((resolve) => {
1059
+ let u;
1060
+ try { u = new URL(url); } catch { resolve(false); return; }
1061
+ const req = https.get({
1062
+ hostname: u.hostname,
1063
+ path: u.pathname + u.search,
1064
+ headers: {
1065
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
1066
+ 'Referer': 'https://www.youtube.com/',
1067
+ 'Range': 'bytes=0-0',
1068
+ },
1069
+ }, (res) => {
1070
+ resolve(res.statusCode === 206 || res.statusCode === 200);
1071
+ res.resume();
1072
+ });
1073
+ req.on('error', () => resolve(false));
1074
+ req.setTimeout(8000, () => { req.destroy(); resolve(false); });
1075
+ });
1076
+ }
1077
+
1078
+ function stream(url) {
1079
+ const u = new URL(url);
1080
+ const req = https.get({
1081
+ hostname: u.hostname,
1082
+ path: u.pathname + u.search,
1083
+ headers: {
1084
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
1085
+ 'Referer': 'https://www.youtube.com/',
1086
+ },
1087
+ });
1088
+ req.setTimeout(30000);
1089
+ return req;
1090
+ }
1091
+
1092
+ module.exports = { request, head, stream };
1093
+
1094
+ },[]],
1095
+ "formats/Format":[function(module,exports,__r__){
1096
+ const { getItagMeta } = __r__('formats/Registry');
1097
+
1098
+ class Format {
1099
+ #raw;
1100
+ #meta;
1101
+
1102
+ constructor(rawFormat) {
1103
+ this.#raw = rawFormat;
1104
+ this.#meta = getItagMeta(rawFormat.itag) || {};
1105
+
1106
+ this.itag = rawFormat.itag;
1107
+ this.url = rawFormat.url || null;
1108
+ this.mimeType = rawFormat.mimeType || '';
1109
+ this.contentLength = rawFormat.contentLength ? Number(rawFormat.contentLength) : 0;
1110
+ this.bitrate = rawFormat.bitrate || 0;
1111
+ this.width = rawFormat.width || 0;
1112
+ this.height = rawFormat.height || 0;
1113
+ this.fps = rawFormat.fps || 0;
1114
+ this.qualityLabel = rawFormat.qualityLabel || this.#meta.quality || null;
1115
+ this.container = rawFormat.container || this.#meta.container || 'mp4';
1116
+ this.codec = rawFormat.codecs || this.#meta.codec || 'unknown';
1117
+ this.isAudio = this.mimeType.includes('audio');
1118
+ this.isVideo = this.mimeType.includes('video');
1119
+ this.isCombined = this.#meta.type === 'combined' || (this.isVideo && this.mimeType.includes('mp4a'));
1120
+ this.source = rawFormat._source || 'adaptive';
1121
+ }
1122
+
1123
+ get hasUrl() {
1124
+ return !!this.url;
1125
+ }
1126
+
1127
+ get sizeMB() {
1128
+ return this.contentLength > 0 ? (this.contentLength / 1024 / 1024).toFixed(1) : '?';
1129
+ }
1130
+
1131
+ get qualityRank() {
1132
+ if (this.isAudio) return this.bitrate;
1133
+ return (this.height || 0) * (this.width || 0);
1134
+ }
1135
+
1136
+ toJSON() {
1137
+ return {
1138
+ itag: this.itag,
1139
+ quality: this.qualityLabel,
1140
+ container: this.container,
1141
+ codec: this.codec,
1142
+ size: this.sizeMB,
1143
+ width: this.width,
1144
+ height: this.height,
1145
+ fps: this.fps,
1146
+ bitrate: this.bitrate,
1147
+ type: this.isCombined ? 'combined' : this.isAudio ? 'audio' : 'video',
1148
+ hasUrl: this.hasUrl,
1149
+ };
1150
+ }
1151
+
1152
+ inspect() {
1153
+ return `Format(${this.itag} | ${this.qualityLabel} | ${this.container} | ${this.codec})`;
1154
+ }
1155
+
1156
+ [Symbol.for('nodejs.util.inspect.custom')]() {
1157
+ return this.inspect();
1158
+ }
1159
+ }
1160
+
1161
+ module.exports = { Format };
1162
+
1163
+ },["formats/Registry"]],
1164
+ "formats/Qualities":[function(module,exports,__r__){
1165
+ const { QualityError } = __r__('core/Errors');
1166
+
1167
+ const QUALITY_MAP = {
1168
+ '4320p': 4320,
1169
+ '2160p': 2160,
1170
+ '1440p': 1440,
1171
+ '1080p': 1080,
1172
+ '720p': 720,
1173
+ '480p': 480,
1174
+ '360p': 360,
1175
+ '240p': 240,
1176
+ '144p': 144,
1177
+ };
1178
+
1179
+ const QUALITY_TIERS = Object.keys(QUALITY_MAP);
1180
+
1181
+ function toHeight(label) {
1182
+ if (!label) return 0;
1183
+ const cleaned = String(label).toLowerCase().replace(/[^0-9]/g, '');
1184
+ const num = parseInt(cleaned, 10);
1185
+ return isNaN(num) ? 0 : num;
1186
+ }
1187
+
1188
+ function matchQualityRank(format, targetHeight, tolerance = 72) {
1189
+ if (!targetHeight) return true;
1190
+ const h = format.height || toHeight(format.qualityLabel);
1191
+ if (!h) return false;
1192
+ return Math.abs(h - targetHeight) <= tolerance;
1193
+ }
1194
+
1195
+ function getFallbackChain(requested) {
1196
+ const t = String(requested || '').toLowerCase();
1197
+ if (t === 'auto' || t === 'best') return [...QUALITY_TIERS];
1198
+ if (t === 'audio') return ['audio'];
1199
+ const idx = QUALITY_TIERS.indexOf(t);
1200
+ if (idx !== -1) return QUALITY_TIERS.slice(idx);
1201
+ return [...QUALITY_TIERS];
1202
+ }
1203
+
1204
+ function validateQuality(requested) {
1205
+ if (!requested) return 'auto';
1206
+ const t = String(requested).toLowerCase();
1207
+ if (t === 'auto' || t === 'best' || t === 'audio') return t;
1208
+ if (QUALITY_TIERS.includes(t)) return t;
1209
+ throw new QualityError(
1210
+ `Unsupported quality "${requested}". Available: ${QUALITY_TIERS.join(', ')}, auto, best, audio`,
1211
+ { requested, supported: [...QUALITY_TIERS, 'auto', 'best', 'audio'] }
1212
+ );
1213
+ }
1214
+
1215
+ module.exports = {
1216
+ QUALITY_MAP,
1217
+ QUALITY_TIERS,
1218
+ toHeight,
1219
+ matchQualityRank,
1220
+ getFallbackChain,
1221
+ validateQuality,
1222
+ };
1223
+
1224
+ },["core/Errors"]],
1225
+ "formats/mime":[function(module,exports,__r__){
1226
+ const { resolveContainer, requiresConversion } = __r__('formats/Registry');
1227
+
1228
+ function mimeTypeToContainer(mimeType) {
1229
+ if (!mimeType) return 'mp4';
1230
+ const parts = mimeType.split('/');
1231
+ if (parts.length < 2) return 'mp4';
1232
+ const sub = parts[1].split(';')[0].trim().toLowerCase();
1233
+ const map = {
1234
+ mp4: 'mp4',
1235
+ webm: 'webm',
1236
+ 'x-matroska': 'mkv',
1237
+ 'x-msvideo': 'avi',
1238
+ quicktime: 'mov',
1239
+ aac: 'aac',
1240
+ mpeg: 'mp3',
1241
+ wav: 'wav',
1242
+ ogg: 'ogg',
1243
+ flac: 'flac',
1244
+ '3gpp': '3gp',
1245
+ };
1246
+ return map[sub] || sub;
1247
+ }
1248
+
1249
+ function checkContainer(format, targetContainer) {
1250
+ const current = format.container || mimeTypeToContainer(format.mimeType);
1251
+ if (!targetContainer) return { compatible: true, current, needsConversion: false };
1252
+ const tc = resolveContainer(targetContainer);
1253
+ if (!tc) return { compatible: false, current, target: targetContainer, needsConversion: false };
1254
+ if (current === targetContainer) return { compatible: true, current, target: targetContainer, needsConversion: false };
1255
+ return {
1256
+ compatible: false,
1257
+ current,
1258
+ target: targetContainer,
1259
+ needsConversion: true,
1260
+ requiresTool: requiresConversion(format, targetContainer),
1261
+ };
1262
+ }
1263
+
1264
+ module.exports = { mimeTypeToContainer, checkContainer };
1265
+
1266
+ },["formats/Registry"]],
1267
+ };
1268
+
1269
+ var __c={};
1270
+ function __r(id){
1271
+ if(__c[id])return __c[id].exports;
1272
+ var f=__m[id][0],d=__m[id][1],m={exports:{}};
1273
+ __c[id]=m;
1274
+ f(m,m.exports,function(q){for(var i=0;i<d.length;i++)if(d[i]===q)return __r(d[i]);throw new Error('Mod not found: '+q+' from '+id)});
1275
+ return m.exports;
1276
+ }
1277
+
1278
+ const __ytdl=__r("index");
1279
+ })();
1280
+
1281
+ export default __ytdl;