Repository: yt-dlp/yt-dlp

Issue:
SovietsCloset broken
### Checklist

- [x] I'm reporting a bug unrelated to a specific site
- [X] I've verified that I'm running yt-dlp version **2022.06.22.1** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/ytdl-org/youtube-dl#video-url-contains-an-ampersand-and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)

### Description

I've read in the supportedsites.md that Sovietscloset and by extension it's playlist are supported but when I run it, it said it's not supported. Any clarification on this ?

### Verbose log

```shell
yt-dlp -vU https://sovietscloset.com/Among-Us
[debug] Command-line config: ['-vU', 'https://sovietscloset.com/Among-Us']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version 2022.06.22.1 [a86e01e]
[debug] Python version 3.9.13 (CPython 64bit) - macOS-12.4-x86_64-i386-64bit
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 5.0.1 (setts), ffprobe 5.0.1
[debug] Optional libraries: Cryptodome-3.14.1, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
[debug] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
Latest version: 2022.06.22.1, Current version: 2022.06.22.1
yt-dlp is up to date (2022.06.22.1)
[debug] [SovietsClosetPlaylist] Extracting URL: https://sovietscloset.com/Among-Us
[SovietsClosetPlaylist] Among-Us: Downloading webpage
ERROR: [SovietsClosetPlaylist] Among-Us: Unable to extract staticAssetsBase; please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "/usr/local/Cellar/yt-dlp/2022.6.22.1/libexec/lib/python3.9/site-packages/yt_dlp/extractor/common.py", line 647, in extract
    ie_result = self._real_extract(url)
  File "/usr/local/Cellar/yt-dlp/2022.6.22.1/libexec/lib/python3.9/site-packages/yt_dlp/extractor/sovietscloset.py", line 184, in _real_extract
    static_assets_base = self._search_regex(r'staticAssetsBase:\"(.*?)\"', webpage, 'staticAssetsBase')
  File "/usr/local/Cellar/yt-dlp/2022.6.22.1/libexec/lib/python3.9/site-packages/yt_dlp/extractor/common.py", line 1191, in _search_regex
    raise RegexNotFoundError('Unable to extract %s' % _name)
```


Discussion:
ChillingPepper: Please ping me if it breaks again.

