Repository: streamlink/streamlink
Pull request #2794 (2020-02-08 04:33:57)

plugins.kugou:  Add Kugou Fanxing live plugin
  plugin for https://fanxing.kugou.com
  

codecov[bot]: # [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/2794?src=pr&el=h1) Report
> Merging [#2794](https://codecov.io/gh/streamlink/streamlink/pull/2794?src=pr&el=desc) into [master](https://codecov.io/gh/streamlink/streamlink/commit/c8e2389b802aa354741a0283971a8cb3cace334f?src=pr&el=desc) will **decrease** coverage by `<.01%`.
> The diff coverage is `43.75%`.


```diff
@@            Coverage Diff             @@
##           master    #2794      +/-   ##
==========================================
- Coverage   52.84%   52.83%   -0.01%     
==========================================
  Files         247      247              
  Lines       15460    15460              
==========================================
- Hits         8170     8169       -1     
- Misses       7290     7291       +1
```


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index e33c51bc..f4e23b13 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -111,6 +111,7 @@ ine                     ine.com              ---   Yes
 itvplayer               itv.com/itvplayer    Yes   Yes   Streams may be geo-restricted to Great Britain.
 invintus                player.invintus.com  Yes   Yes
 kingkong                kingkong.com.tw      Yes   --
+kugou                   fanxing.kugou.com    Yes   --
 latina                  latina.pe            Yes   No    Streams may be geo-restricted to Peru.
 linelive                live.line.me         Yes   Yes
 live_russia_tv          live.russia.tv       Yes   --
diff --git a/src/streamlink/plugins/kugou.py b/src/streamlink/plugins/kugou.py
new file mode 100644
index 00000000..aea7e051
--- /dev/null
+++ b/src/streamlink/plugins/kugou.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+import logging
+import re
+import time
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import validate
+from streamlink.stream import HLSStream, HTTPStream
+
+log = logging.getLogger(__name__)
+
+
+class Kugou(Plugin):
+    _url_re = re.compile(r"""https?://fanxing.kugou.com/(?P<room_id>\d+)""")
+    _roomid_re = re.compile(r"roomId:\s*'(\d+)'")
+    _room_stream_list_schema = validate.Schema(
+        {
+            "data": validate.any(None, {
+                "httpflv": validate.url()
+            })
+        },
+        validate.get("httpflv_room_stream_list_schema")
+    )
+
+    _stream_hv_schema = validate.Schema(validate.any(
+        None,
+        [{
+            "httpshls": [validate.url()],
+            "httpsflv": [validate.url()],
+        }],
+    ))
+    _stream_data_schema = validate.Schema({
+        "msg": validate.text,
+        "code": int,
+        "data": {
+            "status": int,
+            "vertical": _stream_hv_schema,
+            "horizontal": _stream_hv_schema,
+            "roomId": int,
+        }
+    })
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def _get_streams(self):
+        res = self.session.http.get(self.url)
+        m = self._roomid_re.search(res.text)
+        if m:
+            room_id = m.group(1)
+        else:
+            room_id = self._url_re.match(self.url).group("room_id")
+
+        res = self.session.http.get(
+            "https://fx2.service.kugou.com/video/pc/live/pull/v3/streamaddr",
+            params={
+                "ch": "fx",
+                "version": "1.0",
+                # 1=rtmp, 2=httpflv, 3=hls, 5=httpsflv, 6=httpshls
+                "streamType": "1-2-5-6",
+                "ua": "fx-flash",
+                "kugouId": "0",
+                "roomId": room_id,
+                "_": int(time.time()),
+            }
+        )
+        stream_data_json = self.session.http.json(res, schema=self._stream_data_schema)
+        log.trace("{0!r}".format(stream_data_json))
+        if stream_data_json["code"] != 0 or stream_data_json["data"]["status"] != 1:
+            return
+
+        h = stream_data_json["data"]["horizontal"]
+        v = stream_data_json["data"]["vertical"]
+        stream_data = h[0] if h else v[0]
+
+        if stream_data.get("httpshls"):
+            for hls_url in stream_data["httpshls"]:
+                s = HLSStream.parse_variant_playlist(self.session, hls_url)
+                if not s:
+                    yield "live", HLSStream(self.session, hls_url)
+                else:
+                    for _s in s.items():
+                        yield _s
+
+        if stream_data.get("httpsflv"):
+            for http_url in stream_data["httpsflv"]:
+                yield "live", HTTPStream(self.session, http_url)
+
+
+__plugin__ = Kugou


Test patch:
diff --git a/tests/plugins/test_kugou.py b/tests/plugins/test_kugou.py
new file mode 100644
index 00000000..3331b367
--- /dev/null
+++ b/tests/plugins/test_kugou.py
@@ -0,0 +1,24 @@
+import unittest
+
+from streamlink.plugins.kugou import Kugou
+
+
+class TestPluginKugou(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'https://fanxing.kugou.com/1062645?refer=605',
+            'https://fanxing.kugou.com/77997777?refer=605',
+            'https://fanxing.kugou.com/1047927?refer=605',
+            'https://fanxing.kugou.com/1048570?refer=605',
+            'https://fanxing.kugou.com/1062642?refer=605',
+            'https://fanxing.kugou.com/1071651',
+        ]
+        for url in should_match:
+            self.assertTrue(Kugou.can_handle_url(url), url)
+
+    def test_can_handle_url_negative(self):
+        should_not_match = [
+            'https://example.com/index.html',
+        ]
+        for url in should_not_match:
+            self.assertFalse(Kugou.can_handle_url(url), url)