@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/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
+ };