Patch:
diff --git a/yt_dlp/extractor/sovietscloset.py b/yt_dlp/extractor/sovietscloset.py
index fc5a492a6..f1243cc49 100644
--- a/yt_dlp/extractor/sovietscloset.py
+++ b/yt_dlp/extractor/sovietscloset.py
@@ -44,7 +44,7 @@ class SovietsClosetIE(SovietsClosetBaseIE):
     _TESTS = [
         {
             'url': 'https://sovietscloset.com/video/1337',
-            'md5': '11e58781c4ca5b283307aa54db5b3f93',
+            'md5': 'bd012b04b261725510ca5383074cdd55',
             'info_dict': {
                 'id': '1337',
                 'ext': 'mp4',
@@ -69,11 +69,11 @@ class SovietsClosetIE(SovietsClosetBaseIE):
         },
         {
             'url': 'https://sovietscloset.com/video/1105',
-            'md5': '578b1958a379e7110ba38697042e9efb',
+            'md5': '89fa928f183893cb65a0b7be846d8a90',
             'info_dict': {
                 'id': '1105',
                 'ext': 'mp4',
-                'title': 'Arma 3 - Zeus Games #3',
+                'title': 'Arma 3 - Zeus Games #5',
                 'uploader': 'SovietWomble',
                 'thumbnail': r're:^https?://.*\.b-cdn\.net/c0e5e76f-3a93-40b4-bf01-12343c2eec5d/thumbnail\.jpg$',
                 'uploader': 'SovietWomble',
@@ -89,8 +89,8 @@ class SovietsClosetIE(SovietsClosetBaseIE):
                 'availability': 'public',
                 'series': 'Arma 3',
                 'season': 'Zeus Games',
-                'episode_number': 3,
-                'episode': 'Episode 3',
+                'episode_number': 5,
+                'episode': 'Episode 5',
             },
         },
     ]
@@ -122,7 +122,7 @@ def _real_extract(self, url):
         video_id = self._match_id(url)
         webpage = self._download_webpage(url, video_id)
 
-        static_assets_base = self._search_regex(r'staticAssetsBase:\"(.*?)\"', webpage, 'staticAssetsBase')
+        static_assets_base = self._search_regex(r'(/_nuxt/static/\d+)', webpage, 'staticAssetsBase')
         static_assets_base = f'https://sovietscloset.com{static_assets_base}'
 
         stream = self.parse_nuxt_jsonp(f'{static_assets_base}/video/{video_id}/payload.js', video_id, 'video')['stream']
@@ -181,7 +181,7 @@ def _real_extract(self, url):
 
         webpage = self._download_webpage(url, playlist_id)
 
-        static_assets_base = self._search_regex(r'staticAssetsBase:\"(.*?)\"', webpage, 'staticAssetsBase')
+        static_assets_base = self._search_regex(r'(/_nuxt/static/\d+)', webpage, 'staticAssetsBase')
         static_assets_base = f'https://sovietscloset.com{static_assets_base}'
 
         sovietscloset = self.parse_nuxt_jsonp(f'{static_assets_base}/payload.js', playlist_id, 'global')['games']
diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py
index 12634483e..795a4f42f 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -2652,9 +2652,14 @@ def _extract_n_function(self, video_id, player_url):
 
         if self.get_param('youtube_print_sig_code'):
             self.to_screen(f'Extracted nsig function from {player_id}:\n{func_code[1]}\n')
-
         func = jsi.extract_function_from_code(*func_code)
-        return lambda s: func([s])
+
+        def inner(s):
+            ret = func([s])
+            if ret.startswith('enhanced_except_'):
+                raise ExtractorError('Signature function returned an exception')
+            return ret
+        return inner
 
     def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=False):
         """
diff --git a/yt_dlp/jsinterp.py b/yt_dlp/jsinterp.py
index 47cca1176..d3994e90c 100644
--- a/yt_dlp/jsinterp.py
+++ b/yt_dlp/jsinterp.py
@@ -24,9 +24,9 @@
 
     '||': None,
     '&&': None,
-    '&': operator.and_,
-    '|': operator.or_,
-    '^': operator.xor,
+    '&': lambda a, b: (a or 0) & (b or 0),
+    '|': lambda a, b: (a or 0) | (b or 0),
+    '^': lambda a, b: (a or 0) ^ (b or 0),
 
     '===': operator.is_,
     '!==': operator.is_not,
@@ -45,8 +45,8 @@
     '-': lambda a, b: (a or 0) - (b or 0),
 
     '*': lambda a, b: (a or 0) * (b or 0),
-    '/': lambda a, b: (a or 0) / b,
-    '%': operator.mod,
+    '/': lambda a, b: (a or 0) / b if b else float('NaN'),
+    '%': lambda a, b: (a or 0) % b if b else float('NaN'),
 
     '**': operator.pow,
 }
@@ -54,7 +54,7 @@
 _COMP_OPERATORS = {'===', '!==', '==', '!=', '<=', '>=', '<', '>'}
 
 _MATCHING_PARENS = dict(zip('({[', ')}]'))
-_QUOTES = '\'"'
+_QUOTES = '\'"/'
 
 
 def _ternary(cndn, if_true=True, if_false=False):
@@ -77,6 +77,12 @@ def __init__(self):
         ExtractorError.__init__(self, 'Invalid continue')
 
 
+class JS_Throw(ExtractorError):
+    def __init__(self, e):
+        self.error = e
+        ExtractorError.__init__(self, f'Uncaught exception {e}')
+
+
 class LocalNameSpace(collections.ChainMap):
     def __setitem__(self, key, value):
         for scope in self.maps:
@@ -131,19 +137,24 @@ def _named_object(self, namespace, obj):
 
     @staticmethod
     def _separate(expr, delim=',', max_split=None):
+        OP_CHARS = '+-*/%&|^=<>!,;'
         if not expr:
             return
         counters = {k: 0 for k in _MATCHING_PARENS.values()}
         start, splits, pos, delim_len = 0, 0, 0, len(delim) - 1
-        in_quote, escaping = None, False
+        in_quote, escaping, after_op, in_regex_char_group = None, False, True, False
         for idx, char in enumerate(expr):
             if not in_quote and char in _MATCHING_PARENS:
                 counters[_MATCHING_PARENS[char]] += 1
             elif not in_quote and char in counters:
                 counters[char] -= 1
             elif not escaping and char in _QUOTES and in_quote in (char, None):
-                in_quote = None if in_quote else char
+                if in_quote or after_op or char != '/':
+                    in_quote = None if in_quote and not in_regex_char_group else char
+            elif in_quote == '/' and char in '[]':
+                in_regex_char_group = char == '['
             escaping = not escaping and in_quote and char == '\\'
+            after_op = not in_quote and char in OP_CHARS or (char == ' ' and after_op)
 
             if char != delim[pos] or any(counters.values()) or in_quote:
                 pos = 0
@@ -210,16 +221,22 @@ def interpret_statement(self, stmt, local_vars, allow_recursion=100):
             if should_return:
                 return ret, should_return
 
-        m = re.match(r'(?P<var>(?:var|const|let)\s)|return(?:\s+|$)', stmt)
+        m = re.match(r'(?P<var>(?:var|const|let)\s)|return(?:\s+|(?=["\'])|$)|(?P<throw>throw\s+)', stmt)
         if m:
             expr = stmt[len(m.group(0)):].strip()
+            if m.group('throw'):
+                raise JS_Throw(self.interpret_expression(expr, local_vars, allow_recursion))
             should_return = not m.group('var')
         if not expr:
             return None, should_return
 
         if expr[0] in _QUOTES:
             inner, outer = self._separate(expr, expr[0], 1)
-            inner = json.loads(js_to_json(f'{inner}{expr[0]}', strict=True))
+            if expr[0] == '/':
+                inner = inner[1:].replace('"', R'\"')
+                inner = re.compile(json.loads(js_to_json(f'"{inner}"', strict=True)))
+            else:
+                inner = json.loads(js_to_json(f'{inner}{expr[0]}', strict=True))
             if not outer:
                 return inner, should_return
             expr = self._named_object(local_vars, inner) + outer
@@ -263,21 +280,36 @@ def interpret_statement(self, stmt, local_vars, allow_recursion=100):
                 for item in self._separate(inner)])
             expr = name + outer
 
-        m = re.match(r'(?P<try>try|finally)\s*|(?:(?P<catch>catch)|(?P<for>for)|(?P<switch>switch))\s*\(', expr)
+        m = re.match(rf'''(?x)
+            (?P<try>try|finally)\s*|
+            (?P<catch>catch\s*(?P<err>\(\s*{_NAME_RE}\s*\)))|
+            (?P<switch>switch)\s*\(|
+            (?P<for>for)\s*\(|''', expr)
         if m and m.group('try'):
             if expr[m.end()] == '{':
                 try_expr, expr = self._separate_at_paren(expr[m.end():], '}')
             else:
                 try_expr, expr = expr[m.end() - 1:], ''
-            ret, should_abort = self.interpret_statement(try_expr, local_vars, allow_recursion)
-            if should_abort:
-                return ret, True
+            try:
+                ret, should_abort = self.interpret_statement(try_expr, local_vars, allow_recursion)
+                if should_abort:
+                    return ret, True
+            except JS_Throw as e:
+                local_vars['__ytdlp_exception__'] = e.error
+            except Exception as e:
+                # XXX: This works for now, but makes debugging future issues very hard
+                local_vars['__ytdlp_exception__'] = e
             ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion)
             return ret, should_abort or should_return
 
         elif m and m.group('catch'):
-            # We ignore the catch block
-            _, expr = self._separate_at_paren(expr, '}')
+            catch_expr, expr = self._separate_at_paren(expr[m.end():], '}')
+            if '__ytdlp_exception__' in local_vars:
+                catch_vars = local_vars.new_child({m.group('err'): local_vars.pop('__ytdlp_exception__')})
+                ret, should_abort = self.interpret_statement(catch_expr, catch_vars, allow_recursion)
+                if should_abort:
+                    return ret, True
+
             ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion)
             return ret, should_abort or should_return
 
@@ -390,7 +422,7 @@ def interpret_statement(self, stmt, local_vars, allow_recursion=100):
                 raise self.Exception(f'List index {idx} must be integer', expr)
             idx = int(idx)
             left_val[idx] = self._operator(
-                m.group('op'), left_val[idx], m.group('expr'), expr, local_vars, allow_recursion)
+                m.group('op'), self._index(left_val, idx), m.group('expr'), expr, local_vars, allow_recursion)
             return left_val[idx], should_return
 
         elif expr.isdigit():


Test patch:
diff --git a/test/test_youtube_signature.py b/test/test_youtube_signature.py
index 0ac4fd602..f1859a2fc 100644
--- a/test/test_youtube_signature.py
+++ b/test/test_youtube_signature.py
@@ -106,6 +106,10 @@
         'https://www.youtube.com/s/player/c81bbb4a/player_ias.vflset/en_US/base.js',
         'gre3EcLurNY2vqp94', 'Z9DfGxWP115WTg',
     ),
+    (
+        'https://www.youtube.com/s/player/1f7d5369/player_ias.vflset/en_US/base.js',
+        'batNX7sYqIJdkJ', 'IhOkL_zxbkOZBw',
+    ),
 ]