cloud-ytdl 1.0.0-rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1063 -0
- package/lib/agents.js +114 -0
- package/lib/cache.js +45 -0
- package/lib/format-utils.js +148 -0
- package/lib/format.js +82 -0
- package/lib/index.js +169 -0
- package/lib/info.js +209 -0
- package/lib/innertube.js +1 -0
- package/lib/load.js +38 -0
- package/lib/playlist.js +57 -0
- package/lib/post.js +88 -0
- package/lib/sig-decoder.js +1 -0
- package/lib/subtitle.js +106 -0
- package/lib/url-utils.js +58 -0
- package/lib/utils.js +294 -0
- package/lib/xmlTosrt.js +49 -0
- package/package.json +80 -0
- package/types/index.d.ts +488 -0
package/lib/agents.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { ProxyAgent } = require('undici')
|
|
4
|
+
const { HttpsProxyAgent } = require('https-proxy-agent')
|
|
5
|
+
const { Cookie, CookieJar, canonicalDomain } = require('tough-cookie')
|
|
6
|
+
const { CookieAgent } = require('http-cookie-agent/undici')
|
|
7
|
+
|
|
8
|
+
const mapSameSite = s =>
|
|
9
|
+
s === 'strict' || s === 'lax' || s === 'none' ? s : 'none'
|
|
10
|
+
|
|
11
|
+
const toCookie = c => {
|
|
12
|
+
if (c instanceof Cookie) return c
|
|
13
|
+
if (!c || !c.name) return null
|
|
14
|
+
|
|
15
|
+
return new Cookie({
|
|
16
|
+
key: c.name,
|
|
17
|
+
value: c.value ?? '',
|
|
18
|
+
expires:
|
|
19
|
+
typeof c.expirationDate === 'number'
|
|
20
|
+
? new Date(c.expirationDate * 1000)
|
|
21
|
+
: 'Infinity',
|
|
22
|
+
domain: canonicalDomain(c.domain || 'youtube.com'),
|
|
23
|
+
path: c.path || '/',
|
|
24
|
+
secure: !!c.secure,
|
|
25
|
+
httpOnly: !!c.httpOnly,
|
|
26
|
+
sameSite: mapSameSite(c.sameSite),
|
|
27
|
+
hostOnly: !!c.hostOnly,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ensureSOCS = cookies => {
|
|
32
|
+
if (!Array.isArray(cookies)) return
|
|
33
|
+
|
|
34
|
+
if (!cookies.some(c => c?.name === 'SOCS')) {
|
|
35
|
+
cookies.push({
|
|
36
|
+
name: 'SOCS',
|
|
37
|
+
value: 'CAI',
|
|
38
|
+
domain: '.youtube.com',
|
|
39
|
+
path: '/',
|
|
40
|
+
secure: true,
|
|
41
|
+
httpOnly: false,
|
|
42
|
+
sameSite: 'lax',
|
|
43
|
+
hostOnly: false,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
const addCookies = (exports.addCookies = (jar, cookies = []) => {
|
|
50
|
+
if (!Array.isArray(cookies)) return
|
|
51
|
+
|
|
52
|
+
ensureSOCS(cookies)
|
|
53
|
+
|
|
54
|
+
for (const c of cookies) {
|
|
55
|
+
const ck = toCookie(c)
|
|
56
|
+
if (!ck) continue
|
|
57
|
+
jar.setCookieSync(ck, 'https://www.youtube.com')
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
exports.addCookiesFromString = (jar, str) => {
|
|
62
|
+
if (typeof str !== 'string') return
|
|
63
|
+
const parsed = str
|
|
64
|
+
.split(';')
|
|
65
|
+
.map(s => Cookie.parse(s))
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
|
|
68
|
+
addCookies(jar, parsed)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const createAgent = (exports.createAgent = (cookies, opts) => {
|
|
73
|
+
if (cookies && !Array.isArray(cookies) && typeof cookies === 'object') {
|
|
74
|
+
opts = cookies
|
|
75
|
+
cookies = []
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
cookies ??= []
|
|
79
|
+
opts ??= {}
|
|
80
|
+
|
|
81
|
+
const jar = opts.cookies?.jar || new CookieJar()
|
|
82
|
+
|
|
83
|
+
if (!opts.cookies) addCookies(jar, cookies)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
dispatcher: new CookieAgent({
|
|
87
|
+
...opts,
|
|
88
|
+
cookies: { jar },
|
|
89
|
+
}),
|
|
90
|
+
jar,
|
|
91
|
+
localAddress: opts.localAddress,
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
exports.createProxyAgent = (options, cookies = []) => {
|
|
96
|
+
if (typeof options === 'string') {
|
|
97
|
+
options = { uri: options }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const jar = new CookieJar()
|
|
101
|
+
addCookies(jar, cookies)
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
agent: new HttpsProxyAgent(options.uri),
|
|
105
|
+
dispatcher: new CookieAgent({
|
|
106
|
+
dispatcher: new ProxyAgent(options),
|
|
107
|
+
cookies: { jar },
|
|
108
|
+
}),
|
|
109
|
+
jar,
|
|
110
|
+
localAddress: options.localAddress,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
exports.defaultAgent = createAgent()
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { setTimeout, clearTimeout } = require("timers");
|
|
2
|
+
|
|
3
|
+
module.exports = class Cache extends Map {
|
|
4
|
+
constructor(timeout = 1000) {
|
|
5
|
+
super();
|
|
6
|
+
this.timeout = timeout;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
set(key, value) {
|
|
10
|
+
const old = super.get(key);
|
|
11
|
+
if (old) clearTimeout(old.tid);
|
|
12
|
+
|
|
13
|
+
const tid = setTimeout(() => this.delete(key), this.timeout);
|
|
14
|
+
tid.unref?.();
|
|
15
|
+
|
|
16
|
+
return super.set(key, { tid, value });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get(key) {
|
|
20
|
+
return super.get(key)?.value ?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getOrSet(key, fn) {
|
|
24
|
+
if (this.has(key)) return this.get(key);
|
|
25
|
+
|
|
26
|
+
const value = fn();
|
|
27
|
+
this.set(key, value);
|
|
28
|
+
|
|
29
|
+
Promise.resolve(value).catch(() => this.delete(key));
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
delete(key) {
|
|
34
|
+
const entry = super.get(key);
|
|
35
|
+
if (!entry) return false;
|
|
36
|
+
|
|
37
|
+
clearTimeout(entry.tid);
|
|
38
|
+
return super.delete(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clear() {
|
|
42
|
+
for (const { tid } of super.values()) clearTimeout(tid);
|
|
43
|
+
super.clear();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const utils = require("./utils");
|
|
2
|
+
const FORMATS = require("./format");
|
|
3
|
+
|
|
4
|
+
const audioRanks = ["mp4a", "mp3", "vorbis", "aac", "opus", "flac"];
|
|
5
|
+
const videoRanks = ["mp4v", "avc1", "Sorenson H.283", "MPEG-4 Visual", "VP8", "VP9", "H.264"];
|
|
6
|
+
|
|
7
|
+
const vBitrate = f => f.bitrate || 0;
|
|
8
|
+
const aBitrate = f => f.audioBitrate || 0;
|
|
9
|
+
const vRank = f => videoRanks.findIndex(e => f.codecs?.includes(e));
|
|
10
|
+
const aRank = f => audioRanks.findIndex(e => f.codecs?.includes(e));
|
|
11
|
+
|
|
12
|
+
const sortBy = (a, b, fns) => {
|
|
13
|
+
for (const fn of fns) {
|
|
14
|
+
const r = fn(b) - fn(a);
|
|
15
|
+
if (r) return r;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const sortVideo = (a, b) =>
|
|
21
|
+
sortBy(a, b, [f => +f.qualityLabel, vBitrate, vRank]);
|
|
22
|
+
|
|
23
|
+
const sortAudio = (a, b) =>
|
|
24
|
+
sortBy(a, b, [aBitrate, aRank]);
|
|
25
|
+
|
|
26
|
+
const sortFormats = (a, b) =>
|
|
27
|
+
sortBy(a, b, [
|
|
28
|
+
f => +!!f.isHLS,
|
|
29
|
+
f => +!!f.isDashMPD,
|
|
30
|
+
f => +(f.contentLength > 0),
|
|
31
|
+
f => +(f.hasVideo && f.hasAudio),
|
|
32
|
+
f => +f.hasVideo,
|
|
33
|
+
f => +f.qualityLabel || 0,
|
|
34
|
+
vBitrate,
|
|
35
|
+
aBitrate,
|
|
36
|
+
vRank,
|
|
37
|
+
aRank,
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const byQuality = (quality, formats) => {
|
|
41
|
+
const find = q => formats.find(f => `${f.itag}` === `${q}`);
|
|
42
|
+
return Array.isArray(quality)
|
|
43
|
+
? find(quality.find(q => find(q)))
|
|
44
|
+
: find(quality);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const chooseFormat = (formats, opts = {}) => {
|
|
48
|
+
formats = Array.isArray(formats) ? formats : [formats];
|
|
49
|
+
|
|
50
|
+
if (typeof opts.format === "object") {
|
|
51
|
+
if (!opts.format.url) throw Error("Invalid format");
|
|
52
|
+
return opts.format;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (opts.filter) formats = filterFormats(formats, opts.filter);
|
|
56
|
+
|
|
57
|
+
if (formats.some(f => f.isHLS)) {
|
|
58
|
+
formats = formats.filter(f => f.isHLS || !f.isLive);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const q = opts.quality || "highest";
|
|
62
|
+
let f;
|
|
63
|
+
|
|
64
|
+
switch (q) {
|
|
65
|
+
case "highest":
|
|
66
|
+
f = formats[0];
|
|
67
|
+
break;
|
|
68
|
+
case "lowest":
|
|
69
|
+
f = formats[formats.length - 1];
|
|
70
|
+
break;
|
|
71
|
+
case "highestaudio": {
|
|
72
|
+
formats = filterFormats(formats, "audio").sort(sortAudio);
|
|
73
|
+
const best = formats[0];
|
|
74
|
+
const same = formats.filter(x => sortAudio(best, x) === 0);
|
|
75
|
+
const worstV = Math.min(...same.map(x => +x.qualityLabel || 0));
|
|
76
|
+
f = same.find(x => (+x.qualityLabel || 0) === worstV);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "lowestaudio":
|
|
80
|
+
f = filterFormats(formats, "audio").sort(sortAudio).at(-1);
|
|
81
|
+
break;
|
|
82
|
+
case "highestvideo": {
|
|
83
|
+
formats = filterFormats(formats, "video").sort(sortVideo);
|
|
84
|
+
const best = formats[0];
|
|
85
|
+
const same = formats.filter(x => sortVideo(best, x) === 0);
|
|
86
|
+
const worstA = Math.min(...same.map(x => x.audioBitrate || 0));
|
|
87
|
+
f = same.find(x => (x.audioBitrate || 0) === worstA);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "lowestvideo":
|
|
91
|
+
f = filterFormats(formats, "video").sort(sortVideo).at(-1);
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
f = byQuality(q, formats);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!f) throw Error(`No such format found: ${q}`);
|
|
98
|
+
return f;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const filterFormats = (formats, filter) => {
|
|
102
|
+
let fn;
|
|
103
|
+
switch (filter) {
|
|
104
|
+
case "videoandaudio":
|
|
105
|
+
case "audioandvideo":
|
|
106
|
+
fn = f => f.hasVideo && f.hasAudio;
|
|
107
|
+
break;
|
|
108
|
+
case "video":
|
|
109
|
+
fn = f => f.hasVideo;
|
|
110
|
+
break;
|
|
111
|
+
case "videoonly":
|
|
112
|
+
fn = f => f.hasVideo && !f.hasAudio;
|
|
113
|
+
break;
|
|
114
|
+
case "audio":
|
|
115
|
+
fn = f => f.hasAudio;
|
|
116
|
+
break;
|
|
117
|
+
case "audioonly":
|
|
118
|
+
fn = f => !f.hasVideo && f.hasAudio;
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
if (typeof filter !== "function") {
|
|
122
|
+
throw TypeError(`Given filter (${filter}) is not supported`);
|
|
123
|
+
}
|
|
124
|
+
fn = filter;
|
|
125
|
+
}
|
|
126
|
+
return formats.filter(f => f.url && fn(f));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const addFormatMeta = f => {
|
|
130
|
+
f = { ...FORMATS[f.itag], ...f };
|
|
131
|
+
f.hasVideo = !!f.qualityLabel;
|
|
132
|
+
f.hasAudio = !!f.audioBitrate;
|
|
133
|
+
f.container = f.mimeType?.split(";")[0].split("/")[1] || null;
|
|
134
|
+
f.codecs = f.mimeType ? utils.between(f.mimeType, 'codecs="', '"') : null;
|
|
135
|
+
f.videoCodec = f.hasVideo && f.codecs ? f.codecs.split(", ")[0] : null;
|
|
136
|
+
f.audioCodec = f.hasAudio && f.codecs ? f.codecs.split(", ").at(-1) : null;
|
|
137
|
+
f.isLive = /\bsource[/=]yt_live_broadcast\b/.test(f.url);
|
|
138
|
+
f.isHLS = /\/manifest\/hls_(variant|playlist)\//.test(f.url);
|
|
139
|
+
f.isDashMPD = /\/manifest\/dash\//.test(f.url);
|
|
140
|
+
return f;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
sortFormats,
|
|
145
|
+
chooseFormat,
|
|
146
|
+
filterFormats,
|
|
147
|
+
addFormatMeta,
|
|
148
|
+
};
|
package/lib/format.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
5:{mimeType:'video/flv; codecs="Sorenson H.283, mp3"',qualityLabel:"240p",bitrate:250000,audioBitrate:64},
|
|
3
|
+
6:{mimeType:'video/flv; codecs="Sorenson H.263, mp3"',qualityLabel:"270p",bitrate:800000,audioBitrate:64},
|
|
4
|
+
13:{mimeType:'video/3gp; codecs="MPEG-4 Visual, aac"',qualityLabel:null,bitrate:500000,audioBitrate:null},
|
|
5
|
+
17:{mimeType:'video/3gp; codecs="MPEG-4 Visual, aac"',qualityLabel:"144p",bitrate:50000,audioBitrate:24},
|
|
6
|
+
18:{mimeType:'video/mp4; codecs="H.264, aac"',qualityLabel:"360p",bitrate:500000,audioBitrate:96},
|
|
7
|
+
22:{mimeType:'video/mp4; codecs="H.264, aac"',qualityLabel:"720p",bitrate:2000000,audioBitrate:192},
|
|
8
|
+
34:{mimeType:'video/flv; codecs="H.264, aac"',qualityLabel:"360p",bitrate:500000,audioBitrate:128},
|
|
9
|
+
35:{mimeType:'video/flv; codecs="H.264, aac"',qualityLabel:"480p",bitrate:800000,audioBitrate:128},
|
|
10
|
+
36:{mimeType:'video/3gp; codecs="MPEG-4 Visual, aac"',qualityLabel:"240p",bitrate:175000,audioBitrate:32},
|
|
11
|
+
37:{mimeType:'video/mp4; codecs="H.264, aac"',qualityLabel:"1080p",bitrate:3000000,audioBitrate:192},
|
|
12
|
+
38:{mimeType:'video/mp4; codecs="H.264, aac"',qualityLabel:"3072p",bitrate:3500000,audioBitrate:192},
|
|
13
|
+
43:{mimeType:'video/webm; codecs="VP8, vorbis"',qualityLabel:"360p",bitrate:500000,audioBitrate:128},
|
|
14
|
+
44:{mimeType:'video/webm; codecs="VP8, vorbis"',qualityLabel:"480p",bitrate:1000000,audioBitrate:128},
|
|
15
|
+
45:{mimeType:'video/webm; codecs="VP8, vorbis"',qualityLabel:"720p",bitrate:2000000,audioBitrate:192},
|
|
16
|
+
46:{mimeType:'audio/webm; codecs="vp8, vorbis"',qualityLabel:"1080p",bitrate:null,audioBitrate:192},
|
|
17
|
+
82:{mimeType:'video/mp4; codecs="H.264, aac"',qualityLabel:"360p",bitrate:500000,audioBitrate:96},
|
|
18
|
+
83:{mimeType:'video/mp4; codecs="H.264, aac"',qualityLabel:"240p",bitrate:500000,audioBitrate:96},
|
|
19
|
+
84:{mimeType:'video/mp4; codecs="H.264, aac"',qualityLabel:"720p",bitrate:2000000,audioBitrate:192},
|
|
20
|
+
85:{mimeType:'video/mp4; codecs="H.264, aac"',qualityLabel:"1080p",bitrate:3000000,audioBitrate:192},
|
|
21
|
+
91:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"144p",bitrate:100000,audioBitrate:48},
|
|
22
|
+
92:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"240p",bitrate:150000,audioBitrate:48},
|
|
23
|
+
93:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"360p",bitrate:500000,audioBitrate:128},
|
|
24
|
+
94:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"480p",bitrate:800000,audioBitrate:128},
|
|
25
|
+
95:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"720p",bitrate:1500000,audioBitrate:256},
|
|
26
|
+
96:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"1080p",bitrate:2500000,audioBitrate:256},
|
|
27
|
+
100:{mimeType:'audio/webm; codecs="VP8, vorbis"',qualityLabel:"360p",bitrate:null,audioBitrate:128},
|
|
28
|
+
101:{mimeType:'audio/webm; codecs="VP8, vorbis"',qualityLabel:"360p",bitrate:null,audioBitrate:192},
|
|
29
|
+
102:{mimeType:'audio/webm; codecs="VP8, vorbis"',qualityLabel:"720p",bitrate:null,audioBitrate:192},
|
|
30
|
+
120:{mimeType:'video/flv; codecs="H.264, aac"',qualityLabel:"720p",bitrate:2000000,audioBitrate:128},
|
|
31
|
+
127:{mimeType:'audio/ts; codecs="aac"',qualityLabel:null,bitrate:null,audioBitrate:96},
|
|
32
|
+
128:{mimeType:'audio/ts; codecs="aac"',qualityLabel:null,bitrate:null,audioBitrate:96},
|
|
33
|
+
132:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"240p",bitrate:150000,audioBitrate:48},
|
|
34
|
+
133:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"240p",bitrate:200000,audioBitrate:null},
|
|
35
|
+
134:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"360p",bitrate:300000,audioBitrate:null},
|
|
36
|
+
135:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"480p",bitrate:500000,audioBitrate:null},
|
|
37
|
+
136:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"720p",bitrate:1000000,audioBitrate:null},
|
|
38
|
+
137:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"1080p",bitrate:2500000,audioBitrate:null},
|
|
39
|
+
138:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"4320p",bitrate:13500000,audioBitrate:null},
|
|
40
|
+
139:{mimeType:'audio/mp4; codecs="aac"',qualityLabel:null,bitrate:null,audioBitrate:48},
|
|
41
|
+
140:{mimeType:'audio/m4a; codecs="aac"',qualityLabel:null,bitrate:null,audioBitrate:128},
|
|
42
|
+
141:{mimeType:'audio/mp4; codecs="aac"',qualityLabel:null,bitrate:null,audioBitrate:256},
|
|
43
|
+
151:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"720p",bitrate:50000,audioBitrate:24},
|
|
44
|
+
160:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"144p",bitrate:100000,audioBitrate:null},
|
|
45
|
+
171:{mimeType:'audio/webm; codecs="vorbis"',qualityLabel:null,bitrate:null,audioBitrate:128},
|
|
46
|
+
172:{mimeType:'audio/webm; codecs="vorbis"',qualityLabel:null,bitrate:null,audioBitrate:192},
|
|
47
|
+
231:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"480p",bitrate:500000,audioBitrate:null},
|
|
48
|
+
232:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"720p",bitrate:800000,audioBitrate:null},
|
|
49
|
+
242:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"240p",bitrate:100000,audioBitrate:null},
|
|
50
|
+
243:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"360p",bitrate:250000,audioBitrate:null},
|
|
51
|
+
244:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"480p",bitrate:500000,audioBitrate:null},
|
|
52
|
+
247:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"720p",bitrate:700000,audioBitrate:null},
|
|
53
|
+
248:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"1080p",bitrate:1500000,audioBitrate:null},
|
|
54
|
+
249:{mimeType:'audio/webm; codecs="opus"',qualityLabel:null,bitrate:null,audioBitrate:48},
|
|
55
|
+
250:{mimeType:'audio/webm; codecs="opus"',qualityLabel:null,bitrate:null,audioBitrate:64},
|
|
56
|
+
251:{mimeType:'audio/webm; codecs="opus"',qualityLabel:null,bitrate:null,audioBitrate:160},
|
|
57
|
+
264:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"1440p",bitrate:4000000,audioBitrate:null},
|
|
58
|
+
266:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"2160p",bitrate:12500000,audioBitrate:null},
|
|
59
|
+
270:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"1080p",bitrate:2500000,audioBitrate:null},
|
|
60
|
+
271:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"1440p",bitrate:9000000,audioBitrate:null},
|
|
61
|
+
272:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"4320p",bitrate:20000000,audioBitrate:null},
|
|
62
|
+
278:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"144p 30fps",bitrate:80000,audioBitrate:null},
|
|
63
|
+
298:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"720p",bitrate:3000000,audioBitrate:null},
|
|
64
|
+
299:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"1080p",bitrate:5500000,audioBitrate:null},
|
|
65
|
+
300:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"720p",bitrate:1318000,audioBitrate:48},
|
|
66
|
+
301:{mimeType:'video/ts; codecs="H.264, aac"',qualityLabel:"1080p",bitrate:3000000,audioBitrate:128},
|
|
67
|
+
302:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"720p HFR",bitrate:2500000,audioBitrate:null},
|
|
68
|
+
303:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"1080p HFR",bitrate:5000000,audioBitrate:null},
|
|
69
|
+
308:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"1440p HFR",bitrate:10000000,audioBitrate:null},
|
|
70
|
+
311:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"720p",bitrate:1250000,audioBitrate:null},
|
|
71
|
+
312:{mimeType:'video/mp4; codecs="H.264"',qualityLabel:"1080p",bitrate:2500000,audioBitrate:null},
|
|
72
|
+
313:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"2160p",bitrate:13000000,audioBitrate:null},
|
|
73
|
+
315:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"2160p HFR",bitrate:20000000,audioBitrate:null},
|
|
74
|
+
330:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"144p HDR, HFR",bitrate:80000,audioBitrate:null},
|
|
75
|
+
331:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"240p HDR, HFR",bitrate:100000,audioBitrate:null},
|
|
76
|
+
332:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"360p HDR, HFR",bitrate:250000,audioBitrate:null},
|
|
77
|
+
333:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"240p HDR, HFR",bitrate:500000,audioBitrate:null},
|
|
78
|
+
334:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"720p HDR, HFR",bitrate:1000000,audioBitrate:null},
|
|
79
|
+
335:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"1080p HDR, HFR",bitrate:1500000,audioBitrate:null},
|
|
80
|
+
336:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"1440p HDR, HFR",bitrate:5000000,audioBitrate:null},
|
|
81
|
+
337:{mimeType:'video/webm; codecs="VP9"',qualityLabel:"2160p HDR, HFR",bitrate:12000000,audioBitrate:null}
|
|
82
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const { PassThrough } = require("stream");
|
|
2
|
+
const miniget = require("miniget");
|
|
3
|
+
const m3u8stream = require("m3u8stream");
|
|
4
|
+
const { parseTimestamp } = require("m3u8stream");
|
|
5
|
+
const { request } = require("undici");
|
|
6
|
+
const getInfo = require("./info");
|
|
7
|
+
const utils = require("./utils");
|
|
8
|
+
const formatUtils = require("./format-utils");
|
|
9
|
+
const getPlaylistInfo = require('./playlist').getPlaylistInfo
|
|
10
|
+
const urlUtils = require("./url-utils");
|
|
11
|
+
const agent = require("./agents.js");
|
|
12
|
+
const subtitle = require('./subtitle')
|
|
13
|
+
const getPostInfo = require("./post");
|
|
14
|
+
const { version } = require("../package.json");
|
|
15
|
+
|
|
16
|
+
const createUndiciStream = (url, opts = {}) => {
|
|
17
|
+
const s = new PassThrough();
|
|
18
|
+
|
|
19
|
+
(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const res = await request(url, {
|
|
22
|
+
method: "GET",
|
|
23
|
+
headers: opts.headers || {},
|
|
24
|
+
dispatcher: opts.dispatcher,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
s.emit("response", { statusCode: res.statusCode, headers: res.headers });
|
|
28
|
+
if (res.statusCode < 200 || res.statusCode >= 400) {
|
|
29
|
+
const e = new Error(`Status code: ${res.statusCode}`);
|
|
30
|
+
e.statusCode = res.statusCode;
|
|
31
|
+
return s.emit("error", e);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for await (const c of res.body) {
|
|
35
|
+
if (s.destroyed) break;
|
|
36
|
+
s.push(c);
|
|
37
|
+
}
|
|
38
|
+
if (!s.destroyed) s.push(null);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (!s.destroyed) s.emit("error", e);
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
|
|
44
|
+
return s;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const createStream = opts => {
|
|
48
|
+
const s = new PassThrough({ highWaterMark: opts?.highWaterMark || 512 * 1024 });
|
|
49
|
+
s._destroy = () => (s.destroyed = true);
|
|
50
|
+
return s;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const pipeEvents = (req, stream, end) => {
|
|
54
|
+
["abort", "request", "response", "error", "redirect", "retry", "reconnect"]
|
|
55
|
+
.forEach(e => req.prependListener(e, stream.emit.bind(stream, e)));
|
|
56
|
+
req.pipe(stream, { end });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const downloadFromInfoCallback = (stream, info, opts = {}) => {
|
|
60
|
+
const err = utils.playError(info.player_response);
|
|
61
|
+
if (err) return stream.emit("error", err);
|
|
62
|
+
if (!info.formats.length)
|
|
63
|
+
return stream.emit("error", Error("This video is unavailable"));
|
|
64
|
+
|
|
65
|
+
let format;
|
|
66
|
+
try {
|
|
67
|
+
format = formatUtils.chooseFormat(info.formats, opts);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return stream.emit("error", e);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
stream.emit("info", info, format);
|
|
73
|
+
if (stream.destroyed) return;
|
|
74
|
+
|
|
75
|
+
let downloaded = 0;
|
|
76
|
+
const ondata = c => {
|
|
77
|
+
downloaded += c.length;
|
|
78
|
+
stream.emit("progress", c.length, downloaded);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
utils.applyDefaultHeaders(opts);
|
|
82
|
+
opts.requestOptions ||= {};
|
|
83
|
+
opts.requestOptions.headers ||= {};
|
|
84
|
+
|
|
85
|
+
if (opts.agent) {
|
|
86
|
+
opts.requestOptions.agent = opts.agent.agent;
|
|
87
|
+
if (opts.agent.jar) {
|
|
88
|
+
utils.setPropInsensitive(
|
|
89
|
+
opts.requestOptions.headers,
|
|
90
|
+
"cookie",
|
|
91
|
+
opts.agent.jar.getCookieStringSync("https://www.youtube.com"),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (opts.agent.localAddress) {
|
|
95
|
+
opts.requestOptions.localAddress = opts.agent.localAddress;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let req;
|
|
100
|
+
|
|
101
|
+
if (format.isHLS || format.isDashMPD) {
|
|
102
|
+
req = m3u8stream(format.url, {
|
|
103
|
+
begin: opts.begin || (format.isLive && Date.now()),
|
|
104
|
+
liveBuffer: opts.liveBuffer,
|
|
105
|
+
requestOptions: opts.requestOptions,
|
|
106
|
+
parser: format.isDashMPD ? "dash-mpd" : "m3u8",
|
|
107
|
+
id: format.itag,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
req.on("progress", (seg, total) =>
|
|
111
|
+
stream.emit("progress", seg.size, seg.num, total),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
pipeEvents(req, stream, true);
|
|
115
|
+
} else {
|
|
116
|
+
const ro = { ...opts.requestOptions };
|
|
117
|
+
|
|
118
|
+
if (opts.begin) format.url += `&begin=${parseTimestamp(opts.begin)}`;
|
|
119
|
+
if (opts.range) {
|
|
120
|
+
ro.headers = {
|
|
121
|
+
...ro.headers,
|
|
122
|
+
Range: `bytes=${opts.range.start || 0}-${opts.range.end || ""}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
req = miniget(format.url, ro);
|
|
127
|
+
req.on("data", ondata);
|
|
128
|
+
pipeEvents(req, stream, true);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
stream._destroy = () => {
|
|
132
|
+
stream.destroyed = true;
|
|
133
|
+
req?.destroy?.();
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const ytdl = (link, opts) => {
|
|
138
|
+
const s = createStream(opts);
|
|
139
|
+
ytdl.getInfo(link, opts).then(
|
|
140
|
+
info => downloadFromInfoCallback(s, info, opts),
|
|
141
|
+
e => s.emit("error", e),
|
|
142
|
+
);
|
|
143
|
+
return s;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
module.exports = ytdl;
|
|
147
|
+
|
|
148
|
+
ytdl.getBasicInfo = getInfo.getBasicInfo;
|
|
149
|
+
ytdl.getInfo = getInfo.getInfo;
|
|
150
|
+
ytdl.chooseFormat = formatUtils.chooseFormat;
|
|
151
|
+
ytdl.getPostInfo = getPostInfo.getPostInfo;
|
|
152
|
+
ytdl.filterFormats = formatUtils.filterFormats;
|
|
153
|
+
ytdl.validateID = urlUtils.validateID;
|
|
154
|
+
ytdl.getPlaylistInfo = getPlaylistInfo
|
|
155
|
+
ytdl.validateURL = urlUtils.validateURL;
|
|
156
|
+
ytdl.getSubtitles = subtitle.getSubtitles
|
|
157
|
+
ytdl.getURLVideoID = urlUtils.getURLVideoID;
|
|
158
|
+
ytdl.getVideoID = urlUtils.getVideoID;
|
|
159
|
+
ytdl.createAgent = agent.createAgent;
|
|
160
|
+
ytdl.createProxyAgent = agent.createProxyAgent;
|
|
161
|
+
ytdl.cache = { info: getInfo.cache, watch: getInfo.watchPageCache };
|
|
162
|
+
ytdl.version = version;
|
|
163
|
+
|
|
164
|
+
ytdl.downloadFromInfo = (info, opts) => {
|
|
165
|
+
if (!info.full) throw Error("Invalid info object");
|
|
166
|
+
const s = createStream(opts);
|
|
167
|
+
setImmediate(() => downloadFromInfoCallback(s, info, opts));
|
|
168
|
+
return s;
|
|
169
|
+
};
|