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.js CHANGED
@@ -1,165 +1,134 @@
1
- /*! yt-direct v1.0.0 */
2
- // index.js
3
- // core/InnerTube.js
4
- // core/Client.js
5
- const https = require('node:https');
6
- const zlib = require('node:zlib');
7
- const { URL } = require('node:url');
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');
8
18
 
9
- const UA = 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip';
10
- const API_KEY = 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w';
11
- const HOST = 'www.youtube.com';
12
- const PATH = '/youtubei/v1/player';
13
- const TIMEOUT = 20000;
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
+ }
14
24
 
15
- function buildOptions(videoId) {
16
- return JSON.stringify({
17
- context: {
18
- client: {
19
- clientName: 'ANDROID',
20
- clientVersion: '20.10.38',
21
- androidSdkVersion: 30,
22
- osName: 'Android',
23
- osVersion: '11',
24
- userAgent: UA,
25
- hl: 'en',
26
- gl: 'US',
27
- },
28
- },
29
- videoId,
30
- contentCheckOk: true,
31
- racyCheckOk: true,
32
- });
33
- }
25
+ const normalized = validateOptions(options);
34
26
 
35
- function request(payload) {
36
- return new Promise((resolve, reject) => {
37
- const body = typeof payload === 'string' ? payload : buildOptions(payload);
38
- const opts = {
39
- hostname: HOST,
40
- path: `${PATH}?key=${API_KEY}`,
41
- method: 'POST',
42
- headers: {
43
- 'Content-Type': 'application/json',
44
- 'User-Agent': UA,
45
- 'Content-Length': Buffer.byteLength(body),
46
- 'Accept-Encoding': 'gzip',
47
- 'Origin': `https://${HOST}`,
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
+ });
48
53
  },
49
54
  };
50
- const req = https.request(opts, (res) => {
51
- const chunks = [];
52
- res.on('data', (c) => chunks.push(c));
53
- res.on('end', () => {
54
- let buf = Buffer.concat(chunks);
55
- if (res.headers['content-encoding'] === 'gzip') {
56
- try { buf = zlib.gunzipSync(buf); } catch {}
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}`;
57
61
  }
58
- if (res.statusCode !== 200) {
59
- return reject(new Error(`YouTube API returned HTTP ${res.statusCode}`));
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 {}
60
75
  }
61
- resolve(JSON.parse(buf.toString('utf8')));
62
- });
63
- });
64
- req.on('error', reject);
65
- req.setTimeout(TIMEOUT, () => { req.destroy(new Error('YouTube API request timed out')); });
66
- req.write(body);
67
- req.end();
68
- });
69
- }
70
-
71
- function head(url) {
72
- return new Promise((resolve) => {
73
- let u;
74
- try { u = new URL(url); } catch { resolve(false); return; }
75
- const req = https.get({
76
- hostname: u.hostname,
77
- path: u.pathname + u.search,
78
- headers: {
79
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
80
- 'Referer': 'https://www.youtube.com/',
81
- 'Range': 'bytes=0-0',
82
- },
83
- }, (res) => {
84
- resolve(res.statusCode === 206 || res.statusCode === 200);
85
- res.resume();
86
- });
87
- req.on('error', () => resolve(false));
88
- req.setTimeout(8000, () => { req.destroy(); resolve(false); });
89
- });
90
- }
91
-
92
- function stream(url) {
93
- const u = new URL(url);
94
- return https.get({
95
- hostname: u.hostname,
96
- path: u.pathname + u.search,
97
- headers: {
98
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
99
- 'Referer': 'https://www.youtube.com/',
100
- },
101
- });
102
- }
103
-
104
- module.exports = { request, head, stream, buildOptions, UA, API_KEY, HOST, PATH };
76
+ };
77
+ }
105
78
 
106
- // core/Errors.js
107
- class YouTubeError extends Error {
108
- constructor(message, code = 'YOUTUBE_ERROR', details = null) {
109
- super(message);
110
- this.name = 'YouTubeError';
111
- this.code = code;
112
- this.details = details;
113
- Error.captureStackTrace(this, this.constructor);
114
- }
79
+ return response;
80
+ })();
115
81
  }
116
82
 
117
- class FormatError extends YouTubeError {
118
- constructor(message, details = null) {
119
- super(message, 'FORMAT_ERROR', details);
120
- this.name = 'FormatError';
121
- }
122
- }
83
+ async function getInfo(input) {
84
+ const videoId = extractVideoId(input);
85
+ if (!videoId) throw new ValidationError(`Invalid YouTube URL or video ID: "${input}"`);
123
86
 
124
- class QualityError extends YouTubeError {
125
- constructor(message, details = null) {
126
- super(message, 'QUALITY_ERROR', details);
127
- this.name = 'QualityError';
128
- }
129
- }
87
+ const result = await fetch(videoId);
88
+ const selector = new FormatSelector(result.streamingData);
130
89
 
131
- class MergeError extends YouTubeError {
132
- constructor(message, details = null) {
133
- super(message, 'MERGE_ERROR', details);
134
- this.name = 'MergeError';
135
- }
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
+ };
136
105
  }
137
106
 
138
- class NetworkError extends YouTubeError {
139
- constructor(message, details = null) {
140
- super(message, 'NETWORK_ERROR', details);
141
- this.name = 'NetworkError';
142
- }
107
+ function sanitize(name) {
108
+ return String(name || 'video').replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, ' ').trim() || 'video';
143
109
  }
144
110
 
145
- class ValidationError extends YouTubeError {
146
- constructor(message, details = null) {
147
- super(message, 'VALIDATION_ERROR', details);
148
- this.name = 'ValidationError';
149
- }
150
- }
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;
151
124
 
152
- module.exports = {
153
- YouTubeError,
154
- FormatError,
155
- QualityError,
156
- MergeError,
157
- NetworkError,
158
- ValidationError,
159
- };
125
+ module.exports = ytdl;
126
+ module.exports.default = ytdl;
160
127
 
161
- const { request } = require('./Client');
162
- const { YouTubeError } = require('./Errors');
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');
163
132
 
164
133
  const CLIENT_PROFILES = [
165
134
  {
@@ -255,296 +224,68 @@ async function fetch(videoId) {
255
224
 
256
225
  module.exports = { fetch, CLIENT_PROFILES };
257
226
 
258
- // formats/Selector.js
259
- // formats/Format.js
260
- // formats/Registry.js
261
- const { FormatError } = require('../core/Errors');
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
+ }
262
238
 
263
- const ITAG_REGISTRY = {
264
- '18': { quality: '360p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
265
- '22': { quality: '720p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
266
- '37': { quality: '1080p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
267
- '38': { quality: '4K', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
268
- '59': { quality: '480p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
269
- '78': { quality: '480p', container: 'mp4', type: 'combined', codec: 'H.264 + AAC' },
270
- '133': { quality: '240p', container: 'mp4', type: 'video', codec: 'H.264' },
271
- '134': { quality: '360p', container: 'mp4', type: 'video', codec: 'H.264' },
272
- '135': { quality: '480p', container: 'mp4', type: 'video', codec: 'H.264' },
273
- '136': { quality: '720p', container: 'mp4', type: 'video', codec: 'H.264' },
274
- '137': { quality: '1080p', container: 'mp4', type: 'video', codec: 'H.264' },
275
- '138': { quality: '2160p', container: 'mp4', type: 'video', codec: 'H.264' },
276
- '139': { quality: '48kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
277
- '140': { quality: '128kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
278
- '141': { quality: '256kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
279
- '160': { quality: '144p', container: 'mp4', type: 'video', codec: 'H.264' },
280
- '242': { quality: '240p', container: 'webm', type: 'video', codec: 'VP9' },
281
- '243': { quality: '360p', container: 'webm', type: 'video', codec: 'VP9' },
282
- '244': { quality: '480p', container: 'webm', type: 'video', codec: 'VP9' },
283
- '247': { quality: '720p', container: 'webm', type: 'video', codec: 'VP9' },
284
- '248': { quality: '1080p', container: 'webm', type: 'video', codec: 'VP9' },
285
- '249': { quality: '50kbps',container: 'webm', type: 'audio', codec: 'Opus' },
286
- '250': { quality: '70kbps',container: 'webm', type: 'audio', codec: 'Opus' },
287
- '251': { quality: '160kbps',container: 'webm',type: 'audio', codec: 'Opus' },
288
- '271': { quality: '1440p', container: 'webm', type: 'video', codec: 'VP9' },
289
- '272': { quality: '2160p', container: 'webm', type: 'video', codec: 'VP9' },
290
- '278': { quality: '144p', container: 'webm', type: 'video', codec: 'VP9' },
291
- '298': { quality: '720p', container: 'mp4', type: 'video', codec: 'H.264' },
292
- '299': { quality: '1080p', container: 'mp4', type: 'video', codec: 'H.264' },
293
- '302': { quality: '720p', container: 'webm', type: 'video', codec: 'VP9' },
294
- '303': { quality: '1080p', container: 'webm', type: 'video', codec: 'VP9' },
295
- '308': { quality: '1440p', container: 'webm', type: 'video', codec: 'VP9' },
296
- '313': { quality: '2160p', container: 'webm', type: 'video', codec: 'VP9' },
297
- '315': { quality: '2160p', container: 'webm', type: 'video', codec: 'VP9' },
298
- '394': { quality: '144p', container: 'mp4', type: 'video', codec: 'AV1' },
299
- '395': { quality: '240p', container: 'mp4', type: 'video', codec: 'AV1' },
300
- '396': { quality: '360p', container: 'mp4', type: 'video', codec: 'AV1' },
301
- '397': { quality: '480p', container: 'mp4', type: 'video', codec: 'AV1' },
302
- '398': { quality: '720p', container: 'mp4', type: 'video', codec: 'AV1' },
303
- '399': { quality: '1080p', container: 'mp4', type: 'video', codec: 'AV1' },
304
- '400': { quality: '1440p', container: 'mp4', type: 'video', codec: 'AV1' },
305
- '401': { quality: '2160p', container: 'mp4', type: 'video', codec: 'AV1' },
306
- '402': { quality: '4320p', container: 'mp4', type: 'video', codec: 'AV1' },
307
- '571': { quality: '48kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
308
- '597': { quality: '48kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
309
- '598': { quality: '144p', container: 'webm', type: 'video', codec: 'VP9' },
310
- '599': { quality: '32kbps',container: 'mp4', type: 'audio', codec: 'AAC' },
311
- '600': { quality: '32kbps',container: 'webm', type: 'audio', codec: 'Opus' },
312
- };
313
-
314
- const CONTAINER_MAP = {
315
- mp4: { mime: 'video/mp4', extension: '.mp4', default: true },
316
- webm: { mime: 'video/webm', extension: '.webm', default: false },
317
- mkv: { mime: 'video/x-matroska', extension: '.mkv', default: false, requiresMerge: true },
318
- avi: { mime: 'video/x-msvideo', extension: '.avi', default: false, requiresMerge: true },
319
- mov: { mime: 'video/quicktime', extension: '.mov', default: false, requiresMerge: true },
320
- m4a: { mime: 'audio/mp4', extension: '.m4a', default: false },
321
- aac: { mime: 'audio/aac', extension: '.aac', default: false, requiresMerge: true },
322
- flac: { mime: 'audio/flac', extension: '.flac', default: false, requiresMerge: true },
323
- ogg: { mime: 'audio/ogg', extension: '.ogg', default: false, requiresMerge: true },
324
- mp3: { mime: 'audio/mpeg', extension: '.mp3', default: false, requiresMerge: true },
325
- wav: { mime: 'audio/wav', extension: '.wav', default: false, requiresMerge: true },
326
- };
327
-
328
- const QUALITY_TIERS = ['4320p', '2160p', '1440p', '1080p', '720p', '480p', '360p', '240p', '144p'];
329
-
330
- function getItagMeta(itag) {
331
- return ITAG_REGISTRY[String(itag)] || null;
332
- }
333
-
334
- function resolveContainer(name) {
335
- const key = String(name).toLowerCase().replace(/^\./, '');
336
- return CONTAINER_MAP[key] || null;
337
- }
338
-
339
- function requiresConversion(format, targetContainer) {
340
- const container = resolveContainer(targetContainer);
341
- if (!container) return false;
342
- if (container.requiresMerge) return true;
343
- const fmtContainer = resolveContainer(format.container || 'mp4');
344
- if (!fmtContainer) return true;
345
- return fmtContainer.extension !== container.extension;
346
- }
347
-
348
- function qualityIndex(label) {
349
- const idx = QUALITY_TIERS.indexOf(label);
350
- if (idx !== -1) return idx;
351
- if (/^\d+p$/.test(label)) return QUALITY_TIERS.indexOf(label) !== -1 ? QUALITY_TIERS.indexOf(label) : -1;
352
- return -1;
353
- }
354
-
355
- function isQualitySupported(label) {
356
- return QUALITY_TIERS.includes(label) || ['audio', 'best', 'auto'].includes(label);
357
- }
358
-
359
- function isContainerSupported(name) {
360
- return !!resolveContainer(name);
361
- }
362
-
363
- module.exports = {
364
- ITAG_REGISTRY,
365
- CONTAINER_MAP,
366
- QUALITY_TIERS,
367
- getItagMeta,
368
- resolveContainer,
369
- requiresConversion,
370
- qualityIndex,
371
- isQualitySupported,
372
- isContainerSupported,
373
- };
374
-
375
- const { getItagMeta, resolveContainer } = require('./Registry');
376
-
377
- class Format {
378
- #raw;
379
- #meta;
380
-
381
- constructor(rawFormat) {
382
- this.#raw = rawFormat;
383
- this.#meta = getItagMeta(rawFormat.itag) || {};
384
-
385
- this.itag = rawFormat.itag;
386
- this.url = rawFormat.url || null;
387
- this.mimeType = rawFormat.mimeType || '';
388
- this.contentLength = rawFormat.contentLength ? Number(rawFormat.contentLength) : 0;
389
- this.bitrate = rawFormat.bitrate || 0;
390
- this.width = rawFormat.width || 0;
391
- this.height = rawFormat.height || 0;
392
- this.fps = rawFormat.fps || 0;
393
- this.qualityLabel = rawFormat.qualityLabel || this.#meta.quality || null;
394
- this.container = rawFormat.container || this.#meta.container || 'mp4';
395
- this.codec = rawFormat.codecs || this.#meta.codec || 'unknown';
396
- this.isAudio = this.mimeType.includes('audio');
397
- this.isVideo = this.mimeType.includes('video');
398
- this.isCombined = this.#meta.type === 'combined' || (this.isVideo && this.mimeType.includes('mp4a'));
399
- this.source = rawFormat._source || 'adaptive';
400
- }
401
-
402
- get hasUrl() {
403
- return !!this.url;
404
- }
405
-
406
- get sizeMB() {
407
- return this.contentLength > 0 ? (this.contentLength / 1024 / 1024).toFixed(1) : '?';
408
- }
409
-
410
- get qualityRank() {
411
- if (this.isAudio) return this.bitrate;
412
- return (this.height || 0) * (this.width || 0);
413
- }
414
-
415
- toJSON() {
416
- return {
417
- itag: this.itag,
418
- quality: this.qualityLabel,
419
- container: this.container,
420
- codec: this.codec,
421
- size: this.sizeMB,
422
- width: this.width,
423
- height: this.height,
424
- fps: this.fps,
425
- bitrate: this.bitrate,
426
- type: this.isCombined ? 'combined' : this.isAudio ? 'audio' : 'video',
427
- hasUrl: this.hasUrl,
428
- };
429
- }
430
-
431
- inspect() {
432
- return `Format(${this.itag} | ${this.qualityLabel} | ${this.container} | ${this.codec})`;
433
- }
434
-
435
- [Symbol.for('nodejs.util.inspect.custom')]() {
436
- return this.inspect();
239
+ class FormatError extends YouTubeError {
240
+ constructor(message, details = null) {
241
+ super(message, 'FORMAT_ERROR', details);
242
+ this.name = 'FormatError';
437
243
  }
438
244
  }
439
245
 
440
- module.exports = { Format };
441
-
442
- // formats/Qualities.js
443
- const { QualityError } = require('../core/Errors');
444
-
445
- const QUALITY_MAP = {
446
- '4320p': 4320,
447
- '2160p': 2160,
448
- '1440p': 1440,
449
- '1080p': 1080,
450
- '720p': 720,
451
- '480p': 480,
452
- '360p': 360,
453
- '240p': 240,
454
- '144p': 144,
455
- };
456
-
457
- const QUALITY_TIERS = Object.keys(QUALITY_MAP);
458
-
459
- function toHeight(label) {
460
- if (!label) return 0;
461
- const cleaned = String(label).toLowerCase().replace(/[^0-9]/g, '');
462
- const num = parseInt(cleaned, 10);
463
- return isNaN(num) ? 0 : num;
246
+ class QualityError extends YouTubeError {
247
+ constructor(message, details = null) {
248
+ super(message, 'QUALITY_ERROR', details);
249
+ this.name = 'QualityError';
250
+ }
464
251
  }
465
252
 
466
- function matchQualityRank(format, targetHeight, tolerance = 72) {
467
- if (!targetHeight) return true;
468
- const h = format.height || toHeight(format.qualityLabel);
469
- if (!h) return false;
470
- return Math.abs(h - targetHeight) <= tolerance;
253
+ class MergeError extends YouTubeError {
254
+ constructor(message, details = null) {
255
+ super(message, 'MERGE_ERROR', details);
256
+ this.name = 'MergeError';
257
+ }
471
258
  }
472
259
 
473
- function getFallbackChain(requested) {
474
- const t = String(requested || '').toLowerCase();
475
- if (t === 'auto' || t === 'best') return [...QUALITY_TIERS];
476
- if (t === 'audio') return ['audio'];
477
- const idx = QUALITY_TIERS.indexOf(t);
478
- if (idx !== -1) return QUALITY_TIERS.slice(idx);
479
- return [...QUALITY_TIERS];
260
+ class NetworkError extends YouTubeError {
261
+ constructor(message, details = null) {
262
+ super(message, 'NETWORK_ERROR', details);
263
+ this.name = 'NetworkError';
264
+ }
480
265
  }
481
266
 
482
- function validateQuality(requested) {
483
- if (!requested) return 'auto';
484
- const t = String(requested).toLowerCase();
485
- if (t === 'auto' || t === 'best' || t === 'audio') return t;
486
- if (QUALITY_TIERS.includes(t)) return t;
487
- throw new QualityError(
488
- `Unsupported quality "${requested}". Available: ${QUALITY_TIERS.join(', ')}, auto, best, audio`,
489
- { requested, supported: [...QUALITY_TIERS, 'auto', 'best', 'audio'] }
490
- );
267
+ class ValidationError extends YouTubeError {
268
+ constructor(message, details = null) {
269
+ super(message, 'VALIDATION_ERROR', details);
270
+ this.name = 'ValidationError';
271
+ }
491
272
  }
492
273
 
493
274
  module.exports = {
494
- QUALITY_MAP,
495
- QUALITY_TIERS,
496
- toHeight,
497
- matchQualityRank,
498
- getFallbackChain,
499
- validateQuality,
275
+ YouTubeError,
276
+ FormatError,
277
+ QualityError,
278
+ MergeError,
279
+ NetworkError,
280
+ ValidationError,
500
281
  };
501
282
 
502
- // formats/mime.js
503
- const { resolveContainer, requiresConversion } = require('./Registry');
504
-
505
- function mimeTypeToContainer(mimeType) {
506
- if (!mimeType) return 'mp4';
507
- const parts = mimeType.split('/');
508
- if (parts.length < 2) return 'mp4';
509
- const sub = parts[1].split(';')[0].trim().toLowerCase();
510
- const map = {
511
- mp4: 'mp4',
512
- webm: 'webm',
513
- 'x-matroska': 'mkv',
514
- 'x-msvideo': 'avi',
515
- quicktime: 'mov',
516
- aac: 'aac',
517
- mpeg: 'mp3',
518
- wav: 'wav',
519
- ogg: 'ogg',
520
- flac: 'flac',
521
- '3gpp': '3gp',
522
- };
523
- return map[sub] || sub;
524
- }
525
-
526
- function checkContainer(format, targetContainer) {
527
- const current = format.container || mimeTypeToContainer(format.mimeType);
528
- if (!targetContainer) return { compatible: true, current, needsConversion: false };
529
- const tc = resolveContainer(targetContainer);
530
- if (!tc) return { compatible: false, current, target: targetContainer, needsConversion: false };
531
- if (current === targetContainer) return { compatible: true, current, target: targetContainer, needsConversion: false };
532
- return {
533
- compatible: false,
534
- current,
535
- target: targetContainer,
536
- needsConversion: true,
537
- requiresTool: requiresConversion(format, targetContainer),
538
- };
539
- }
540
-
541
- module.exports = { mimeTypeToContainer, checkContainer };
542
-
543
- const { Format } = require('./Format');
544
- const { getFallbackChain, matchQualityRank, toHeight, validateQuality } = require('./Qualities');
545
- const { checkContainer, mimeTypeToContainer } = require('./mime');
546
- const { requiresConversion } = require('./Registry');
547
- const { FormatError, QualityError } = require('../core/Errors');
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');
548
289
 
549
290
  class FormatSelector {
550
291
  #combined;
@@ -586,7 +327,7 @@ class FormatSelector {
586
327
 
587
328
  select(options = {}) {
588
329
  const quality = validateQuality(options.quality || 'auto');
589
- const container = options.format || options.container || null;
330
+ const container = options.format || null;
590
331
 
591
332
  if (quality === 'audio') {
592
333
  return this.#selectAudio(options);
@@ -671,7 +412,7 @@ class FormatSelector {
671
412
  }
672
413
 
673
414
  #selectAudio(options) {
674
- const container = options.format || options.container || null;
415
+ const container = options.format || null;
675
416
  const audios = this.#all
676
417
  .filter((f) => f.isAudio && f.hasUrl)
677
418
  .sort((a, b) => b.bitrate - a.bitrate);
@@ -724,18 +465,137 @@ class FormatSelector {
724
465
 
725
466
  module.exports = { FormatSelector };
726
467
 
727
- // utils/validators.js
728
- const { ValidationError } = require('../core/Errors');
729
-
730
- const VALID_QUALITIES = ['4320p', '2160p', '1440p', '1080p', '720p', '480p', '360p', '240p', '144p', 'auto', 'best', 'audio'];
731
- const VALID_CONTAINERS = ['mp4', 'webm', 'mkv', 'avi', 'mov', 'm4a', 'aac', 'flac', 'ogg', 'mp3', 'wav'];
732
-
733
- function validateOptions(options = {}) {
734
- const errors = [];
468
+ },["formats/Format","formats/Qualities","formats/mime","core/Errors"]],
469
+ "formats/Registry":[function(module,exports,__r__){
470
+ const { FormatError } = __r__('core/Errors');
735
471
 
736
- if (options.quality && !VALID_QUALITIES.includes(String(options.quality).toLowerCase())) {
737
- errors.push(`Invalid quality "${options.quality}". Valid: ${VALID_QUALITIES.join(', ')}`);
738
- }
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
+ }
739
599
 
740
600
  if (options.format && !VALID_CONTAINERS.includes(String(options.format).toLowerCase())) {
741
601
  errors.push(`Invalid format "${options.format}". Valid: ${VALID_CONTAINERS.join(', ')}`);
@@ -768,7 +628,8 @@ function validateOptions(options = {}) {
768
628
 
769
629
  module.exports = { validateOptions, VALID_QUALITIES, VALID_CONTAINERS };
770
630
 
771
- // utils/url.js
631
+ },["core/Errors","formats/Qualities","formats/Registry"]],
632
+ "utils/url":[function(module,exports,__r__){
772
633
  const { URL } = require('node:url');
773
634
 
774
635
  function extractVideoId(input) {
@@ -805,10 +666,11 @@ function isValidVideoId(id) {
805
666
 
806
667
  module.exports = { extractVideoId, isValidVideoId };
807
668
 
808
- // download/Merge.js
669
+ },[]],
670
+ "download/Merge":[function(module,exports,__r__){
809
671
  const fs = require('node:fs');
810
672
  const { spawn } = require('node:child_process');
811
- const { MergeError } = require('../core/Errors');
673
+ const { MergeError } = __r__('core/Errors');
812
674
 
813
675
  const TOOLS = {
814
676
  ffmpeg: {
@@ -912,17 +774,18 @@ module.exports = {
912
774
  TOOLS,
913
775
  };
914
776
 
915
- // download/Downloader.js
777
+ },["core/Errors"]],
778
+ "download/Downloader":[function(module,exports,__r__){
916
779
  const fs = require('node:fs');
917
780
  const https = require('node:https');
918
781
  const { URL } = require('node:url');
919
- const { head, stream } = require('../core/Client');
920
- const { NetworkError } = require('../core/Errors');
782
+ const { head } = __r__('core/Client');
783
+ const { NetworkError } = __r__('core/Errors');
921
784
 
922
785
  const CHUNK_SIZE = 10 * 1024 * 1024;
923
786
  const MAX_CONCURRENCY = 6;
924
787
 
925
- async function verify(url) {
788
+ function verify(url) {
926
789
  return head(url);
927
790
  }
928
791
 
@@ -933,24 +796,23 @@ function createReadStream(url) {
933
796
  async function downloadToFile(url, filePath, options = {}) {
934
797
  const concurrency = options.concurrency || MAX_CONCURRENCY;
935
798
  const onProgress = options.onProgress || null;
936
- const chunkSize = options.chunkSize || CHUNK_SIZE;
937
799
 
938
800
  const size = await getContentLength(url);
939
801
 
940
- if (!size || size < chunkSize) {
802
+ if (!size || size < CHUNK_SIZE) {
941
803
  return simpleDownload(url, filePath, onProgress);
942
804
  }
943
805
 
944
- return parallelDownload(url, filePath, size, concurrency, chunkSize, onProgress);
806
+ return parallelDownload(url, filePath, size, concurrency, onProgress);
945
807
  }
946
808
 
947
809
  function getContentLength(url) {
948
810
  return new Promise((resolve) => {
949
- const u = new URL(url);
811
+ let u;
812
+ try { u = new URL(url); } catch { resolve(0); return; }
950
813
  const req = https.get({
951
814
  hostname: u.hostname,
952
815
  path: u.pathname + u.search,
953
- method: 'GET',
954
816
  headers: {
955
817
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
956
818
  'Referer': 'https://www.youtube.com/',
@@ -959,8 +821,8 @@ function getContentLength(url) {
959
821
  }, (res) => {
960
822
  const cr = res.headers['content-range'];
961
823
  if (cr) {
962
- const match = cr.match(/\/(\d+)/);
963
- if (match) resolve(parseInt(match[1], 10));
824
+ const m = cr.match(/\/(\d+)/);
825
+ if (m) { resolve(parseInt(m[1], 10)); res.resume(); return; }
964
826
  }
965
827
  resolve(parseInt(res.headers['content-length'] || '0', 10));
966
828
  res.resume();
@@ -973,7 +835,8 @@ function getContentLength(url) {
973
835
  function simpleDownload(url, filePath, onProgress, redirects = 0) {
974
836
  return new Promise((resolve, reject) => {
975
837
  if (redirects > 5) return reject(new NetworkError('Too many redirects'));
976
- const u = new URL(url);
838
+ let u;
839
+ try { u = new URL(url); } catch { return reject(new NetworkError('Invalid URL')); }
977
840
  const req = https.get({
978
841
  hostname: u.hostname,
979
842
  path: u.pathname + u.search,
@@ -996,9 +859,9 @@ function simpleDownload(url, filePath, onProgress, redirects = 0) {
996
859
  res.on('data', (chunk) => {
997
860
  downloaded += chunk.length;
998
861
  if (onProgress && total) onProgress(downloaded, total);
862
+ out.write(chunk);
999
863
  });
1000
-
1001
- res.pipe(out);
864
+ res.on('end', () => { out.end(); });
1002
865
  out.on('finish', () => resolve(filePath));
1003
866
  out.on('error', reject);
1004
867
  });
@@ -1007,37 +870,33 @@ function simpleDownload(url, filePath, onProgress, redirects = 0) {
1007
870
  });
1008
871
  }
1009
872
 
1010
- function parallelDownload(url, filePath, totalSize, concurrency, chunkSize, onProgress) {
1011
- const actualConcurrency = Math.min(concurrency, MAX_CONCURRENCY);
1012
- const count = Math.min(actualConcurrency, Math.ceil(totalSize / chunkSize));
1013
- const actualChunkSize = Math.ceil(totalSize / count);
1014
-
1015
- const ranges = Array.from({ length: count }, (_, i) => ({
1016
- start: i * actualChunkSize,
1017
- end: i === count - 1 ? totalSize - 1 : (i + 1) * actualChunkSize - 1,
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,
1018
880
  }));
1019
881
 
1020
- return new Promise(async (resolve, reject) => {
1021
- try {
1022
- const buffers = await Promise.all(
1023
- ranges.map((r, i) => downloadChunk(url, r.start, r.end, i + 1, count))
1024
- );
1025
-
1026
- if (onProgress) onProgress(totalSize, totalSize);
1027
-
1028
- const full = Buffer.concat(buffers);
1029
- fs.writeFileSync(filePath, full);
1030
- resolve(filePath);
1031
- } catch (err) {
1032
- reject(err);
1033
- }
1034
- });
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
+ }
1035
893
  }
1036
894
 
1037
- function downloadChunk(url, start, end, index, total, redirects = 0) {
895
+ function downloadChunk(url, start, end, redirects = 0) {
1038
896
  return new Promise((resolve, reject) => {
1039
- if (redirects > 5) return reject(new NetworkError('Too many redirects'));
1040
- const u = new URL(url);
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')); }
1041
900
  const req = https.get({
1042
901
  hostname: u.hostname,
1043
902
  path: u.pathname + u.search,
@@ -1049,17 +908,17 @@ function downloadChunk(url, start, end, index, total, redirects = 0) {
1049
908
  }, (res) => {
1050
909
  if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
1051
910
  res.resume();
1052
- return resolve(downloadChunk(res.headers.location, start, end, index, total, redirects + 1));
911
+ return resolve(downloadChunk(res.headers.location, start, end, redirects + 1));
1053
912
  }
1054
913
  if (res.statusCode !== 200 && res.statusCode !== 206) {
1055
- return reject(new NetworkError(`Chunk HTTP ${res.statusCode}`));
914
+ return reject(new Error(`Chunk HTTP ${res.statusCode}`));
1056
915
  }
1057
916
  const chunks = [];
1058
917
  res.on('data', (c) => chunks.push(c));
1059
918
  res.on('end', () => resolve(Buffer.concat(chunks)));
1060
919
  });
1061
920
  req.on('error', reject);
1062
- req.setTimeout(120000, () => { req.destroy(new Error(`Chunk ${index} timed out`)); });
921
+ req.setTimeout(120000, () => { req.destroy(new Error('Chunk timed out')); });
1063
922
  });
1064
923
  }
1065
924
 
@@ -1070,9 +929,10 @@ module.exports = {
1070
929
  getContentLength,
1071
930
  };
1072
931
 
1073
- // download/Stream.js
932
+ },["core/Client","core/Errors"]],
933
+ "download/Stream":[function(module,exports,__r__){
1074
934
  const { Transform } = require('node:stream');
1075
- const { createReadStream } = require('./Downloader');
935
+ const { createReadStream } = __r__('download/Downloader');
1076
936
 
1077
937
  class DownloadStream extends Transform {
1078
938
  #url;
@@ -1118,7 +978,6 @@ function createStream(url, options = {}) {
1118
978
  const source = createReadStream(url);
1119
979
 
1120
980
  source.on('error', (err) => transform.destroy(err));
1121
- source.on('response', () => {});
1122
981
  source.pipe(transform);
1123
982
 
1124
983
  return transform;
@@ -1126,49 +985,9 @@ function createStream(url, options = {}) {
1126
985
 
1127
986
  module.exports = { DownloadStream, createStream };
1128
987
 
1129
- // utils/constants.js
1130
- // ../package.json
1131
- {
1132
- "name": "yt-direct",
1133
- "version": "1.0.0",
1134
- "description": "Hello, I present to you a module to download YouTube videos directly",
1135
- "main": "dist/index.js",
1136
- "types": "dist/index.d.ts",
1137
- "files": ["dist", "README.md", "LICENSE"],
1138
- "scripts": {
1139
- "build": "node build/build.js",
1140
- "prepublishOnly": "npm run build",
1141
- "test": "node test/basic.js",
1142
- "example": "node examples/basic.js"
1143
- },
1144
- "keywords": [
1145
- "youtube",
1146
- "download",
1147
- "video",
1148
- "yt-dlp",
1149
- "innertube",
1150
- "downloader",
1151
- "ytdl",
1152
- "mp4",
1153
- "stream",
1154
- "no-dependencies"
1155
- ],
1156
- "license": "MIT",
1157
- "engines": {
1158
- "node": ">=18.0.0"
1159
- },
1160
- "repository": {
1161
- "type": "git",
1162
- "url": "https://github.com/SoyMaycol/yt-direct.git"
1163
- },
1164
- "bugs": {
1165
- "url": "https://github.com/SoyMaycol/yt-direct/issues"
1166
- },
1167
- "homepage": "https://github.com/SoyMaycol/yt-direct#readme",
1168
- "author": "SoyMaycol"
1169
- }
1170
-
1171
- const pkg = require('../../package.json');
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"};
1172
991
 
1173
992
  module.exports = {
1174
993
  VERSION: pkg.version,
@@ -1184,121 +1003,277 @@ module.exports = {
1184
1003
  INTEGRITY_SEED: 'yt-direct-v1',
1185
1004
  };
1186
1005
 
1187
- const { fetch } = require('./core/InnerTube');
1188
- const { head } = require('./core/Client');
1189
- const { YouTubeError, FormatError, ValidationError } = require('./core/Errors');
1190
- const { FormatSelector } = require('./formats/Selector');
1191
- const { Format } = require('./formats/Format');
1192
- const { resolveContainer, requiresConversion, QUALITY_TIERS, CONTAINER_MAP } = require('./formats/Registry');
1193
- const { validateOptions } = require('./utils/validators');
1194
- const { extractVideoId } = require('./utils/url');
1195
- const { merge } = require('./download/Merge');
1196
- const { downloadToFile, createReadStream, verify } = require('./download/Downloader');
1197
- const { createStream } = require('./download/Stream');
1198
- const { VERSION } = require('./utils/constants');
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');
1199
1011
 
1200
- function ytdl(input, options = {}) {
1201
- const videoId = extractVideoId(input);
1202
- if (!videoId) {
1203
- return Promise.reject(new ValidationError(`Invalid YouTube URL or video ID: "${input}"`));
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';
1204
1121
  }
1205
1122
 
1206
- const normalized = validateOptions(options);
1123
+ get hasUrl() {
1124
+ return !!this.url;
1125
+ }
1207
1126
 
1208
- return (async () => {
1209
- const info = await getInfo(videoId);
1210
- const selector = new FormatSelector(info.streamingData);
1211
- const selected = selector.select(normalized);
1127
+ get sizeMB() {
1128
+ return this.contentLength > 0 ? (this.contentLength / 1024 / 1024).toFixed(1) : '?';
1129
+ }
1212
1130
 
1213
- const format = selected.format;
1214
- const audio = selected.audio || null;
1131
+ get qualityRank() {
1132
+ if (this.isAudio) return this.bitrate;
1133
+ return (this.height || 0) * (this.width || 0);
1134
+ }
1215
1135
 
1216
- const response = {
1217
- videoId,
1218
- title: info.title || 'video',
1219
- url: format.url,
1220
- format,
1221
- audio,
1222
- type: selected.type,
1223
- stream: () => createStream(format.url),
1224
- pipe: (writable) => createStream(format.url).pipe(writable),
1225
- download: async (filePath) => {
1226
- if (!filePath) {
1227
- const ext = normalized.format || format.container || 'mp4';
1228
- filePath = `${sanitize(info.title || 'video')}.${ext}`;
1229
- }
1230
- return downloadToFile(format.url, filePath, {
1231
- concurrency: normalized.concurrency,
1232
- onProgress: normalized.onProgress,
1233
- });
1234
- },
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,
1235
1149
  };
1150
+ }
1236
1151
 
1237
- if (normalized.merge && audio) {
1238
- response.merge = async (outputPath) => {
1239
- if (!outputPath) {
1240
- const ext = normalized.format || 'mkv';
1241
- outputPath = `${sanitize(info.title || 'video')}.${ext}`;
1242
- }
1243
- const tmpVideo = `/tmp/yt-direct-${format.itag}-video`;
1244
- const tmpAudio = `/tmp/yt-direct-${audio.itag}-audio`;
1245
- try {
1246
- await Promise.all([
1247
- downloadToFile(format.url, tmpVideo),
1248
- downloadToFile(audio.url, tmpAudio),
1249
- ]);
1250
- await merge(tmpVideo, tmpAudio, outputPath, normalized.merge);
1251
- return outputPath;
1252
- } finally {
1253
- try { require('node:fs').unlinkSync(tmpVideo); } catch {}
1254
- try { require('node:fs').unlinkSync(tmpAudio); } catch {}
1255
- }
1256
- };
1257
- }
1152
+ inspect() {
1153
+ return `Format(${this.itag} | ${this.qualityLabel} | ${this.container} | ${this.codec})`;
1154
+ }
1258
1155
 
1259
- return response;
1260
- })();
1156
+ [Symbol.for('nodejs.util.inspect.custom')]() {
1157
+ return this.inspect();
1158
+ }
1261
1159
  }
1262
1160
 
1263
- async function getInfo(input) {
1264
- const videoId = extractVideoId(input);
1265
- if (!videoId) throw new ValidationError(`Invalid YouTube URL or video ID: "${input}"`);
1161
+ module.exports = { Format };
1266
1162
 
1267
- const result = await fetch(videoId);
1268
- const selector = new FormatSelector(result.streamingData);
1163
+ },["formats/Registry"]],
1164
+ "formats/Qualities":[function(module,exports,__r__){
1165
+ const { QualityError } = __r__('core/Errors');
1269
1166
 
1270
- return {
1271
- id: videoId,
1272
- title: result.videoDetails.title || 'Unknown',
1273
- author: result.videoDetails.author || result.videoDetails?.channelId || null,
1274
- duration: parseInt(result.videoDetails.lengthSeconds || '0', 10),
1275
- thumbnails: result.videoDetails.thumbnail?.thumbnails || [],
1276
- description: result.videoDetails.shortDescription || '',
1277
- viewCount: parseInt(result.videoDetails.viewCount || '0', 10),
1278
- isLive: result.videoDetails.isLive === true,
1279
- streamingData: result.streamingData,
1280
- formats: selector.list(),
1281
- combined: selector.combined.map((f) => f.toJSON()),
1282
- adaptive: selector.adaptive.map((f) => f.toJSON()),
1283
- clientUsed: result.clientUsed,
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',
1284
1245
  };
1246
+ return map[sub] || sub;
1285
1247
  }
1286
1248
 
1287
- function sanitize(name) {
1288
- return String(name || 'video').replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, ' ').trim() || 'video';
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
+ };
1289
1262
  }
1290
1263
 
1291
- ytdl.getInfo = getInfo;
1292
- ytdl.getFormats = (input) => getInfo(input).then((i) => i.formats);
1293
- ytdl.verifyURL = verify;
1294
- ytdl.createStream = createStream;
1295
- ytdl.version = VERSION;
1264
+ module.exports = { mimeTypeToContainer, checkContainer };
1296
1265
 
1297
- ytdl.FORMATS = Object.keys(CONTAINER_MAP);
1298
- ytdl.QUALITIES = [...QUALITY_TIERS, 'auto', 'best', 'audio'];
1299
- ytdl.FormatError = FormatError;
1300
- ytdl.YouTubeError = YouTubeError;
1301
- ytdl.ValidationError = ValidationError;
1266
+ },["formats/Registry"]],
1267
+ };
1302
1268
 
1303
- module.exports = ytdl;
1304
- module.exports.default = ytdl;
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
+ module.exports=__r("index");
1279
+ })();