turing-usp commited on
Commit
a16db5b
1 Parent(s): 229c16e

Upload monkey_patch.py

Browse files
Files changed (1) hide show
  1. monkey_patch.py +699 -0
monkey_patch.py ADDED
@@ -0,0 +1,699 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ This module contains all logic necessary to decipher the signature.
3
+
4
+ YouTube's strategy to restrict downloading videos is to send a ciphered version
5
+ of the signature to the client, along with the decryption algorithm obfuscated
6
+ in JavaScript. For the clients to play the videos, JavaScript must take the
7
+ ciphered version, cycle it through a series of "transform functions," and then
8
+ signs the media URL with the output.
9
+
10
+ This module is responsible for (1) finding and extracting those "transform
11
+ functions" (2) maps them to Python equivalents and (3) taking the ciphered
12
+ signature and decoding it.
13
+
14
+ """
15
+ import logging
16
+ import re
17
+ from itertools import chain
18
+ from typing import Any, Callable, Dict, List, Optional, Tuple
19
+
20
+ from pytube.exceptions import ExtractError, RegexMatchError
21
+ from pytube.helpers import cache, regex_search
22
+ from pytube.parser import find_object_from_startpoint, throttling_array_split
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ class CustomCipher:
27
+ def __init__(self, js: str):
28
+ self.transform_plan: List[str] = get_transform_plan(js)
29
+ var_regex = re.compile(r"^[\w\$_]+\W")
30
+ var_match = var_regex.search(self.transform_plan[0])
31
+ if not var_match:
32
+ raise RegexMatchError(
33
+ caller="__init__", pattern=var_regex.pattern
34
+ )
35
+ var = var_match.group(0)[:-1]
36
+ self.transform_map = get_transform_map(js, var)
37
+ self.js_func_patterns = [
38
+ r"\w+\.(\w+)\(\w,(\d+)\)",
39
+ r"\w+\[(\"\w+\")\]\(\w,(\d+)\)"
40
+ ]
41
+
42
+ self.throttling_plan = get_throttling_plan(js)
43
+ self.throttling_array = get_throttling_function_array(js)
44
+
45
+ self.calculated_n = None
46
+
47
+ def calculate_n(self, initial_n: list):
48
+ """Converts n to the correct value to prevent throttling."""
49
+ if self.calculated_n:
50
+ return self.calculated_n
51
+
52
+ # First, update all instances of 'b' with the list(initial_n)
53
+ for i in range(len(self.throttling_array)):
54
+ if self.throttling_array[i] == 'b':
55
+ self.throttling_array[i] = initial_n
56
+
57
+ for step in self.throttling_plan:
58
+ curr_func = self.throttling_array[int(step[0])]
59
+ if not callable(curr_func):
60
+ logger.debug(f'{curr_func} is not callable.')
61
+ logger.debug(f'Throttling array:\n{self.throttling_array}\n')
62
+ raise ExtractError(f'{curr_func} is not callable.')
63
+
64
+ first_arg = self.throttling_array[int(step[1])]
65
+
66
+ if len(step) == 2:
67
+ curr_func(first_arg)
68
+ elif len(step) == 3:
69
+ second_arg = self.throttling_array[int(step[2])]
70
+ curr_func(first_arg, second_arg)
71
+
72
+ self.calculated_n = ''.join(initial_n)
73
+ return self.calculated_n
74
+
75
+ def get_signature(self, ciphered_signature: str) -> str:
76
+ """Decipher the signature.
77
+
78
+ Taking the ciphered signature, applies the transform functions.
79
+
80
+ :param str ciphered_signature:
81
+ The ciphered signature sent in the ``player_config``.
82
+ :rtype: str
83
+ :returns:
84
+ Decrypted signature required to download the media content.
85
+ """
86
+ signature = list(ciphered_signature)
87
+
88
+ for js_func in self.transform_plan:
89
+ name, argument = self.parse_function(js_func) # type: ignore
90
+ signature = self.transform_map[name](signature, argument)
91
+ logger.debug(
92
+ "applied transform function\n"
93
+ "output: %s\n"
94
+ "js_function: %s\n"
95
+ "argument: %d\n"
96
+ "function: %s",
97
+ "".join(signature),
98
+ name,
99
+ argument,
100
+ self.transform_map[name],
101
+ )
102
+
103
+ return "".join(signature)
104
+
105
+ @cache
106
+ def parse_function(self, js_func: str) -> Tuple[str, int]:
107
+ """Parse the Javascript transform function.
108
+
109
+ Break a JavaScript transform function down into a two element ``tuple``
110
+ containing the function name and some integer-based argument.
111
+
112
+ :param str js_func:
113
+ The JavaScript version of the transform function.
114
+ :rtype: tuple
115
+ :returns:
116
+ two element tuple containing the function name and an argument.
117
+
118
+ **Example**:
119
+
120
+ parse_function('DE.AJ(a,15)')
121
+ ('AJ', 15)
122
+
123
+ """
124
+ logger.debug("parsing transform function")
125
+ for pattern in self.js_func_patterns:
126
+ regex = re.compile(pattern)
127
+ parse_match = regex.search(js_func)
128
+ if parse_match:
129
+ fn_name, fn_arg = parse_match.groups()
130
+ return fn_name, int(fn_arg)
131
+
132
+ raise RegexMatchError(
133
+ caller="parse_function", pattern="js_func_patterns"
134
+ )
135
+
136
+
137
+ def get_initial_function_name(js: str) -> str:
138
+ """Extract the name of the function responsible for computing the signature.
139
+ :param str js:
140
+ The contents of the base.js asset file.
141
+ :rtype: str
142
+ :returns:
143
+ Function name from regex match
144
+ """
145
+
146
+ function_patterns = [
147
+ r"\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
148
+ r"\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
149
+ r'(?:\b|[^a-zA-Z0-9$])(?P<sig>[a-zA-Z0-9$]{2})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)', # noqa: E501
150
+ r'(?P<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)', # noqa: E501
151
+ r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
152
+ r"\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(",
153
+ r"yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
154
+ r"\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
155
+ r"\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
156
+ r"\bc\s*&&\s*a\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
157
+ r"\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
158
+ r"\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(", # noqa: E501
159
+ ]
160
+ logger.debug("finding initial function name")
161
+ for pattern in function_patterns:
162
+ regex = re.compile(pattern)
163
+ function_match = regex.search(js)
164
+ if function_match:
165
+ logger.debug("finished regex search, matched: %s", pattern)
166
+ return function_match.group(1)
167
+
168
+ raise RegexMatchError(
169
+ caller="get_initial_function_name", pattern="multiple"
170
+ )
171
+
172
+
173
+ def get_transform_plan(js: str) -> List[str]:
174
+ """Extract the "transform plan".
175
+
176
+ The "transform plan" is the functions that the ciphered signature is
177
+ cycled through to obtain the actual signature.
178
+
179
+ :param str js:
180
+ The contents of the base.js asset file.
181
+
182
+ **Example**:
183
+
184
+ ['DE.AJ(a,15)',
185
+ 'DE.VR(a,3)',
186
+ 'DE.AJ(a,51)',
187
+ 'DE.VR(a,3)',
188
+ 'DE.kT(a,51)',
189
+ 'DE.kT(a,8)',
190
+ 'DE.VR(a,3)',
191
+ 'DE.kT(a,21)']
192
+ """
193
+ name = re.escape(get_initial_function_name(js))
194
+ pattern = r"%s=function\(\w\){[a-z=\.\(\"\)]*;(.*);(?:.+)}" % name
195
+ logger.debug("getting transform plan")
196
+ return regex_search(pattern, js, group=1).split(";")
197
+
198
+
199
+ def get_transform_object(js: str, var: str) -> List[str]:
200
+ """Extract the "transform object".
201
+
202
+ The "transform object" contains the function definitions referenced in the
203
+ "transform plan". The ``var`` argument is the obfuscated variable name
204
+ which contains these functions, for example, given the function call
205
+ ``DE.AJ(a,15)`` returned by the transform plan, "DE" would be the var.
206
+
207
+ :param str js:
208
+ The contents of the base.js asset file.
209
+ :param str var:
210
+ The obfuscated variable name that stores an object with all functions
211
+ that descrambles the signature.
212
+
213
+ **Example**:
214
+
215
+ >>> get_transform_object(js, 'DE')
216
+ ['AJ:function(a){a.reverse()}',
217
+ 'VR:function(a,b){a.splice(0,b)}',
218
+ 'kT:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}']
219
+
220
+ """
221
+ pattern = r"var %s={(.*?)};" % re.escape(var)
222
+ logger.debug("getting transform object")
223
+ regex = re.compile(pattern, flags=re.DOTALL)
224
+ transform_match = regex.search(js)
225
+ if not transform_match:
226
+ raise RegexMatchError(caller="get_transform_object", pattern=pattern)
227
+
228
+ return transform_match.group(1).replace("\n", " ").split(", ")
229
+
230
+
231
+ def get_transform_map(js: str, var: str) -> Dict:
232
+ """Build a transform function lookup.
233
+
234
+ Build a lookup table of obfuscated JavaScript function names to the
235
+ Python equivalents.
236
+
237
+ :param str js:
238
+ The contents of the base.js asset file.
239
+ :param str var:
240
+ The obfuscated variable name that stores an object with all functions
241
+ that descrambles the signature.
242
+
243
+ """
244
+ transform_object = get_transform_object(js, var)
245
+ mapper = {}
246
+ for obj in transform_object:
247
+ # AJ:function(a){a.reverse()} => AJ, function(a){a.reverse()}
248
+ name, function = obj.split(":", 1)
249
+ fn = map_functions(function)
250
+ mapper[name] = fn
251
+ return mapper
252
+
253
+
254
+ def get_throttling_function_name(js: str) -> str:
255
+ """Extract the name of the function that computes the throttling parameter.
256
+
257
+ :param str js:
258
+ The contents of the base.js asset file.
259
+ :rtype: str
260
+ :returns:
261
+ The name of the function used to compute the throttling parameter.
262
+ """
263
+ function_patterns = [
264
+ # https://github.com/ytdl-org/youtube-dl/issues/29326#issuecomment-865985377
265
+ # https://github.com/yt-dlp/yt-dlp/commit/48416bc4a8f1d5ff07d5977659cb8ece7640dcd8
266
+ # var Bpa = [iha];
267
+ # ...
268
+ # a.C && (b = a.get("n")) && (b = Bpa[0](b), a.set("n", b),
269
+ # Bpa.length || iha("")) }};
270
+ # In the above case, `iha` is the relevant function name
271
+ r'a\.[a-zA-Z]\s*&&\s*\([a-z]\s*=\s*a\.get\("n"\)\)\s*&&\s*'
272
+ r'\([a-z]\s*=\s*([a-zA-Z0-9$]+)(\[\d+\])?\([a-z]\)',
273
+ ]
274
+ logger.debug('Finding throttling function name')
275
+ for pattern in function_patterns:
276
+ regex = re.compile(pattern)
277
+ function_match = regex.search(js)
278
+ if function_match:
279
+ logger.debug("finished regex search, matched: %s", pattern)
280
+ if len(function_match.groups()) == 1:
281
+ return function_match.group(1)
282
+ idx = function_match.group(2)
283
+ if idx:
284
+ idx = idx.strip("[]")
285
+ array = re.search(
286
+ r'var {nfunc}\s*=\s*(\[.+?\]);'.format(
287
+ nfunc=re.escape(function_match.group(1))),
288
+ js
289
+ )
290
+ if array:
291
+ array = array.group(1).strip("[]").split(",")
292
+ array = [x.strip() for x in array]
293
+ return array[int(idx)]
294
+
295
+ raise RegexMatchError(
296
+ caller="get_throttling_function_name", pattern="multiple"
297
+ )
298
+
299
+
300
+ def get_throttling_function_code(js: str) -> str:
301
+ """Extract the raw code for the throttling function.
302
+
303
+ :param str js:
304
+ The contents of the base.js asset file.
305
+ :rtype: str
306
+ :returns:
307
+ The name of the function used to compute the throttling parameter.
308
+ """
309
+ # Begin by extracting the correct function name
310
+ name = re.escape(get_throttling_function_name(js))
311
+
312
+ # Identify where the function is defined
313
+ pattern_start = r"%s=function\(\w\)" % name
314
+ regex = re.compile(pattern_start)
315
+ match = regex.search(js)
316
+
317
+ # Extract the code within curly braces for the function itself, and merge any split lines
318
+ code_lines_list = find_object_from_startpoint(js, match.span()[1]).split('\n')
319
+ joined_lines = "".join(code_lines_list)
320
+
321
+ # Prepend function definition (e.g. `Dea=function(a)`)
322
+ return match.group(0) + joined_lines
323
+
324
+
325
+ def get_throttling_function_array(js: str) -> List[Any]:
326
+ """Extract the "c" array.
327
+
328
+ :param str js:
329
+ The contents of the base.js asset file.
330
+ :returns:
331
+ The array of various integers, arrays, and functions.
332
+ """
333
+ raw_code = get_throttling_function_code(js)
334
+
335
+ array_start = r",c=\["
336
+ array_regex = re.compile(array_start)
337
+ match = array_regex.search(raw_code)
338
+
339
+ array_raw = find_object_from_startpoint(raw_code, match.span()[1] - 1)
340
+ str_array = throttling_array_split(array_raw)
341
+
342
+ converted_array = []
343
+ for el in str_array:
344
+ try:
345
+ converted_array.append(int(el))
346
+ continue
347
+ except ValueError:
348
+ # Not an integer value.
349
+ pass
350
+
351
+ if el == 'null':
352
+ converted_array.append(None)
353
+ continue
354
+
355
+ if el.startswith('"') and el.endswith('"'):
356
+ # Convert e.g. '"abcdef"' to string without quotation marks, 'abcdef'
357
+ converted_array.append(el[1:-1])
358
+ continue
359
+
360
+ if el.startswith('function'):
361
+ mapper = (
362
+ (r"{for\(\w=\(\w%\w\.length\+\w\.length\)%\w\.length;\w--;\)\w\.unshift\(\w.pop\(\)\)}", throttling_unshift), # noqa:E501
363
+ (r"{\w\.reverse\(\)}", throttling_reverse),
364
+ (r"{\w\.push\(\w\)}", throttling_push),
365
+ (r";var\s\w=\w\[0\];\w\[0\]=\w\[\w\];\w\[\w\]=\w}", throttling_swap),
366
+ (r"case\s\d+", throttling_cipher_function),
367
+ (r"\w\.splice\(0,1,\w\.splice\(\w,1,\w\[0\]\)\[0\]\)", throttling_nested_splice), # noqa:E501
368
+ (r";\w\.splice\(\w,1\)}", js_splice),
369
+ (r"\w\.splice\(-\w\)\.reverse\(\)\.forEach\(function\(\w\){\w\.unshift\(\w\)}\)", throttling_prepend), # noqa:E501
370
+ (r"for\(var \w=\w\.length;\w;\)\w\.push\(\w\.splice\(--\w,1\)\[0\]\)}", throttling_reverse), # noqa:E501
371
+ )
372
+
373
+ found = False
374
+ for pattern, fn in mapper:
375
+ if re.search(pattern, el):
376
+ converted_array.append(fn)
377
+ found = True
378
+ if found:
379
+ continue
380
+
381
+ converted_array.append(el)
382
+
383
+ # Replace null elements with array itself
384
+ for i in range(len(converted_array)):
385
+ if converted_array[i] is None:
386
+ converted_array[i] = converted_array
387
+
388
+ return converted_array
389
+
390
+
391
+ def get_throttling_plan(js: str):
392
+ """Extract the "throttling plan".
393
+
394
+ The "throttling plan" is a list of tuples used for calling functions
395
+ in the c array. The first element of the tuple is the index of the
396
+ function to call, and any remaining elements of the tuple are arguments
397
+ to pass to that function.
398
+
399
+ :param str js:
400
+ The contents of the base.js asset file.
401
+ :returns:
402
+ The full function code for computing the throttlign parameter.
403
+ """
404
+ raw_code = get_throttling_function_code(js)
405
+
406
+ transform_start = r"try{"
407
+ plan_regex = re.compile(transform_start)
408
+ match = plan_regex.search(raw_code)
409
+
410
+ transform_plan_raw = find_object_from_startpoint(raw_code, match.span()[1] - 1)
411
+
412
+ # Steps are either c[x](c[y]) or c[x](c[y],c[z])
413
+ step_start = r"c\[(\d+)\]\(c\[(\d+)\](,c(\[(\d+)\]))?\)"
414
+ step_regex = re.compile(step_start)
415
+ matches = step_regex.findall(transform_plan_raw)
416
+ transform_steps = []
417
+ for match in matches:
418
+ if match[4] != '':
419
+ transform_steps.append((match[0],match[1],match[4]))
420
+ else:
421
+ transform_steps.append((match[0],match[1]))
422
+
423
+ return transform_steps
424
+
425
+
426
+ def reverse(arr: List, _: Optional[Any]):
427
+ """Reverse elements in a list.
428
+
429
+ This function is equivalent to:
430
+
431
+ .. code-block:: javascript
432
+
433
+ function(a, b) { a.reverse() }
434
+
435
+ This method takes an unused ``b`` variable as their transform functions
436
+ universally sent two arguments.
437
+
438
+ **Example**:
439
+
440
+ >>> reverse([1, 2, 3, 4])
441
+ [4, 3, 2, 1]
442
+ """
443
+ return arr[::-1]
444
+
445
+
446
+ def splice(arr: List, b: int):
447
+ """Add/remove items to/from a list.
448
+
449
+ This function is equivalent to:
450
+
451
+ .. code-block:: javascript
452
+
453
+ function(a, b) { a.splice(0, b) }
454
+
455
+ **Example**:
456
+
457
+ >>> splice([1, 2, 3, 4], 2)
458
+ [1, 2]
459
+ """
460
+ return arr[b:]
461
+
462
+
463
+ def swap(arr: List, b: int):
464
+ """Swap positions at b modulus the list length.
465
+
466
+ This function is equivalent to:
467
+
468
+ .. code-block:: javascript
469
+
470
+ function(a, b) { var c=a[0];a[0]=a[b%a.length];a[b]=c }
471
+
472
+ **Example**:
473
+
474
+ >>> swap([1, 2, 3, 4], 2)
475
+ [3, 2, 1, 4]
476
+ """
477
+ r = b % len(arr)
478
+ return list(chain([arr[r]], arr[1:r], [arr[0]], arr[r + 1 :]))
479
+
480
+
481
+ def throttling_reverse(arr: list):
482
+ """Reverses the input list.
483
+
484
+ Needs to do an in-place reversal so that the passed list gets changed.
485
+ To accomplish this, we create a reversed copy, and then change each
486
+ indvidual element.
487
+ """
488
+ reverse_copy = arr.copy()[::-1]
489
+ for i in range(len(reverse_copy)):
490
+ arr[i] = reverse_copy[i]
491
+
492
+
493
+ def throttling_push(d: list, e: Any):
494
+ """Pushes an element onto a list."""
495
+ d.append(e)
496
+
497
+
498
+ def throttling_mod_func(d: list, e: int):
499
+ """Perform the modular function from the throttling array functions.
500
+
501
+ In the javascript, the modular operation is as follows:
502
+ e = (e % d.length + d.length) % d.length
503
+
504
+ We simply translate this to python here.
505
+ """
506
+ return (e % len(d) + len(d)) % len(d)
507
+
508
+
509
+ def throttling_unshift(d: list, e: int):
510
+ """Rotates the elements of the list to the right.
511
+
512
+ In the javascript, the operation is as follows:
513
+ for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())
514
+ """
515
+ e = throttling_mod_func(d, e)
516
+ new_arr = d[-e:] + d[:-e]
517
+ d.clear()
518
+ for el in new_arr:
519
+ d.append(el)
520
+
521
+
522
+ def throttling_cipher_function(d: list, e: str):
523
+ """This ciphers d with e to generate a new list.
524
+
525
+ In the javascript, the operation is as follows:
526
+ var h = [A-Za-z0-9-_], f = 96; // simplified from switch-case loop
527
+ d.forEach(
528
+ function(l,m,n){
529
+ this.push(
530
+ n[m]=h[
531
+ (h.indexOf(l)-h.indexOf(this[m])+m-32+f--)%h.length
532
+ ]
533
+ )
534
+ },
535
+ e.split("")
536
+ )
537
+ """
538
+ h = list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_')
539
+ f = 96
540
+ # by naming it "this" we can more closely reflect the js
541
+ this = list(e)
542
+
543
+ # This is so we don't run into weirdness with enumerate while
544
+ # we change the input list
545
+ copied_list = d.copy()
546
+
547
+ for m, l in enumerate(copied_list):
548
+ bracket_val = (h.index(l) - h.index(this[m]) + m - 32 + f) % len(h)
549
+ this.append(
550
+ h[bracket_val]
551
+ )
552
+ d[m] = h[bracket_val]
553
+ f -= 1
554
+
555
+
556
+ def throttling_nested_splice(d: list, e: int):
557
+ """Nested splice function in throttling js.
558
+
559
+ In the javascript, the operation is as follows:
560
+ function(d,e){
561
+ e=(e%d.length+d.length)%d.length;
562
+ d.splice(
563
+ 0,
564
+ 1,
565
+ d.splice(
566
+ e,
567
+ 1,
568
+ d[0]
569
+ )[0]
570
+ )
571
+ }
572
+
573
+ While testing, all this seemed to do is swap element 0 and e,
574
+ but the actual process is preserved in case there was an edge
575
+ case that was not considered.
576
+ """
577
+ e = throttling_mod_func(d, e)
578
+ inner_splice = js_splice(
579
+ d,
580
+ e,
581
+ 1,
582
+ d[0]
583
+ )
584
+ js_splice(
585
+ d,
586
+ 0,
587
+ 1,
588
+ inner_splice[0]
589
+ )
590
+
591
+
592
+ def throttling_prepend(d: list, e: int):
593
+ """
594
+
595
+ In the javascript, the operation is as follows:
596
+ function(d,e){
597
+ e=(e%d.length+d.length)%d.length;
598
+ d.splice(-e).reverse().forEach(
599
+ function(f){
600
+ d.unshift(f)
601
+ }
602
+ )
603
+ }
604
+
605
+ Effectively, this moves the last e elements of d to the beginning.
606
+ """
607
+ start_len = len(d)
608
+ # First, calculate e
609
+ e = throttling_mod_func(d, e)
610
+
611
+ # Then do the prepending
612
+ new_arr = d[-e:] + d[:-e]
613
+
614
+ # And update the input list
615
+ d.clear()
616
+ for el in new_arr:
617
+ d.append(el)
618
+
619
+ end_len = len(d)
620
+ assert start_len == end_len
621
+
622
+
623
+ def throttling_swap(d: list, e: int):
624
+ """Swap positions of the 0'th and e'th elements in-place."""
625
+ e = throttling_mod_func(d, e)
626
+ f = d[0]
627
+ d[0] = d[e]
628
+ d[e] = f
629
+
630
+
631
+ def js_splice(arr: list, start: int, delete_count=None, *items):
632
+ """Implementation of javascript's splice function.
633
+
634
+ :param list arr:
635
+ Array to splice
636
+ :param int start:
637
+ Index at which to start changing the array
638
+ :param int delete_count:
639
+ Number of elements to delete from the array
640
+ :param *items:
641
+ Items to add to the array
642
+
643
+ Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice # noqa:E501
644
+ """
645
+ # Special conditions for start value
646
+ try:
647
+ if start > len(arr):
648
+ start = len(arr)
649
+ # If start is negative, count backwards from end
650
+ if start < 0:
651
+ start = len(arr) - start
652
+ except TypeError:
653
+ # Non-integer start values are treated as 0 in js
654
+ start = 0
655
+
656
+ # Special condition when delete_count is greater than remaining elements
657
+ if not delete_count or delete_count >= len(arr) - start:
658
+ delete_count = len(arr) - start # noqa: N806
659
+
660
+ deleted_elements = arr[start:start + delete_count]
661
+
662
+ # Splice appropriately.
663
+ new_arr = arr[:start] + list(items) + arr[start + delete_count:]
664
+
665
+ # Replace contents of input array
666
+ arr.clear()
667
+ for el in new_arr:
668
+ arr.append(el)
669
+
670
+ return deleted_elements
671
+
672
+
673
+ def map_functions(js_func: str) -> Callable:
674
+ """For a given JavaScript transform function, return the Python equivalent.
675
+
676
+ :param str js_func:
677
+ The JavaScript version of the transform function.
678
+ """
679
+ mapper = (
680
+ # function(a){a.reverse()}
681
+ (r"{\w\.reverse\(\)}", reverse),
682
+ # function(a,b){a.splice(0,b)}
683
+ (r"{\w\.splice\(0,\w\)}", splice),
684
+ # function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}
685
+ (r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];\w\[\w\]=\w}", swap),
686
+ # function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}
687
+ (
688
+ r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w\%\w.length\];\w\[\w\%\w.length\]=\w}",
689
+ swap,
690
+ ),
691
+ )
692
+
693
+ for pattern, fn in mapper:
694
+ if re.search(pattern, js_func):
695
+ return fn
696
+ raise RegexMatchError(caller="map_functions", pattern="multiple")
697
+
698
+ from pytube import cipher
699
+ cipher.Cipher.__init__ = CustomCipher.__init__