Repository: streamlink/streamlink

Issue:
Mixcloud Live
### Checklist

- [X] This is a plugin request and not a different kind of issue
- [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 requests](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+request%22)

### Description

Mixcloud.com is a website that allows DJs and other musical events to upload and store DJ sets legally. They also provide an HLS based livestreaming feature that allows for the live broadcasting of video sets. These livestreams are what I am requesting support for.

There are no login checks, and I do not know of any region checks.

Thank you!

### Input URLs

https://www.mixcloud.com/live/Dj_Bridge/
https://www.mixcloud.com/live/AnthroNorthwest/



Discussion:

Resolving patch:
diff --git a/src/streamlink/plugins/mixcloud.py b/src/streamlink/plugins/mixcloud.py
new file mode 100644
--- /dev/null
+++ b/src/streamlink/plugins/mixcloud.py
@@ -0,0 +1,76 @@
+"""
+$description British music live-streaming platform for radio shows and DJ mixes.
+$url mixcloud.com
+$type live
+"""
+
+import logging
+import re
+
+from streamlink.plugin import Plugin, pluginmatcher
+from streamlink.plugin.api import validate
+from streamlink.stream.hls import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+@pluginmatcher(re.compile(r"https?://(?:www\.)?mixcloud\.com/live/(?P<user>[^/]+)"))
+class Mixcloud(Plugin):
+    def _get_streams(self):
+        data = self.session.http.post(
+            "https://www.mixcloud.com/graphql",
+            json={
+                "query": """
+                    query streamData($user: UserLookup!) {
+                        userLookup(lookup: $user) {
+                            id
+                            displayName
+                            liveStream(isPublic: false) {
+                                name
+                                streamStatus
+                                hlsUrl
+                            }
+                        }
+                    }
+                """,
+                "variables": {"user": {"username": self.match.group("user")}}
+            },
+            schema=validate.Schema(
+                validate.parse_json(),
+                {
+                    "data": {
+                        "userLookup": validate.none_or_all(
+                            {
+                                "id": str,
+                                "displayName": str,
+                                "liveStream": {
+                                    "name": str,
+                                    "streamStatus": validate.any("ENDED", "LIVE"),
+                                    "hlsUrl": validate.none_or_all(validate.url()),
+                                },
+                            },
+                        ),
+                    },
+                },
+                validate.get(("data", "userLookup")),
+            ),
+        )
+        if not data:
+            log.error("User not found")
+            return
+
+        self.id = data.get("id")
+        self.author = data.get("displayName")
+        data = data.get("liveStream")
+
+        if data.get("streamStatus") == "ENDED":
+            log.info("This stream has ended")
+            return
+
+        self.title = data.get("name")
+
+        if data.get("hlsUrl"):
+            return HLSStream.parse_variant_playlist(self.session, data.get("hlsUrl"))
+
+
+__plugin__ = Mixcloud


Test patch:
diff --git a/tests/plugins/test_mixcloud.py b/tests/plugins/test_mixcloud.py
new file mode 100644
--- /dev/null
+++ b/tests/plugins/test_mixcloud.py
@@ -0,0 +1,21 @@
+from streamlink.plugins.mixcloud import Mixcloud
+from tests.plugins import PluginCanHandleUrl
+
+
+class TestPluginCanHandleUrlMixcloud(PluginCanHandleUrl):
+    __plugin__ = Mixcloud
+
+    should_match_groups = [
+        ("http://mixcloud.com/live/user", {"user": "user"}),
+        ("http://www.mixcloud.com/live/user", {"user": "user"}),
+        ("https://mixcloud.com/live/user", {"user": "user"}),
+        ("https://www.mixcloud.com/live/user", {"user": "user"}),
+        ("https://www.mixcloud.com/live/user/anything", {"user": "user"}),
+    ]
+
+    should_not_match = [
+        "https://www.mixcloud.com/",
+        "https://www.mixcloud.com/live",
+        "https://www.mixcloud.com/live/",
+        "https://www.mixcloud.com/other",
+    ]