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/.integrity +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +597 -622
- package/dist/index.mjs +1281 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,165 +1,134 @@
|
|
|
1
|
-
/*! yt-direct v1.0.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
FormatError,
|
|
155
|
-
QualityError,
|
|
156
|
-
MergeError,
|
|
157
|
-
NetworkError,
|
|
158
|
-
ValidationError,
|
|
159
|
-
};
|
|
125
|
+
module.exports = ytdl;
|
|
126
|
+
module.exports.default = ytdl;
|
|
160
127
|
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
275
|
+
YouTubeError,
|
|
276
|
+
FormatError,
|
|
277
|
+
QualityError,
|
|
278
|
+
MergeError,
|
|
279
|
+
NetworkError,
|
|
280
|
+
ValidationError,
|
|
500
281
|
};
|
|
501
282
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
728
|
-
|
|
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
|
-
|
|
737
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 } =
|
|
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
|
-
|
|
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
|
|
920
|
-
const { NetworkError } =
|
|
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
|
-
|
|
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 <
|
|
802
|
+
if (!size || size < CHUNK_SIZE) {
|
|
941
803
|
return simpleDownload(url, filePath, onProgress);
|
|
942
804
|
}
|
|
943
805
|
|
|
944
|
-
return parallelDownload(url, filePath, size, concurrency,
|
|
806
|
+
return parallelDownload(url, filePath, size, concurrency, onProgress);
|
|
945
807
|
}
|
|
946
808
|
|
|
947
809
|
function getContentLength(url) {
|
|
948
810
|
return new Promise((resolve) => {
|
|
949
|
-
|
|
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
|
|
963
|
-
if (
|
|
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
|
-
|
|
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,
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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,
|
|
895
|
+
function downloadChunk(url, start, end, redirects = 0) {
|
|
1038
896
|
return new Promise((resolve, reject) => {
|
|
1039
|
-
if (redirects > 5) return reject(new
|
|
1040
|
-
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
932
|
+
},["core/Client","core/Errors"]],
|
|
933
|
+
"download/Stream":[function(module,exports,__r__){
|
|
1074
934
|
const { Transform } = require('node:stream');
|
|
1075
|
-
const { createReadStream } =
|
|
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
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
const
|
|
1190
|
-
const
|
|
1191
|
-
const {
|
|
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
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
1123
|
+
get hasUrl() {
|
|
1124
|
+
return !!this.url;
|
|
1125
|
+
}
|
|
1207
1126
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
const selected = selector.select(normalized);
|
|
1127
|
+
get sizeMB() {
|
|
1128
|
+
return this.contentLength > 0 ? (this.contentLength / 1024 / 1024).toFixed(1) : '?';
|
|
1129
|
+
}
|
|
1212
1130
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1131
|
+
get qualityRank() {
|
|
1132
|
+
if (this.isAudio) return this.bitrate;
|
|
1133
|
+
return (this.height || 0) * (this.width || 0);
|
|
1134
|
+
}
|
|
1215
1135
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1260
|
-
|
|
1156
|
+
[Symbol.for('nodejs.util.inspect.custom')]() {
|
|
1157
|
+
return this.inspect();
|
|
1158
|
+
}
|
|
1261
1159
|
}
|
|
1262
1160
|
|
|
1263
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
1163
|
+
},["formats/Registry"]],
|
|
1164
|
+
"formats/Qualities":[function(module,exports,__r__){
|
|
1165
|
+
const { QualityError } = __r__('core/Errors');
|
|
1269
1166
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
|
1288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
1299
|
-
ytdl.FormatError = FormatError;
|
|
1300
|
-
ytdl.YouTubeError = YouTubeError;
|
|
1301
|
-
ytdl.ValidationError = ValidationError;
|
|
1266
|
+
},["formats/Registry"]],
|
|
1267
|
+
};
|
|
1302
1268
|
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
+
})();
|