@oreohq/ytdl-core 4.15.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ };
@@ -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
+ };