# Path: pytube/exceptions.py
# class ExtractError(PytubeError):
#     """Data extraction based exception."""
# 
# class RegexMatchError(ExtractError):
#     """Regex pattern did not return any matches."""
# 
#     def __init__(self, caller: str, pattern: Union[str, Pattern]):
#         """
#         :param str caller:
#             Calling function
#         :param str pattern:
#             Pattern that failed to match
#         """
#         super().__init__(f"{caller}: could not find match for {pattern}")
#         self.caller = caller
#         self.pattern = pattern
# 
# Path: pytube/helpers.py
# def cache(func: Callable[..., GenericType]) -> GenericType:
#     """ mypy compatible annotation wrapper for lru_cache"""
#     return functools.lru_cache()(func)  # type: ignore
# 
# def regex_search(pattern: str, string: str, group: int) -> str:
#     """Shortcut method to search a string for a given pattern.
# 
#     :param str pattern:
#         A regular expression pattern.
#     :param str string:
#         A target string to search.
#     :param int group:
#         Index of group to return.
#     :rtype:
#         str or tuple
#     :returns:
#         Substring pattern matches.
#     """
#     regex = re.compile(pattern)
#     results = regex.search(string)
#     if not results:
#         raise RegexMatchError(caller="regex_search", pattern=pattern)
# 
#     logger.debug("matched regex search: %s", pattern)
# 
#     return results.group(group)
# 
# Path: pytube/parser.py
# def find_object_from_startpoint(html, start_point):
#     """Parses input html to find the end of a JavaScript object.
# 
#     :param str html:
#         HTML to be parsed for an object.
#     :param int start_point:
#         Index of where the object starts.
#     :rtype dict:
#     :returns:
#         A dict created from parsing the object.
#     """
#     html = html[start_point:]
#     if html[0] not in ['{','[']:
#         raise HTMLParseError(f'Invalid start point. Start of HTML:\n{html[:20]}')
# 
#     # First letter MUST be a open brace, so we put that in the stack,
#     # and skip the first character.
#     stack = [html[0]]
#     i = 1
# 
#     context_closers = {
#         '{': '}',
#         '[': ']',
#         '"': '"'
#     }
# 
#     while i < len(html):
#         if len(stack) == 0:
#             break
#         curr_char = html[i]
#         curr_context = stack[-1]
# 
#         # If we've reached a context closer, we can remove an element off the stack
#         if curr_char == context_closers[curr_context]:
#             stack.pop()
#             i += 1
#             continue
# 
#         # Strings require special context handling because they can contain
#         #  context openers *and* closers
#         if curr_context == '"':
#             # If there's a backslash in a string, we skip a character
#             if curr_char == '\\':
#                 i += 2
#                 continue
#         else:
#             # Non-string contexts are when we need to look for context openers.
#             if curr_char in context_closers.keys():
#                 stack.append(curr_char)
# 
#         i += 1
# 
#     full_obj = html[:i]
#     return full_obj  # noqa: R504
# 
# def throttling_array_split(js_array):
#     """Parses the throttling array into a python list of strings.
# 
#     Expects input to begin with `[` and close with `]`.
# 
#     :param str js_array:
#         The javascript array, as a string.
#     :rtype: list:
#     :returns:
#         A list of strings representing splits on `,` in the throttling array.
#     """
#     results = []
#     curr_substring = js_array[1:]
# 
#     comma_regex = re.compile(r",")
#     func_regex = re.compile(r"function\([^)]*\)")
# 
#     while len(curr_substring) > 0:
#         if curr_substring.startswith('function'):
#             # Handle functions separately. These can contain commas
#             match = func_regex.search(curr_substring)
#             match_start, match_end = match.span()
# 
#             function_text = find_object_from_startpoint(curr_substring, match.span()[1])
#             full_function_def = curr_substring[:match_end + len(function_text)]
#             results.append(full_function_def)
#             curr_substring = curr_substring[len(full_function_def) + 1:]
#         else:
#             match = comma_regex.search(curr_substring)
# 
#             # Try-catch to capture end of array
#             try:
#                 match_start, match_end = match.span()
#             except AttributeError:
#                 match_start = len(curr_substring) - 1
#                 match_end = match_start + 1
# 
#             curr_el = curr_substring[:match_start]
#             results.append(curr_el)
#             curr_substring = curr_substring[match_end:]
# 
#     return results


import logging
import re
from itertools import chain
from typing import Any, Callable, Dict, List, Optional, Tuple
from pytube.exceptions import ExtractError, RegexMatchError
from pytube.helpers import cache, regex_search
from pytube.parser import find_object_from_startpoint, throttling_array_split
"""
This module contains all logic necessary to decipher the signature.

YouTube's strategy to restrict downloading videos is to send a ciphered version
of the signature to the client, along with the decryption algorithm obfuscated
in JavaScript. For the clients to play the videos, JavaScript must take the
ciphered version, cycle it through a series of "transform functions," and then
signs the media URL with the output.

This module is responsible for (1) finding and extracting those "transform
functions" (2) maps them to Python equivalents and (3) taking the ciphered
signature and decoding it.

"""


logger = logging.getLogger(__name__)


class Cipher:
    def __init__(self, js: str):
        self.transform_plan: List[str] = get_transform_plan(js)
        var_regex = re.compile(r"^\w+\W")
        var_match = var_regex.search(self.transform_plan[0])
        if not var_match:

