@oreohq/ytdl-core 4.15.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/LICENSE +21 -0
- package/README.md +209 -0
- package/lib/agent.js +100 -0
- package/lib/cache.js +54 -0
- package/lib/format-utils.js +218 -0
- package/lib/formats.js +564 -0
- package/lib/index.js +228 -0
- package/lib/info-extras.js +362 -0
- package/lib/info.js +580 -0
- package/lib/sig.js +280 -0
- package/lib/url-utils.js +87 -0
- package/lib/utils.js +437 -0
- package/package.json +46 -0
- package/typings/index.d.ts +1016 -0
package/lib/sig.js
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
const querystring = require("querystring");
|
2
|
+
const Cache = require("./cache");
|
3
|
+
const utils = require("./utils");
|
4
|
+
const vm = require("vm");
|
5
|
+
|
6
|
+
// A shared cache to keep track of html5player js functions.
|
7
|
+
exports.cache = new Cache(1);
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Extract signature deciphering and n parameter transform functions from html5player file.
|
11
|
+
*
|
12
|
+
* @param {string} html5playerfile
|
13
|
+
* @param {Object} options
|
14
|
+
* @returns {Promise<Array.<string>>}
|
15
|
+
*/
|
16
|
+
exports.getFunctions = (html5playerfile, options) =>
|
17
|
+
exports.cache.getOrSet(html5playerfile, async () => {
|
18
|
+
const body = await utils.request(html5playerfile, options);
|
19
|
+
const functions = exports.extractFunctions(body);
|
20
|
+
exports.cache.set(html5playerfile, functions);
|
21
|
+
return functions;
|
22
|
+
});
|
23
|
+
|
24
|
+
// NewPipeExtractor regexps
|
25
|
+
const DECIPHER_NAME_REGEXPS = [
|
26
|
+
"\\bm=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)",
|
27
|
+
"\\bc&&\\(c=([a-zA-Z0-9$]{2,})\\(decodeURIComponent\\(c\\)\\)",
|
28
|
+
'(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)\\s*\\{\\s*a\\s*=\\s*a\\.split\\(\\s*""\\s*\\)',
|
29
|
+
'([\\w$]+)\\s*=\\s*function\\((\\w+)\\)\\{\\s*\\2=\\s*\\2\\.split\\(""\\)\\s*;',
|
30
|
+
];
|
31
|
+
|
32
|
+
// LavaPlayer regexps
|
33
|
+
const VARIABLE_PART = "[a-zA-Z_\\$][a-zA-Z_0-9]*";
|
34
|
+
const VARIABLE_PART_DEFINE = `\\"?${VARIABLE_PART}\\"?`;
|
35
|
+
const BEFORE_ACCESS = '(?:\\[\\"|\\.)';
|
36
|
+
const AFTER_ACCESS = '(?:\\"\\]|)';
|
37
|
+
const VARIABLE_PART_ACCESS = BEFORE_ACCESS + VARIABLE_PART + AFTER_ACCESS;
|
38
|
+
const REVERSE_PART = ":function\\(a\\)\\{(?:return )?a\\.reverse\\(\\)\\}";
|
39
|
+
const SLICE_PART = ":function\\(a,b\\)\\{return a\\.slice\\(b\\)\\}";
|
40
|
+
const SPLICE_PART = ":function\\(a,b\\)\\{a\\.splice\\(0,b\\)\\}";
|
41
|
+
const SWAP_PART =
|
42
|
+
":function\\(a,b\\)\\{" + "var c=a\\[0\\];a\\[0\\]=a\\[b%a\\.length\\];a\\[b(?:%a.length|)\\]=c(?:;return a)?\\}";
|
43
|
+
|
44
|
+
const DECIPHER_REGEXP =
|
45
|
+
`function(?: ${VARIABLE_PART})?\\(a\\)\\{` +
|
46
|
+
`a=a\\.split\\(""\\);\\s*` +
|
47
|
+
`((?:(?:a=)?${VARIABLE_PART}${VARIABLE_PART_ACCESS}\\(a,\\d+\\);)+)` +
|
48
|
+
`return a\\.join\\(""\\)` +
|
49
|
+
`\\}`;
|
50
|
+
|
51
|
+
const HELPER_REGEXP = `var (${VARIABLE_PART})=\\{((?:(?:${VARIABLE_PART_DEFINE}${REVERSE_PART}|${
|
52
|
+
VARIABLE_PART_DEFINE
|
53
|
+
}${SLICE_PART}|${VARIABLE_PART_DEFINE}${SPLICE_PART}|${VARIABLE_PART_DEFINE}${SWAP_PART}),?\\n?)+)\\};`;
|
54
|
+
|
55
|
+
const SCVR = "[a-zA-Z0-9$_]";
|
56
|
+
const MCR = `${SCVR}+`;
|
57
|
+
const AAR = "\\[(\\d+)]";
|
58
|
+
const N_TRANSFORM_NAME_REGEXPS = [
|
59
|
+
// NewPipeExtractor regexps
|
60
|
+
`${SCVR}="nn"\\[\\+${MCR}\\.${MCR}],${MCR}\\(${MCR}\\),${MCR}=${MCR}\\.${MCR}\\[${MCR}]\\|\\|null\\).+\\|\\|(${
|
61
|
+
MCR
|
62
|
+
})\\(""\\)`,
|
63
|
+
`${SCVR}="nn"\\[\\+${MCR}\\.${MCR}],${MCR}\\(${MCR}\\),${MCR}=${MCR}\\.${MCR}\\[${MCR}]\\|\\|null\\)&&\\(${MCR}=(${
|
64
|
+
MCR
|
65
|
+
})${AAR}`,
|
66
|
+
`${SCVR}="nn"\\[\\+${MCR}\\.${MCR}],${MCR}=${MCR}\\.get\\(${MCR}\\)\\).+\\|\\|(${MCR})\\(""\\)`,
|
67
|
+
`${SCVR}="nn"\\[\\+${MCR}\\.${MCR}],${MCR}=${MCR}\\.get\\(${MCR}\\)\\)&&\\(${MCR}=(${MCR})\\[(\\d+)]`,
|
68
|
+
`\\(${SCVR}=String\\.fromCharCode\\(110\\),${SCVR}=${SCVR}\\.get\\(${SCVR}\\)\\)&&\\(${SCVR}=(${MCR})(?:${AAR})?\\(${
|
69
|
+
SCVR
|
70
|
+
}\\)`,
|
71
|
+
`\\.get\\("n"\\)\\)&&\\(${SCVR}=(${MCR})(?:${AAR})?\\(${SCVR}\\)`,
|
72
|
+
// Skick regexps
|
73
|
+
'(\\w+).length\\|\\|\\w+\\(""\\)',
|
74
|
+
'\\w+.length\\|\\|(\\w+)\\(""\\)',
|
75
|
+
];
|
76
|
+
|
77
|
+
// LavaPlayer regexps
|
78
|
+
const N_TRANSFORM_REGEXP =
|
79
|
+
"function\\(\\s*(\\w+)\\s*\\)\\s*\\{" +
|
80
|
+
"var\\s*(\\w+)=(?:\\1\\.split\\(.*?\\)|String\\.prototype\\.split\\.call\\(\\1,.*?\\))," +
|
81
|
+
"\\s*(\\w+)=(\\[.*?]);\\s*\\3\\[\\d+]" +
|
82
|
+
"(.*?try)(\\{.*?})catch\\(\\s*(\\w+)\\s*\\)\\s*\\{" +
|
83
|
+
'\\s*return"enhanced_except_([A-z0-9-]+)"\\s*\\+\\s*\\1\\s*}' +
|
84
|
+
'\\s*return\\s*(\\2\\.join\\(""\\)|Array\\.prototype\\.join\\.call\\(\\2,.*?\\))};';
|
85
|
+
|
86
|
+
const DECIPHER_ARGUMENT = "sig";
|
87
|
+
const N_ARGUMENT = "ncode";
|
88
|
+
|
89
|
+
const matchRegex = (regex, str) => {
|
90
|
+
const match = str.match(new RegExp(regex, "s"));
|
91
|
+
if (!match) throw new Error(`Could not match ${regex}`);
|
92
|
+
return match;
|
93
|
+
};
|
94
|
+
|
95
|
+
const matchFirst = (regex, str) => matchRegex(regex, str)[0];
|
96
|
+
|
97
|
+
const matchGroup1 = (regex, str) => matchRegex(regex, str)[1];
|
98
|
+
|
99
|
+
const getFuncName = (body, regexps) => {
|
100
|
+
let fn;
|
101
|
+
for (const regex of regexps) {
|
102
|
+
try {
|
103
|
+
fn = matchGroup1(regex, body);
|
104
|
+
try {
|
105
|
+
fn = matchGroup1(`${fn.replace(/\$/g, "\\$")}=\\[([a-zA-Z0-9$\\[\\]]{2,})\\]`, body);
|
106
|
+
} catch (err) {
|
107
|
+
// Function name is not inside an array
|
108
|
+
}
|
109
|
+
break;
|
110
|
+
} catch (err) {
|
111
|
+
continue;
|
112
|
+
}
|
113
|
+
}
|
114
|
+
if (!fn || fn.includes("[")) throw Error();
|
115
|
+
return fn;
|
116
|
+
};
|
117
|
+
|
118
|
+
const DECIPHER_FUNC_NAME = "DisTubeDecipherFunc";
|
119
|
+
const extractDecipherFunc = body => {
|
120
|
+
try {
|
121
|
+
const helperObject = matchFirst(HELPER_REGEXP, body);
|
122
|
+
const decipherFunc = matchFirst(DECIPHER_REGEXP, body);
|
123
|
+
const resultFunc = `var ${DECIPHER_FUNC_NAME}=${decipherFunc};`;
|
124
|
+
const callerFunc = `${DECIPHER_FUNC_NAME}(${DECIPHER_ARGUMENT});`;
|
125
|
+
return helperObject + resultFunc + callerFunc;
|
126
|
+
} catch (e) {
|
127
|
+
return null;
|
128
|
+
}
|
129
|
+
};
|
130
|
+
|
131
|
+
const extractDecipherWithName = body => {
|
132
|
+
try {
|
133
|
+
const decipherFuncName = getFuncName(body, DECIPHER_NAME_REGEXPS);
|
134
|
+
const funcPattern = `(${decipherFuncName.replace(/\$/g, "\\$")}=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})`;
|
135
|
+
const decipherFunc = `var ${matchGroup1(funcPattern, body)};`;
|
136
|
+
const helperObjectName = matchGroup1(";([A-Za-z0-9_\\$]{2,})\\.\\w+\\(", decipherFunc);
|
137
|
+
const helperPattern = `(var ${helperObjectName.replace(/\$/g, "\\$")}=\\{[\\s\\S]+?\\}\\};)`;
|
138
|
+
const helperObject = matchGroup1(helperPattern, body);
|
139
|
+
const callerFunc = `${decipherFuncName}(${DECIPHER_ARGUMENT});`;
|
140
|
+
return helperObject + decipherFunc + callerFunc;
|
141
|
+
} catch (e) {
|
142
|
+
return null;
|
143
|
+
}
|
144
|
+
};
|
145
|
+
|
146
|
+
const getExtractFunctions = (extractFunctions, body) => {
|
147
|
+
for (const extractFunction of extractFunctions) {
|
148
|
+
try {
|
149
|
+
const func = extractFunction(body);
|
150
|
+
if (!func) continue;
|
151
|
+
return new vm.Script(func);
|
152
|
+
} catch (err) {
|
153
|
+
continue;
|
154
|
+
}
|
155
|
+
}
|
156
|
+
return null;
|
157
|
+
};
|
158
|
+
|
159
|
+
let decipherWarning = false;
|
160
|
+
// This is required function to get the stream url, but we can continue if user doesn't need stream url.
|
161
|
+
const extractDecipher = body => {
|
162
|
+
// Faster: extractDecipherFunc
|
163
|
+
const decipherFunc = getExtractFunctions([extractDecipherFunc, extractDecipherWithName], body);
|
164
|
+
if (!decipherFunc && !decipherWarning) {
|
165
|
+
console.warn(
|
166
|
+
"\x1b[33mWARNING:\x1B[0m Could not parse decipher function.\n" +
|
167
|
+
`Please report this issue with the "${utils.saveDebugFile(
|
168
|
+
"base.js",
|
169
|
+
body,
|
170
|
+
)}" file on https://github.com/distubejs/ytdl-core/issues.\nStream URL will be missing.`,
|
171
|
+
);
|
172
|
+
decipherWarning = true;
|
173
|
+
}
|
174
|
+
return decipherFunc;
|
175
|
+
};
|
176
|
+
|
177
|
+
const N_TRANSFORM_FUNC_NAME = "DisTubeNTransformFunc";
|
178
|
+
const extractNTransformFunc = body => {
|
179
|
+
try {
|
180
|
+
const nFunc = matchFirst(N_TRANSFORM_REGEXP, body);
|
181
|
+
const resultFunc = `var ${N_TRANSFORM_FUNC_NAME}=${nFunc}`;
|
182
|
+
const callerFunc = `${N_TRANSFORM_FUNC_NAME}(${N_ARGUMENT});`;
|
183
|
+
return resultFunc + callerFunc;
|
184
|
+
} catch (e) {
|
185
|
+
return null;
|
186
|
+
}
|
187
|
+
};
|
188
|
+
|
189
|
+
const extractNTransformWithName = body => {
|
190
|
+
try {
|
191
|
+
const nFuncName = getFuncName(body, N_TRANSFORM_NAME_REGEXPS);
|
192
|
+
const funcPattern = `(${
|
193
|
+
nFuncName.replace(/\$/g, "\\$")
|
194
|
+
// eslint-disable-next-line max-len
|
195
|
+
}=\\s*function([\\S\\s]*?\\}\\s*return (([\\w$]+?\\.join\\(""\\))|(Array\\.prototype\\.join\\.call\\([\\w$]+?,[\\n\\s]*(("")|(\\("",""\\)))\\)))\\s*\\}))`;
|
196
|
+
const nTransformFunc = `var ${matchGroup1(funcPattern, body)};`;
|
197
|
+
const callerFunc = `${nFuncName}(${N_ARGUMENT});`;
|
198
|
+
return nTransformFunc + callerFunc;
|
199
|
+
} catch (e) {
|
200
|
+
return null;
|
201
|
+
}
|
202
|
+
};
|
203
|
+
|
204
|
+
let nTransformWarning = false;
|
205
|
+
const extractNTransform = body => {
|
206
|
+
// Faster: extractNTransformFunc
|
207
|
+
const nTransformFunc = getExtractFunctions([extractNTransformFunc, extractNTransformWithName], body);
|
208
|
+
if (!nTransformFunc && !nTransformWarning) {
|
209
|
+
// This is optional, so we can continue if it's not found, but it will bottleneck the download.
|
210
|
+
console.warn(
|
211
|
+
"\x1b[33mWARNING:\x1B[0m Could not parse n transform function.\n" +
|
212
|
+
`Please report this issue with the "${utils.saveDebugFile(
|
213
|
+
"base.js",
|
214
|
+
body,
|
215
|
+
)}" file on https://github.com/distubejs/ytdl-core/issues.`,
|
216
|
+
);
|
217
|
+
nTransformWarning = true;
|
218
|
+
}
|
219
|
+
return nTransformFunc;
|
220
|
+
};
|
221
|
+
|
222
|
+
/**
|
223
|
+
* Extracts the actions that should be taken to decipher a signature
|
224
|
+
* and transform the n parameter
|
225
|
+
*
|
226
|
+
* @param {string} body
|
227
|
+
* @returns {Array.<string>}
|
228
|
+
*/
|
229
|
+
exports.extractFunctions = body => [extractDecipher(body), extractNTransform(body)];
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Apply decipher and n-transform to individual format
|
233
|
+
*
|
234
|
+
* @param {Object} format
|
235
|
+
* @param {vm.Script} decipherScript
|
236
|
+
* @param {vm.Script} nTransformScript
|
237
|
+
*/
|
238
|
+
exports.setDownloadURL = (format, decipherScript, nTransformScript) => {
|
239
|
+
if (!decipherScript) return;
|
240
|
+
const decipher = url => {
|
241
|
+
const args = querystring.parse(url);
|
242
|
+
if (!args.s) return args.url;
|
243
|
+
const components = new URL(decodeURIComponent(args.url));
|
244
|
+
const context = {};
|
245
|
+
context[DECIPHER_ARGUMENT] = decodeURIComponent(args.s);
|
246
|
+
components.searchParams.set(args.sp || "sig", decipherScript.runInNewContext(context));
|
247
|
+
return components.toString();
|
248
|
+
};
|
249
|
+
const nTransform = url => {
|
250
|
+
const components = new URL(decodeURIComponent(url));
|
251
|
+
const n = components.searchParams.get("n");
|
252
|
+
if (!n || !nTransformScript) return url;
|
253
|
+
const context = {};
|
254
|
+
context[N_ARGUMENT] = n;
|
255
|
+
components.searchParams.set("n", nTransformScript.runInNewContext(context));
|
256
|
+
return components.toString();
|
257
|
+
};
|
258
|
+
const cipher = !format.url;
|
259
|
+
const url = format.url || format.signatureCipher || format.cipher;
|
260
|
+
format.url = nTransform(cipher ? decipher(url) : url);
|
261
|
+
delete format.signatureCipher;
|
262
|
+
delete format.cipher;
|
263
|
+
};
|
264
|
+
|
265
|
+
/**
|
266
|
+
* Applies decipher and n parameter transforms to all format URL's.
|
267
|
+
*
|
268
|
+
* @param {Array.<Object>} formats
|
269
|
+
* @param {string} html5player
|
270
|
+
* @param {Object} options
|
271
|
+
*/
|
272
|
+
exports.decipherFormats = async (formats, html5player, options) => {
|
273
|
+
const decipheredFormats = {};
|
274
|
+
const [decipherScript, nTransformScript] = await exports.getFunctions(html5player, options);
|
275
|
+
formats.forEach(format => {
|
276
|
+
exports.setDownloadURL(format, decipherScript, nTransformScript);
|
277
|
+
decipheredFormats[format.url] = format;
|
278
|
+
});
|
279
|
+
return decipheredFormats;
|
280
|
+
};
|
package/lib/url-utils.js
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
/**
|
2
|
+
* Get video ID.
|
3
|
+
*
|
4
|
+
* There are a few type of video URL formats.
|
5
|
+
* - https://www.youtube.com/watch?v=VIDEO_ID
|
6
|
+
* - https://m.youtube.com/watch?v=VIDEO_ID
|
7
|
+
* - https://youtu.be/VIDEO_ID
|
8
|
+
* - https://www.youtube.com/v/VIDEO_ID
|
9
|
+
* - https://www.youtube.com/embed/VIDEO_ID
|
10
|
+
* - https://music.youtube.com/watch?v=VIDEO_ID
|
11
|
+
* - https://gaming.youtube.com/watch?v=VIDEO_ID
|
12
|
+
*
|
13
|
+
* @param {string} link
|
14
|
+
* @return {string}
|
15
|
+
* @throws {Error} If unable to find a id
|
16
|
+
* @throws {TypeError} If videoid doesn't match specs
|
17
|
+
*/
|
18
|
+
const validQueryDomains = new Set([
|
19
|
+
"youtube.com",
|
20
|
+
"www.youtube.com",
|
21
|
+
"m.youtube.com",
|
22
|
+
"music.youtube.com",
|
23
|
+
"gaming.youtube.com",
|
24
|
+
]);
|
25
|
+
const validPathDomains = /^https?:\/\/(youtu\.be\/|(www\.)?youtube\.com\/(embed|v|shorts|live)\/)/;
|
26
|
+
exports.getURLVideoID = link => {
|
27
|
+
const parsed = new URL(link.trim());
|
28
|
+
let id = parsed.searchParams.get("v");
|
29
|
+
if (validPathDomains.test(link.trim()) && !id) {
|
30
|
+
const paths = parsed.pathname.split("/");
|
31
|
+
id = parsed.host === "youtu.be" ? paths[1] : paths[2];
|
32
|
+
} else if (parsed.hostname && !validQueryDomains.has(parsed.hostname)) {
|
33
|
+
throw Error("Not a YouTube domain");
|
34
|
+
}
|
35
|
+
if (!id) {
|
36
|
+
throw Error(`No video id found: "${link}"`);
|
37
|
+
}
|
38
|
+
id = id.substring(0, 11);
|
39
|
+
if (!exports.validateID(id)) {
|
40
|
+
throw TypeError(`Video id (${id}) does not match expected ` + `format (${idRegex.toString()})`);
|
41
|
+
}
|
42
|
+
return id;
|
43
|
+
};
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Gets video ID either from a url or by checking if the given string
|
47
|
+
* matches the video ID format.
|
48
|
+
*
|
49
|
+
* @param {string} str
|
50
|
+
* @returns {string}
|
51
|
+
* @throws {Error} If unable to find a id
|
52
|
+
* @throws {TypeError} If videoid doesn't match specs
|
53
|
+
*/
|
54
|
+
const urlRegex = /^https?:\/\//;
|
55
|
+
exports.getVideoID = str => {
|
56
|
+
if (exports.validateID(str)) {
|
57
|
+
return str;
|
58
|
+
} else if (urlRegex.test(str.trim())) {
|
59
|
+
return exports.getURLVideoID(str);
|
60
|
+
} else {
|
61
|
+
throw Error(`No video id found: ${str}`);
|
62
|
+
}
|
63
|
+
};
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Returns true if given id satifies YouTube's id format.
|
67
|
+
*
|
68
|
+
* @param {string} id
|
69
|
+
* @return {boolean}
|
70
|
+
*/
|
71
|
+
const idRegex = /^[a-zA-Z0-9-_]{11}$/;
|
72
|
+
exports.validateID = id => idRegex.test(id.trim());
|
73
|
+
|
74
|
+
/**
|
75
|
+
* Checks wether the input string includes a valid id.
|
76
|
+
*
|
77
|
+
* @param {string} string
|
78
|
+
* @returns {boolean}
|
79
|
+
*/
|
80
|
+
exports.validateURL = string => {
|
81
|
+
try {
|
82
|
+
exports.getURLVideoID(string);
|
83
|
+
return true;
|
84
|
+
} catch (e) {
|
85
|
+
return false;
|
86
|
+
}
|
87
|
+
};
|