Repository: streamlink/streamlink

Issue:
plugins.qq: 403 Client Error: Forbidden for url 
### Checklist

- [X] This is a bug report 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 bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

6.5.0

### Description

seems like i am getting 403 Forbidden when trying to use streamlink for qq.com stream. is this a geo-block issue, as in i need to be in China to be able to access the stream or is the plugin outdated somehow? 

### Debug log

```text
streamlink --loglevel=debug https://live.qq.com/10111543 best
[cli][debug] OS:         macOS 12.4
[cli][debug] Python:     3.10.5
[cli][debug] OpenSSL:    OpenSSL 1.1.1n  15 Mar 2022
[cli][debug] Streamlink: 6.5.0
[cli][debug] Dependencies:
[cli][debug]  certifi: 2022.9.14
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.2
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.17
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.28.1
[cli][debug]  trio: 0.22.0
[cli][debug]  trio-websocket: 0.10.2
[cli][debug]  typing-extensions: 4.3.0
[cli][debug]  urllib3: 1.26.12
[cli][debug]  websocket-client: 1.4.1
[cli][debug] Arguments:
[cli][debug]  url=https://live.qq.com/10111543
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin qq for URL https://live.qq.com/10111543
[cli][info] Available streams: live (worst, best)
[cli][info] Opening stream: live (hls)
[cli][info] Starting player: /opt/homebrew/Caskroom/vlc/3.0.18/vlc.wrapper.sh
[stream.hls][debug] Reloading playlist
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][error] Unable to open URL: https://hls-broadcast-bd1.qiecdn.com/live/10111543rySdGBcY.m3u8?timestamp=<redacted>&secret=<redacted>&token=<redacted> (403 Client Error: Forbidden for url: https://hls-broadcast-bd1.qiecdn.com/live/10111543rySdGBcY.m3u8?timestamp=<redacted>&secret=<redacted>&token=<redacted>)
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[cli][error] Try 1/1: Could not open stream <HLSStream ['hls', 'https://hls-broadcast-bd1.qiecdn.com/live/10111543rySdGBcY.m3u8?timestamp=<redacted>&secret=<redacted>&token=<redacted>']> (No data returned from stream)
error: Could not open stream <HLSStream ['hls', 'https://hls-broadcast-bd1.qiecdn.com/live/10111543rySdGBcY.m3u8?timestamp=<redacted>&secret=<redacted>&token=<redacted>']>, tried 1 times, exiting
[cli][info] Closing currently open stream...
```



Discussion:
i can access the streams fine on a browser, if that matters for whether it is a geo-block or not.
This site now uses a different API endpoint for getting the stream URL, which is a progressive HTTP stream using an FLV container (why is this always the case with Asian streaming sites?).

Those stream URLs however include a signature parameter which is calculated somewhere deep in their minified/obfuscated JS code, and from what it looks like according to my debugging so far, web assembly is involved here too.

These are the relevant code parts:

- https://static.qiecdn.com/static-next/_next/static/chunks/313.b7190cb8f095d56ab52b.js (entry point is the `m(n, t)` function with `n` being the channel ID and `t` some other integer)
- https://static.qiecdn.com/static-next/_next/static/wasm/1ef6d28bf63c8ccf0cf8.wasm (the `var u=r("oZrZ")` reference)

I don't think it's worth trying to fix this plugin. This is way too much effort to debug this, understand the code, rewrite it in python and hope that they don't make any changes. The only viable alternative I see is implementing Streamlink's webbrowser plugin and trying to intercept the stream URL from the browser's network requests, but this plugin is not worth the effort considering the number of actual users.

Resolving patch:
diff --git a/src/streamlink/plugins/qq.py b/src/streamlink/plugins/qq.py
deleted file mode 100644
--- a/src/streamlink/plugins/qq.py
+++ /dev/null
@@ -1,81 +0,0 @@
-"""
-$description Chinese live-streaming platform for live video game broadcasts and live sports game related streams.
-$url live.qq.com
-$type live
-$metadata id
-$metadata author
-$metadata category
-$metadata title
-"""
-
-import logging
-import re
-
-from streamlink.plugin import Plugin, pluginmatcher
-from streamlink.plugin.api import validate
-from streamlink.stream.hls import HLSStream
-from streamlink.utils.url import update_scheme
-
-
-log = logging.getLogger(__name__)
-
-
-@pluginmatcher(re.compile(
-    r"https?://(m\.)?live\.qq\.com/(?P<room_id>\d+)?",
-))
-class QQ(Plugin):
-    _URL_API = "https://live.qq.com/api/h5/room"
-
-    def _get_streams(self):
-        room_id = self.match.group("room_id")
-
-        if not room_id:
-            room_id = self.session.http.get(self.url, schema=validate.Schema(
-                validate.parse_html(),
-                validate.xml_xpath_string(".//*[@data-rid][1]/@data-rid"),
-            ))
-        if not room_id:
-            return
-
-        error, data = self.session.http.get(
-            self._URL_API,
-            params={"room_id": room_id},
-            schema=validate.Schema(
-                validate.parse_json(),
-                validate.any(
-                    validate.all(
-                        {
-                            "error": 0,
-                            "data": {
-                                "nickname": str,
-                                "game_name": str,
-                                "room_name": str,
-                                "hls_url": validate.url(path=validate.endswith(".m3u8")),
-                            },
-                        },
-                    ),
-                    validate.all(
-                        {
-                            "error": int,
-                            "data": str,
-                        },
-                    ),
-                ),
-                validate.union_get("error", "data"),
-            ),
-        )
-        if error != 0:
-            log.error(data)
-            return
-
-        self.id = room_id
-        self.author = data["nickname"]
-        self.category = data["game_name"]
-        self.title = data["room_name"]
-
-        hls_url = update_scheme("https://", data["hls_url"], force=True)
-
-        return {"live": HLSStream(self.session, hls_url)}
-
-
-__plugin__ = QQ


Test patch:
diff --git a/tests/plugins/test_qq.py b/tests/plugins/test_qq.py
deleted file mode 100644
--- a/tests/plugins/test_qq.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from streamlink.plugins.qq import QQ
-from tests.plugins import PluginCanHandleUrl
-
-
-class TestPluginCanHandleUrlQQ(PluginCanHandleUrl):
-    __plugin__ = QQ
-
-    should_match_groups = [
-        ("https://live.qq.com/", {}),
-        ("https://live.qq.com/123456789", {"room_id": "123456789"}),
-        ("https://m.live.qq.com/123456789", {"room_id": "123456789"}),
-    ]