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:
Thanks @bastimeyer . Very sorry that I forgot to update the title. :(
All good... In regards to the issue, there are two JSON payloads embedded in their site now, while the regex capture group was greedy, unintentionally matching the second one that comes right after the first one, including the JavaScript code, hence the JSON parsing issue of the data.



Patch:
diff --git a/src/streamlink/plugins/bloomberg.py b/src/streamlink/plugins/bloomberg.py
index 2beedc66f78..b7aeb6dfea7 100644
--- 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
index 5c113ccf078..35f09dbba8a 100644
--- 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 = [