# Path: pytube/exceptions.py
# class ExtractError(PytubeError):
#     """Data extraction based exception."""
# 
# class RegexMatchError(ExtractError):
#     """Regex pattern did not return any matches."""
# 
#     def __init__(self, caller: str, pattern: Union[str, Pattern]):
#         """
#         :param str caller:
#             Calling function
#         :param str pattern:
#             Pattern that failed to match
#         """
#         super().__init__(f"{caller}: could not find match for {pattern}")
#         self.caller = caller
#         self.pattern = pattern
# 
# Path: pytube/helpers.py
# def cache(func: Callable[..., GenericType]) -> GenericType:
#     """ mypy compatible annotation wrapper for lru_cache"""
#     return functools.lru_cache()(func)  # type: ignore
# 
# def regex_search(pattern: str, string: str, group: int) -> str:
#     """Shortcut method to search a string for a given pattern.
# 
#     :param str pattern:
#         A regular expression pattern.
#     :param str string:
#         A target string to search.
#     :param int group:
#         Index of group to return.
#     :rtype:
#         str or tuple
#     :returns:
#         Substring pattern matches.
#     """
#     regex = re.compile(pattern)
#     results = regex.search(string)
#     if not results:
#         raise RegexMatchError(caller="regex_search", pattern=pattern)
# 
#     logger.debug("matched regex search: %s", pattern)
# 
#     return results.group(group)
# 
# Path: pytube/parser.py
# def find_object_from_startpoint(html, start_point):
#     """Parses input html to find the end of a JavaScript object.
# 
#     :param str html:
#         HTML to be parsed for an object.
#     :param int start_point:
#         Index of where the object starts.
#     :rtype dict:
#     :returns:
#         A dict created from parsing the object.
#     """
#     html = html[start_point:]
#     if html[0] not in ['{','[']:
#         raise HTMLParseError(f'Invalid start point. Start of HTML:\n{html[:20]}')
# 
#     # First letter MUST be a open brace, so we put that in the stack,
#     # and skip the first character.
#     stack = [html[0]]
#     i = 1
# 
#     context_closers = {
#         '{': '}',
#         '[': ']',
#         '"': '"'
#     }
# 
#     while i < len(html):
#         if len(stack) == 0:
#             break
#         curr_char = html[i]
#         curr_context = stack[-1]
# 
#         # If we've reached a context closer, we can remove an element off the stack
#         if curr_char == context_closers[curr_context]:
#             stack.pop()
#             i += 1
#             continue
# 
#         # Strings require special context handling because they can contain
#         #  context openers *and* closers
#         if curr_context == '"':
#             # If there's a backslash in a string, we skip a character
#             if curr_char == '\\':
#                 i += 2
#                 continue
#         else:
#             # Non-string contexts are when we need to look for context openers.
#             if curr_char in context_closers.keys():
#                 stack.append(curr_char)
# 
#         i += 1
# 
#     full_obj = html[:i]
#     return full_obj  # noqa: R504
# 
# def throttling_array_split(js_array):
#     """Parses the throttling array into a python list of strings.
# 
#     Expects input to begin with `[` and close with `]`.
# 
#     :param str js_array:
#         The javascript array, as a string.
#     :rtype: list:
#     :returns:
#         A list of strings representing splits on `,` in the throttling array.
#     """
#     results = []
#     curr_substring = js_array[1:]
# 
#     comma_regex = re.compile(r",")
#     func_regex = re.compile(r"function\([^)]*\)")
# 
#     while len(curr_substring) > 0:
#         if curr_substring.startswith('function'):
#             # Handle functions separately. These can contain commas
#             match = func_regex.search(curr_substring)
#             match_start, match_end = match.span()
# 
#             function_text = find_object_from_startpoint(curr_substring, match.span()[1])
#             full_function_def = curr_substring[:match_end + len(function_text)]
#             results.append(full_function_def)
#             curr_substring = curr_substring[len(full_function_def) + 1:]
#         else:
#             match = comma_regex.search(curr_substring)
# 
#             # Try-catch to capture end of array
#             try:
#                 match_start, match_end = match.span()
#             except AttributeError:
#                 match_start = len(curr_substring) - 1
#                 match_end = match_start + 1
# 
#             curr_el = curr_substring[:match_start]
#             results.append(curr_el)
#             curr_substring = curr_substring[match_end:]
# 
#     return results


# Path: pytube/cipher.py
import logging
import re
from itertools import chain
from typing import Any, Callable, Dict, List, Optional, Tuple
from pytube.exceptions import ExtractError, RegexMatchError
from pytube.helpers import cache, regex_search
from pytube.parser import find_object_from_startpoint, throttling_array_split

"""
This module contains all logic necessary to decipher the signature.

YouTube's strategy to restrict downloading videos is to send a ciphered version
of the signature to the client, along with the decryption algorithm obfuscated
in JavaScript. For the clients to play the videos, JavaScript must take the
ciphered version, cycle it through a series of "transform functions," and then
signs the media URL with the output.

This module is responsible for (1) finding and extracting those "transform
functions" (2) maps them to Python equivalents and (3) taking the ciphered
signature and decoding it.

"""


logger = logging.getLogger(__name__)


class Cipher:
    def __init__(self, js: str):
        self.transform_plan: List[str] = get_transform_plan(js)
        var_regex = re.compile(r"^\w+\W")
        var_match = var_regex.search(self.transform_plan[0])
        if not var_match:

            raise RegexMatchError(