Repository: streamlink/streamlink
Pull request #3584 (2021-02-25 20:09:06)

plugin.mildom: new plugin for mildom.com
Mildom is a Japanese video live streaming focused on gaming (similar to twitch) operated by DouYu Japan. I don't speak Japanese, however I came across this platform because of e-sports. This website is currently hosting [Apex Global Series Winter Circuit APAC North](https://www.ea.com/en-gb/games/apex-legends/compete/news/algs-winter-circuit) (official main tournament of Apex Legends). I also came across Mildom before through Valorant, as some tournaments specific to Japan are streamed there. I wanted to watch the Apex tournament with Streamlink, and that was my main motivation for creating the plugin. 

URL patterns:
Livestream-  [https://www.mildom.com/10707087](https://www.mildom.com/10707087)(user's page)
VOD (archive)- [https://www.mildom.com/playback/10707087/10707087-c0p1d4d2lrnb79gc0kqg](https://www.mildom.com/playback/10707087/10707087-c0p1d4d2lrnb79gc0kqg)

The API (Json based) endpoints to obtain the .m3u8 stream lists were fairly easily to discover with the browser inspector. However the url is different for livestreams and VODs.
Note: The live API only requires a extra argument aside from the user id -> __platform=web (if this is not set it will return a error)
It doesn't require any additional handshakes,cookies,etc

I impletemented checks for the API return code (0 is OK) and to check if a streamer is live (anchor_code = 11).



gravyboat: @back-to Can you re-review this when you have time please?

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 5ca4e761..ba3502e5 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -107,6 +107,7 @@ ltv_lsm_lv              ltv.lsm.lv           Yes   No    Streams may be geo-rest
 mediaklikk              - mediaklikk.hu      Yes   No    Streams may be geo-restricted to Hungary.
                         - m4sport.hu
 mediavitrina            mediavitrina.ru      Yes   No    Streams may be geo-restricted to Russia.
+mildom                  mildom.com           Yes   Yes
 mitele                  mitele.es            Yes   No    Streams may be geo-restricted to Spain.
 mjunoon                 mjunoon.tv           Yes   Yes   Streams may be geo-restricted to Pakistan.
 mrtmk                   play.mrt.com.mk      Yes   Yes   Streams may be geo-restricted to North Macedonia.
diff --git a/src/streamlink/plugins/mildom.py b/src/streamlink/plugins/mildom.py
new file mode 100644
index 00000000..50d6db42
--- /dev/null
+++ b/src/streamlink/plugins/mildom.py
@@ -0,0 +1,130 @@
+import logging
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import validate
+from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
+from streamlink.utils.url import url_concat
+
+log = logging.getLogger(__name__)
+
+
+class Mildom(Plugin):
+    _re_url = re.compile(r"""https?://(?:www\.)?mildom\.com/
+        (?:
+            playback/(\d+)(/(?P<video_id>(\d+)\-(\w+)))
+            |
+            (?P<channel_id>\d+)
+        )
+    """, re.VERBOSE)
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._re_url.match(url)
+
+    def _get_vod_streams(self, video_id):
+        data = self.session.http.get(
+            "https://cloudac.mildom.com/nonolive/videocontent/playback/getPlaybackDetail",
+            params={
+                "__platform": "web",
+                "v_id": video_id,
+            },
+            schema=validate.Schema(validate.transform(parse_json), {
+                "code": int,
+                validate.optional("message"): str,
+                validate.optional("body"): {
+                    "playback": {
+                        "video_link": [{"name": str, "url": validate.url()}],
+                    },
+                },
+            })
+        )
+        log.trace(f"{data!r}")
+        if data["code"] != 0:
+            log.debug(data.get("message", "Mildom API returned an error"))
+            return
+        for stream in data["body"]["playback"]["video_link"]:
+            yield stream["name"], HLSStream(self.session, stream["url"])
+
+    def _get_live_streams(self, channel_id):
+        # Get quality info and check if user is live1
+        data = self.session.http.get(
+            "https://cloudac.mildom.com/nonolive/gappserv/live/enterstudio",
+            params={
+                "__platform": "web",
+                "user_id": channel_id,
+            },
+            headers={"Accept-Language": "en"},
+            schema=validate.Schema(
+                validate.transform(parse_json),
+                {
+                    "code": int,
+                    validate.optional("message"): str,
+                    validate.optional("body"): {
+                        validate.optional("status"): int,
+                        "anchor_live": int,
+                        validate.optional("live_type"): int,
+                        "ext": {
+                            "cmode_params": [{
+                                "cmode": str,
+                                "name": str,
+                            }],
+                            validate.optional("live_mode"): int,
+                        },
+                    },
+                },
+            )
+        )
+        log.trace(f"{data!r}")
+        if data["code"] != 0:
+            log.debug(data.get("message", "Mildom API returned an error"))
+            return
+        if data["body"]["anchor_live"] != 11:
+            log.debug("User doesn't appear to be live")
+            return
+        qualities = []
+        for quality_info in data["body"]["ext"]["cmode_params"]:
+            qualities.append((quality_info["name"], "_" + quality_info["cmode"] if quality_info["cmode"] != "raw" else ""))
+
+        # Create stream URLs
+        data = self.session.http.get(
+            "https://cloudac.mildom.com/nonolive/gappserv/live/liveserver",
+            params={
+                "__platform": "web",
+                "user_id": channel_id,
+                "live_server_type": "hls",
+            },
+            headers={"Accept-Language": "en"},
+            schema=validate.Schema(
+                validate.transform(parse_json),
+                {
+                    "code": int,
+                    validate.optional("message"): str,
+                    validate.optional("body"): {
+                        "stream_server": validate.url(),
+                    }
+                }
+            )
+        )
+        log.trace(f"{data!r}")
+        if data["code"] != 0:
+            log.debug(data.get("message", "Mildom API returned an error"))
+            return
+        base_url = url_concat(data["body"]["stream_server"], f"{channel_id}{{}}.m3u8")
+        self.session.http.headers.update({"Referer": "https://www.mildom.com/"})
+        for quality in qualities:
+            yield quality[0], HLSStream(self.session, base_url.format(quality[1]))
+
+    def _get_streams(self):
+        match = self._re_url.match(self.url)
+        channel_id = match.group("channel_id")
+        video_id = match.group("video_id")
+        if video_id:
+            return self._get_vod_streams(video_id)
+        else:
+            return self._get_live_streams(channel_id)
+        return
+
+
+__plugin__ = Mildom


Test patch:
diff --git a/tests/plugins/test_mildom.py b/tests/plugins/test_mildom.py
new file mode 100644
index 00000000..3541a71f
--- /dev/null
+++ b/tests/plugins/test_mildom.py
@@ -0,0 +1,16 @@
+from streamlink.plugins.mildom import Mildom
+from tests.plugins import PluginCanHandleUrl
+
+
+class TestPluginCanHandleUrlMildom(PluginCanHandleUrl):
+    __plugin__ = Mildom
+
+    should_match = [
+        'https://www.mildom.com/10707087',
+        'https://www.mildom.com/playback/10707087/10707087-c0p1d4d2lrnb79gc0kqg',
+    ]
+
+    should_not_match = [
+        'https://support.mildom.com',
+        'https://www.mildom.com/ranking',
+    ]