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:
StasYong: With the edited plugin, the error has been fixed, thnx



[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/Roper89
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=c:\Program Files (x86)\K-Lite Codec Pack\MPC-HC64\mpc-hc64.exe
[cli][debug]  --ffmpeg-ffmpeg=C:\Soft\Streamlink\ffmpeg\ffmpeg.exe
[cli][info] Found matching plugin goodgame for URL https://goodgame.ru/Roper89
[cli][info] Available streams: 240p (worst), 480p, 720p, source (best)
[cli][info] Opening stream: source (hls)
[cli][info] Starting player: c:\Program Files (x86)\K-Lite Codec Pack\MPC-HC64\mpc-hc64.exe
[stream.hls][debug] Reloading playlist
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][debug] First Sequence: 5957; Last Sequence: 5960
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 5958; End Sequence: None
[stream.hls][debug] Adding segment 5958 to queue
[stream.hls][debug] Adding segment 5959 to queue
[stream.hls][debug] Adding segment 5960 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Writing segment 5958 to output
[stream.hls][debug] Segment 5958 complete
[cli.output][debug] Opening subprocess: ['c:\\Program Files (x86)\\K-Lite Codec Pack\\MPC-HC64\\mpc-hc64.exe', '-']
[cli][debug] Writing stream to output
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 5961 to queue
[stream.hls][debug] Writing segment 5959 to output
[stream.hls][debug] Segment 5959 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 5962 to queue
[stream.hls][debug] Writing segment 5960 to output
[stream.hls][debug] Segment 5960 complete
[cli][info] Player closed
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[cli][info] Stream ended
[cli][info] Closing currently open stream...

Patch:
diff --git a/src/streamlink/plugins/goodgame.py b/src/streamlink/plugins/goodgame.py
index f599b9a5..8e985f0e 100644
--- 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
index 87af6803..6fa8e19f 100644
--- 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