Repository: streamlink/streamlink

Issue:
plugins.goodgame: Unable to parse JSON: Expecting ','
### 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.2.0

### Description

Hello,

if put full url of goodgame stream (old version of url on site) (https://goodgame.ru/channel/melharucos), then goes error "Unable to parse JSON: Expecting ',' delimiter: line 1 column 689 (char 688) ('{"id":171713,"title":"melharucos", ...)".

if put shorten url (new version of url on site) (https://goodgame.ru/melharucos), then goes error "error: No plugin can handle URL: https://goodgame.ru/melharucos".

### Debug log

```text
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.11.5
[cli][debug] OpenSSL:    OpenSSL 3.0.9 30 May 2023
[cli][debug] Streamlink: 6.2.0
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.7.22
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.18.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.22.2
[cli][debug]  trio-websocket: 0.10.4
[cli][debug]  typing-extensions: 4.7.1
[cli][debug]  urllib3: 2.0.4
[cli][debug]  websocket-client: 1.6.3
[cli][debug] Arguments:
[cli][debug]  url=https://goodgame.ru/channel/melharucos
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --ffmpeg-ffmpeg=C:\Soft\Streamlink\ffmpeg\ffmpeg.exe
[cli][info] Found matching plugin goodgame for URL https://goodgame.ru/channel/melharucos
error: Unable to parse JSON: Expecting ',' delimiter: line 1 column 689 (char 688) ('{"id":171713,"title":"melharucos", ...)


or


[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.11.5
[cli][debug] OpenSSL:    OpenSSL 3.0.9 30 May 2023
[cli][debug] Streamlink: 6.2.0
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.7.22
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.18.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.22.2
[cli][debug]  trio-websocket: 0.10.4
[cli][debug]  typing-extensions: 4.7.1
[cli][debug]  urllib3: 2.0.4
[cli][debug]  websocket-client: 1.6.3
[cli][debug] Arguments:
[cli][debug]  url=https://goodgame.ru/melharucos
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --ffmpeg-ffmpeg=C:\Soft\Streamlink\ffmpeg\ffmpeg.exe
error: No plugin can handle URL: https://goodgame.ru/melharucos
```



Discussion:
The plugin hasn't been updated in a long time and needs a rewrite. The site seems to be very simple though, so a rewrite should be fairly trivial. I'll have a look later.

Resolving patch:
diff --git a/src/streamlink/plugins/goodgame.py b/src/streamlink/plugins/goodgame.py
--- a/src/streamlink/plugins/goodgame.py
+++ b/src/streamlink/plugins/goodgame.py
@@ -1,71 +1,89 @@
 """
-$description Russian live streaming platform for live video game broadcasts.
+$description Russian live-streaming platform for live video game broadcasts.
 $url goodgame.ru
 $type live
+$metadata id
+$metadata author
+$metadata category
+$metadata title
 """
 
 import logging
 import re
+import sys
+from urllib.parse import urlparse, urlunparse
 
 from streamlink.plugin import Plugin, pluginmatcher
+from streamlink.plugin.api import validate
 from streamlink.stream.hls import HLSStream
-from streamlink.utils.parse import parse_json
 
 
 log = logging.getLogger(__name__)
 
-HLS_URL_FORMAT = "https://hls.goodgame.ru/hls/{0}{1}.m3u8"
-QUALITIES = {
-    "1080p": "",
-    "720p": "_720",
-    "480p": "_480",
-    "240p": "_240",
-}
-
-_apidata_re = re.compile(r"""(?P<quote>["']?)channel(?P=quote)\s*:\s*(?P<data>{.*?})\s*,""")
-_ddos_re = re.compile(r'document.cookie="(__DDOS_[^;]+)')
-
 
 @pluginmatcher(re.compile(
-    r"https?://(?:www\.)?goodgame\.ru/channel/(?P<user>[^/]+)",
+    r"https?://(?:www\.)?goodgame\.ru/(?:channel/)?(?P<user>[^/]+)",
 ))
 class GoodGame(Plugin):
-    def _check_stream(self, url):
-        res = self.session.http.get(url, acceptable_status=(200, 404))
-        if res.status_code == 200:
-            return True
+    @classmethod
+    def stream_weight(cls, stream):
+        if stream == "source":
+            return sys.maxsize, stream
+        return super().stream_weight(stream)
 
-    def _get_streams(self):
-        headers = {
-            "Referer": self.url,
-        }
-        res = self.session.http.get(self.url, headers=headers)
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
 
-        match = _ddos_re.search(res.text)
-        if match:
-            log.debug("Anti-DDOS bypass...")
-            headers["Cookie"] = match.group(1)
-            res = self.session.http.get(self.url, headers=headers)
+        # `/channel/user` returns different JSON data with less useful information
+        url = urlparse(self.url)
+        self.url = urlunparse(url._replace(path=re.sub(r"^/channel/", "/", url.path)))
 
-        match = _apidata_re.search(res.text)
-        channel_info = match and parse_json(match.group("data"))
-        if not channel_info:
+    def _get_streams(self):
+        data = self.session.http.get(
+            self.url,
+            schema=validate.Schema(
+                validate.parse_html(),
+                validate.xml_xpath_string(".//script[contains(text(),'channel:{')][1]/text()"),
+                re.compile(r"channel:(?P<json>{.+?}),?\n"),
+                validate.none_or_all(
+                    validate.get("json"),
+                    validate.parse_json(),
+                    {
+                        "status": bool,
+                        "id": int,
+                        "streamer": {"username": str},
+                        "game": str,
+                        "title": str,
+                        "sources": validate.all(
+                            {str: validate.url()},
+                            validate.filter(lambda k, v: ".m3u8" in v),
+                        ),
+                    },
+                    validate.union_get(
+                        "status",
+                        "id",
+                        ("streamer", "username"),
+                        "game",
+                        "title",
+                        "sources",
+                    ),
+                ),
+            ),
+        )
+
+        if not data:
             log.error("Could not find channel info")
             return
 
-        log.debug("Found channel info: id={id} channelkey={channelkey} pid={streamkey} online={status}".format(**channel_info))
-        if not channel_info["status"]:
-            log.debug("Channel appears to be offline")
-
-        streams = {}
-        for name, url_suffix in QUALITIES.items():
-            url = HLS_URL_FORMAT.format(channel_info["streamkey"], url_suffix)
-            if not self._check_stream(url):
-                continue
+        status, self.id, self.author, self.category, self.title, sources = data
 
-            streams[name] = HLSStream(self.session, url)
+        if not status:
+            log.debug("Channel appears to be offline")
+            return
 
-        return streams
+        for name, url in sources.items():
+            name = f"{name}{'p' if name.isnumeric() else ''}"
+            yield name, HLSStream(self.session, url)
 
 
 __plugin__ = GoodGame


Test patch:
diff --git a/tests/plugins/test_goodgame.py b/tests/plugins/test_goodgame.py
--- a/tests/plugins/test_goodgame.py
+++ b/tests/plugins/test_goodgame.py
@@ -1,13 +1,23 @@
+import pytest
+
 from streamlink.plugins.goodgame import GoodGame
+from streamlink.session import Streamlink
 from tests.plugins import PluginCanHandleUrl
 
 
 class TestPluginCanHandleUrlGoodGame(PluginCanHandleUrl):
     __plugin__ = GoodGame
 
-    should_match = [
-        "https://goodgame.ru/channel/ABC_ABC/#autoplay",
-        "https://goodgame.ru/channel/ABC123ABC/#autoplay",
-        "https://goodgame.ru/channel/ABC/#autoplay",
-        "https://goodgame.ru/channel/123ABC123/#autoplay",
+    should_match_groups = [
+        ("https://goodgame.ru/CHANNELNAME", {"user": "CHANNELNAME"}),
+        ("https://goodgame.ru/channel/CHANNELNAME", {"user": "CHANNELNAME"}),
     ]
+
+
+@pytest.mark.parametrize(("url", "expected"), [
+    ("https://goodgame.ru/CHANNELNAME", "https://goodgame.ru/CHANNELNAME"),
+    ("https://goodgame.ru/channel/CHANNELNAME", "https://goodgame.ru/CHANNELNAME"),
+])
+def test_url_translation(session: Streamlink, url: str, expected: str):
+    plugin = GoodGame(session, url)
+    assert plugin.url == expected