1 | const url = require('url');
|
2 | const miniget = require('miniget');
|
3 | const querystring = require('querystring');
|
4 |
|
5 |
|
6 |
|
7 | exports.cache = new Map();
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | exports.getTokens = async (html5playerfile, options) => {
|
18 | let cachedTokens = exports.cache.get(html5playerfile);
|
19 | if (cachedTokens) {
|
20 | return cachedTokens;
|
21 | } else {
|
22 | let [, body] = await miniget.promise(html5playerfile, options.requestOptions);
|
23 | const tokens = exports.extractActions(body);
|
24 | if (!tokens || !tokens.length) {
|
25 | throw Error('Could not extract signature deciphering actions');
|
26 | }
|
27 |
|
28 | exports.cache.set(html5playerfile, tokens);
|
29 | return tokens;
|
30 | }
|
31 | };
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | exports.decipher = (tokens, sig) => {
|
42 | sig = sig.split('');
|
43 | for (let i = 0, len = tokens.length; i < len; i++) {
|
44 | let token = tokens[i], pos;
|
45 | switch (token[0]) {
|
46 | case 'r':
|
47 | sig = sig.reverse();
|
48 | break;
|
49 | case 'w':
|
50 | pos = ~~token.slice(1);
|
51 | sig = swapHeadAndPosition(sig, pos);
|
52 | break;
|
53 | case 's':
|
54 | pos = ~~token.slice(1);
|
55 | sig = sig.slice(pos);
|
56 | break;
|
57 | case 'p':
|
58 | pos = ~~token.slice(1);
|
59 | sig.splice(0, pos);
|
60 | break;
|
61 | }
|
62 | }
|
63 | return sig.join('');
|
64 | };
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 | const swapHeadAndPosition = (arr, position) => {
|
75 | const first = arr[0];
|
76 | arr[0] = arr[position % arr.length];
|
77 | arr[position] = first;
|
78 | return arr;
|
79 | };
|
80 |
|
81 |
|
82 | const jsVarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*';
|
83 | const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`;
|
84 | const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`;
|
85 | const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`;
|
86 | const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`;
|
87 | const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`;
|
88 | const jsEmptyStr = `(?:''|"")`;
|
89 | const reverseStr = ':function\\(a\\)\\{' +
|
90 | '(?:return )?a\\.reverse\\(\\)' +
|
91 | '\\}';
|
92 | const sliceStr = ':function\\(a,b\\)\\{' +
|
93 | 'return a\\.slice\\(b\\)' +
|
94 | '\\}';
|
95 | const spliceStr = ':function\\(a,b\\)\\{' +
|
96 | 'a\\.splice\\(0,b\\)' +
|
97 | '\\}';
|
98 | const swapStr = ':function\\(a,b\\)\\{' +
|
99 | 'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' +
|
100 | '\\}';
|
101 | const actionsObjRegexp = new RegExp(
|
102 | `var (${jsVarStr})=\\{((?:(?:` +
|
103 | jsKeyStr + reverseStr + '|' +
|
104 | jsKeyStr + sliceStr + '|' +
|
105 | jsKeyStr + spliceStr + '|' +
|
106 | jsKeyStr + swapStr +
|
107 | '),?\\r?\\n?)+)\\};'
|
108 | );
|
109 | const actionsFuncRegexp = new RegExp(`function(?: ${jsVarStr})?\\(a\\)\\{` +
|
110 | `a=a\\.split\\(${jsEmptyStr}\\);\\s*` +
|
111 | `((?:(?:a=)?${jsVarStr}` +
|
112 | jsPropStr +
|
113 | '\\(a,\\d+\\);)+)' +
|
114 | `return a\\.join\\(${jsEmptyStr}\\)` +
|
115 | '\\}'
|
116 | );
|
117 | const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, 'm');
|
118 | const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, 'm');
|
119 | const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, 'm');
|
120 | const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, 'm');
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | exports.extractActions = (body) => {
|
144 | const objResult = actionsObjRegexp.exec(body);
|
145 | const funcResult = actionsFuncRegexp.exec(body);
|
146 | if (!objResult || !funcResult) { return null; }
|
147 |
|
148 | const obj = objResult[1].replace(/\$/g, '\\$');
|
149 | const objBody = objResult[2].replace(/\$/g, '\\$');
|
150 | const funcBody = funcResult[1].replace(/\$/g, '\\$');
|
151 |
|
152 | let result = reverseRegexp.exec(objBody);
|
153 | const reverseKey = result && result[1]
|
154 | .replace(/\$/g, '\\$')
|
155 | .replace(/\$|^'|^"|'$|"$/g, '');
|
156 | result = sliceRegexp.exec(objBody);
|
157 | const sliceKey = result && result[1]
|
158 | .replace(/\$/g, '\\$')
|
159 | .replace(/\$|^'|^"|'$|"$/g, '');
|
160 | result = spliceRegexp.exec(objBody);
|
161 | const spliceKey = result && result[1]
|
162 | .replace(/\$/g, '\\$')
|
163 | .replace(/\$|^'|^"|'$|"$/g, '');
|
164 | result = swapRegexp.exec(objBody);
|
165 | const swapKey = result && result[1]
|
166 | .replace(/\$/g, '\\$')
|
167 | .replace(/\$|^'|^"|'$|"$/g, '');
|
168 |
|
169 | const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`;
|
170 | const myreg = '(?:a=)?' + obj +
|
171 | `(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` +
|
172 | '\\(a,(\\d+)\\)';
|
173 | const tokenizeRegexp = new RegExp(myreg, 'g');
|
174 | const tokens = [];
|
175 | while ((result = tokenizeRegexp.exec(funcBody)) !== null) {
|
176 | let key = result[1] || result[2] || result[3];
|
177 | switch (key) {
|
178 | case swapKey:
|
179 | tokens.push('w' + result[4]);
|
180 | break;
|
181 | case reverseKey:
|
182 | tokens.push('r');
|
183 | break;
|
184 | case sliceKey:
|
185 | tokens.push('s' + result[4]);
|
186 | break;
|
187 | case spliceKey:
|
188 | tokens.push('p' + result[4]);
|
189 | break;
|
190 | }
|
191 | }
|
192 | return tokens;
|
193 | };
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 | exports.setDownloadURL = (format, sig, debug) => {
|
202 | let decodedUrl;
|
203 | if (format.url) {
|
204 | decodedUrl = format.url;
|
205 | } else {
|
206 | if (debug) {
|
207 | console.warn('Download url not found for itag ' + format.itag);
|
208 | }
|
209 | return;
|
210 | }
|
211 |
|
212 | try {
|
213 | decodedUrl = decodeURIComponent(decodedUrl);
|
214 | } catch (err) {
|
215 | if (debug) {
|
216 | console.warn('Could not decode url: ' + err.message);
|
217 | }
|
218 | return;
|
219 | }
|
220 |
|
221 |
|
222 | const parsedUrl = url.parse(decodedUrl, true);
|
223 |
|
224 |
|
225 |
|
226 | delete parsedUrl.search;
|
227 |
|
228 | let query = parsedUrl.query;
|
229 |
|
230 |
|
231 |
|
232 | query.ratebypass = 'yes';
|
233 | if (sig) {
|
234 |
|
235 |
|
236 |
|
237 | if (format.sp) {
|
238 | query[format.sp] = sig;
|
239 | } else {
|
240 | query.signature = sig;
|
241 | }
|
242 | }
|
243 |
|
244 | format.url = url.format(parsedUrl);
|
245 | };
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 | exports.decipherFormats = (formats, tokens, debug) => {
|
256 | formats.forEach((format) => {
|
257 | if (format.cipher) {
|
258 | Object.assign(format, querystring.parse(format.cipher));
|
259 | delete format.cipher;
|
260 | }
|
261 | const sig = tokens && format.s ? exports.decipher(tokens, format.s) : null;
|
262 | exports.setDownloadURL(format, sig, debug);
|
263 | });
|
264 | };
|