diff --git a/yt_dlp/jsinterp.py b/yt_dlp/jsinterp.py index c95a0ff57..e85371574 100644 --- a/yt_dlp/jsinterp.py +++ b/yt_dlp/jsinterp.py @@ -4,7 +4,7 @@ import json import operator import re -from .utils import ExtractorError, remove_quotes +from .utils import ExtractorError, remove_quotes, truncate_string _NAME_RE = r'[a-zA-Z_$][\w$]*' _OPERATORS = { @@ -53,6 +53,12 @@ class JSInterpreter: self.code, self._functions = code, {} self._objects = {} if objects is None else objects + class Exception(ExtractorError): + def __init__(self, msg, expr=None, *args, **kwargs): + if expr is not None: + msg += f' in: {truncate_string(expr, 50, 50)}' + super().__init__(msg, *args, **kwargs) + def _named_object(self, namespace, obj): self.__named_object_counter += 1 name = f'__yt_dlp_jsinterp_obj{self.__named_object_counter}' @@ -92,12 +98,12 @@ class JSInterpreter: def _separate_at_paren(cls, expr, delim): separated = list(cls._separate(expr, delim, 1)) if len(separated) < 2: - raise ExtractorError(f'No terminating paren {delim} in {expr}') + raise cls.Exception(f'No terminating paren {delim}', expr) return separated[0][1:].strip(), separated[1].strip() def interpret_statement(self, stmt, local_vars, allow_recursion=100): if allow_recursion < 0: - raise ExtractorError('Recursion limit reached') + raise self.Exception('Recursion limit reached') should_abort = False sub_statements = list(self._separate(stmt, ';')) or [''] @@ -177,8 +183,7 @@ class JSInterpreter: body, expr = remaining, '' start, cndn, increment = self._separate(constructor, ';') if self.interpret_statement(start, local_vars, allow_recursion - 1)[1]: - raise ExtractorError( - f'Premature return in the initialization of a for loop in {constructor!r}') + raise self.Exception('Premature return in the initialization of a for loop', constructor) while True: if not self.interpret_expression(cndn, local_vars, allow_recursion): break @@ -191,8 +196,7 @@ class JSInterpreter: except JS_Continue: pass if self.interpret_statement(increment, local_vars, allow_recursion - 1)[1]: - raise ExtractorError( - f'Premature return in the initialization of a for loop in {constructor!r}') + raise self.Exception('Premature return in the initialization of a for loop', constructor) return self.interpret_statement(expr, local_vars, allow_recursion - 1)[0] elif m and m.group('switch'): @@ -267,11 +271,11 @@ class JSInterpreter: local_vars[m.group('out')] = opfunc(left_val, right_val) return local_vars[m.group('out')] elif left_val is None: - raise ExtractorError(f'Cannot index undefined variable: {m.group("out")}') + raise self.Exception(f'Cannot index undefined variable {m.group("out")}', expr) idx = self.interpret_expression(m.group('index'), local_vars, allow_recursion) if not isinstance(idx, int): - raise ExtractorError(f'List indices must be integers: {idx}') + raise self.Exception(f'List index {idx} must be integer', expr) left_val[idx] = opfunc(left_val[idx], right_val) return left_val[idx] @@ -303,11 +307,11 @@ class JSInterpreter: left_val, should_abort = self.interpret_statement( left_val, local_vars, allow_recursion - 1) if should_abort: - raise ExtractorError(f'Premature left-side return of {op} in {expr!r}') + raise self.Exception(f'Premature left-side return of {op}', expr) right_val, should_abort = self.interpret_statement( right_val, local_vars, allow_recursion - 1) if should_abort: - raise ExtractorError(f'Premature right-side return of {op} in {expr!r}') + raise self.Exception(f'Premature right-side return of {op}', expr) return opfunc(left_val or 0, right_val) if m and m.group('attribute'): @@ -322,7 +326,7 @@ class JSInterpreter: def assertion(cndn, msg): """ assert, but without risk of getting optimized out """ if not cndn: - raise ExtractorError(f'{member} {msg}: {expr}') + raise self.Exception(f'{member} {msg}', expr) def eval_method(): if variable == 'String': @@ -349,7 +353,7 @@ class JSInterpreter: if member == 'fromCharCode': assertion(argvals, 'takes one or more arguments') return ''.join(map(chr, argvals)) - raise ExtractorError(f'Unsupported string method {member}') + raise self.Exception(f'Unsupported string method {member}', expr) if member == 'split': assertion(argvals, 'takes one or more arguments') @@ -430,7 +434,7 @@ class JSInterpreter: self._functions[fname] = self.extract_function(fname) return self._functions[fname](argvals) - raise ExtractorError(f'Unsupported JS expression {expr!r}') + raise self.Exception('Unsupported JS expression', expr) def extract_object(self, objname): _FUNC_NAME_RE = r'''(?:[a-zA-Z$0-9]+|"[a-zA-Z$0-9]+"|'[a-zA-Z$0-9]+')''' @@ -469,7 +473,7 @@ class JSInterpreter: self.code) code, _ = self._separate_at_paren(func_m.group('code'), '}') # refine the match if func_m is None: - raise ExtractorError(f'Could not find JS function "{funcname}"') + raise self.Exception(f'Could not find JS function "{funcname}"') return func_m.group('args').split(','), code def extract_function(self, funcname): diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 3a33cad2e..17d6e7335 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -5759,6 +5759,13 @@ def make_archive_id(ie, video_id): return f'{ie_key.lower()} {video_id}' +def truncate_string(s, left, right=0): + assert left > 3 and right >= 0 + if s is None or len(s) <= left + right: + return s + return f'{s[:left-3]}...{s[-right:]}' + + # Deprecated has_certifi = bool(certifi) has_websockets = bool(websockets)