Repository: streamlink/streamlink

Issue:
plugins.bloomberg: error: unmatched '{' in format spec
### Checklist

- [X] This is a [plugin issue](https://streamlink.github.io/plugins.html) and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

streamlink 6.6.2

### Description

It's quite a strange error. Seems like there is a change to the JSON data return from Bloomberg, or it is corrupted.

### Debug log

```text
$ streamlink --loglevel=debug https://www.bloomberg.com/live/us
[session][debug] Loading plugin: bloomberg
[cli][debug] OS:         macOS 10.16
[cli][debug] Python:     3.9.12
[cli][debug] OpenSSL:    OpenSSL 1.1.1n  15 Mar 2022
[cli][debug] Streamlink: 6.6.2
[cli][debug] Dependencies:
[cli][debug]  certifi: 2021.10.8
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.8.0
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.19.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.27.1
[cli][debug]  trio: 0.22.2
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.1.1
[cli][debug]  urllib3: 1.26.9
[cli][debug]  websocket-client: 1.6.3
[cli][debug] Arguments:
[cli][debug]  url=https://www.bloomberg.com/live/us
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin bloomberg for URL https://www.bloomberg.com/live/us
error: unmatched '{' in format spec
```



Discussion:

Resolving patch:
diff --git a/src/streamlink/plugins/bloomberg.py b/src/streamlink/plugins/bloomberg.py
--- a/src/streamlink/plugins/bloomberg.py
+++ b/src/streamlink/plugins/bloomberg.py
@@ -16,14 +16,14 @@
 log = logging.getLogger(__name__)
 
 
-@pluginmatcher(re.compile(r"""
-    https?://(?:www\.)?bloomberg\.com/
-    (?:
-        (?P<live>live)(?:/(?P<channel>[^/]+))?
-        |
-        news/videos/[^/]+/[^/]+
-    )
-""", re.VERBOSE))
+@pluginmatcher(
+    name="live",
+    pattern=re.compile(r"https?://(?:www\.)?bloomberg\.com/live(?:/(?P<channel>[^/]+))?"),
+)
+@pluginmatcher(
+    name="vod",
+    pattern=re.compile(r"https?://(?:www\.)?bloomberg\.com/news/videos/[^/]+/[^/]+"),
+)
 class Bloomberg(Plugin):
     LIVE_API_URL = "https://cdn.gotraffic.net/projector/latest/assets/config/config.min.json?v=1"
     VOD_API_URL = "https://www.bloomberg.com/api/embed?id={0}"
@@ -106,21 +106,23 @@ def _get_vod_streams(self, data):
     def _get_streams(self):
         del self.session.http.headers["Accept-Encoding"]
 
-        try:
-            data = self.session.http.get(self.url, schema=validate.Schema(
-                validate.parse_html(),
-                validate.xml_xpath_string(".//script[contains(text(),'window.__PRELOADED_STATE__')][1]/text()"),
-                str,
-                validate.regex(re.compile(r"^\s*window\.__PRELOADED_STATE__\s*=\s*({.+})\s*;?\s*$", re.DOTALL)),
-                validate.get(1),
-                validate.parse_json(),
-            ))
-        except PluginError:
+        data = self.session.http.get(self.url, schema=validate.Schema(
+            validate.parse_html(),
+            validate.xml_xpath_string(".//script[contains(text(),'window.__PRELOADED_STATE__')][1]/text()"),
+            validate.none_or_all(
+                re.compile(r"\bwindow\.__PRELOADED_STATE__\s*=\s*(?P<json>{.+?})\s*;(?:\s|$)"),
+                validate.none_or_all(
+                    validate.get("json"),
+                    validate.parse_json(),
+                ),
+            ),
+        ))
+        if not data:
             log.error("Could not find JSON data. Invalid URL or bot protection...")
             return
 
-        if self.match.group("live"):
-            streams = self._get_live_streams(data, self.match.group("channel") or self.DEFAULT_CHANNEL)
+        if self.matches["live"]:
+            streams = self._get_live_streams(data, self.match["channel"] or self.DEFAULT_CHANNEL)
         else:
             streams = self._get_vod_streams(data)
 


Test patch:
diff --git a/tests/plugins/test_bloomberg.py b/tests/plugins/test_bloomberg.py
--- a/tests/plugins/test_bloomberg.py
+++ b/tests/plugins/test_bloomberg.py
@@ -6,11 +6,22 @@ class TestPluginCanHandleUrlBloomberg(PluginCanHandleUrl):
     __plugin__ = Bloomberg
 
     should_match_groups = [
-        ("https://www.bloomberg.com/live", {"live": "live"}),
-        ("https://www.bloomberg.com/live/", {"live": "live"}),
-        ("https://www.bloomberg.com/live/europe", {"live": "live", "channel": "europe"}),
-        ("https://www.bloomberg.com/live/europe/", {"live": "live", "channel": "europe"}),
-        ("https://www.bloomberg.com/news/videos/2022-08-10/-bloomberg-surveillance-early-edition-full-08-10-22", {}),
+        (
+            ("live", "https://www.bloomberg.com/live"),
+            {},
+        ),
+        (
+            ("live", "https://www.bloomberg.com/live/europe"),
+            {"channel": "europe"},
+        ),
+        (
+            ("live", "https://www.bloomberg.com/live/us"),
+            {"channel": "us"},
+        ),
+        (
+            ("vod", "https://www.bloomberg.com/news/videos/2022-08-10/-bloomberg-surveillance-early-edition-full-08-10-22"),
+            {},
+        ),
     ]
 
     should_not_match = [