Repository: streamlink/streamlink

Issue:
[plugin issue] plugin.vidio.com / can't handle stream URLs anymore
- [x] This is a bug report.
- [ ] This is a feature request.
- [ ] This is a plugin (improvement) request.
- [ ] I have read the contribution guidelines.

### Description
plugin handling of https://www.vidio.com/live  URls not working anymore due to changes at provided stream structure

### Expected / Actual behavior
streamlink -l debug www.vidio.com/live/665-rcti-tv-stream
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.5.2
[cli][debug] Streamlink: 0.12.1
[cli][debug] Requests(2.18.4), Socks(1.6.7), Websocket(0.47.0)
[cli][info] Found matching plugin vidio for URL www.vidio.com/live/665-rcti-tv-stream
[plugin.vidio][debug] HLS URL: https://kmklive-lh.akamaihd.net/i/rcti_ta_regular@94478/master.m3u8
error: Unable to open URL: https://kmklive-lh.akamaihd.net/i/rcti_ta_regular@94478/master.m3u8 (403 Client Error: Forbidden for url: https://kmklive-lh.akamaihd.net/i/rcti_ta_regular@94478/master.m3u8)


Comments:

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index b13dd541..6a472d95 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -111,6 +111,7 @@ idf1                idf1.fr              Yes   Yes
 ine                 ine.com              ---   Yes
 itvplayer           itv.com/itvplayer    Yes   Yes   Streams may be geo-restricted to Great Britain.
 kanal7              kanal7.com           Yes   No
+kingkong            www.kingkong.com.tw  Yes   Yes
 liveedu             - liveedu.tv         Yes   --    Some streams require a login.
                     - livecoding.tv
 liveme              liveme.com           Yes   --
diff --git a/src/streamlink/plugins/kingkong.py b/src/streamlink/plugins/kingkong.py
new file mode 100644
index 00000000..e50b8144
--- /dev/null
+++ b/src/streamlink/plugins/kingkong.py
@@ -0,0 +1,98 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http, validate
+from streamlink.stream import HTTPStream, HLSStream
+
+API_URL = "https://g-api.langlive.com/webapi/v1/room/info?room_id={0}"
+VOD_API_URL = (
+    "https://g-api.langlive.com/webapi/v1/replayer/detail?live_id={0}")
+STATUS_ONLINE = 1
+STATUS_OFFLINE = 0
+STREAM_WEIGHTS = {
+    "360P": 360,
+    "480P": 480,
+    "720P": 720,
+    "source": 1080
+}
+
+_url_re = re.compile(r"""
+    https://www\.kingkong\.com\.tw/
+    (?:
+        video/(?P<vid>[0-9]+G[0-9A-Za-z]+)|
+        (?P<channel>[0-9]+)
+    )
+""", re.VERBOSE)
+
+_room_schema = validate.Schema(
+    {
+        "data": {
+            "live_info": {
+                "live_status": int,
+                "stream_items": [{
+                    "title": validate.text,
+                    "video": validate.any('', validate.url(
+                        scheme="https",
+                        path=validate.endswith(".flv")
+                    ))
+                }]
+            }
+        }
+    },
+    validate.get("data")
+)
+
+_vod_schema = validate.Schema(
+    {
+        "data": {
+            "live_info": {
+                "video": validate.text
+            }
+        }
+    },
+    validate.get("data")
+)
+
+
+class Kingkong(Plugin):
+    @classmethod
+    def can_handle_url(cls, url):
+        return _url_re.match(url)
+
+    @classmethod
+    def stream_weight(cls, stream):
+        if stream in STREAM_WEIGHTS:
+            return STREAM_WEIGHTS[stream], "kingkong"
+        return Plugin.stream_weight(stream)
+
+    def _get_streams(self):
+        match = _url_re.match(self.url)
+        vid = match.group("vid")
+
+        if vid:
+            res = http.get(VOD_API_URL.format(vid))
+            data = http.json(res, schema=_vod_schema)
+            yield "source", HLSStream(
+                self.session, data["live_info"]["video"])
+            return
+
+        channel = match.group("channel")
+        res = http.get(API_URL.format(channel))
+        room = http.json(res, schema=_room_schema)
+        if not room:
+            self.logger.info("Not a valid room url.")
+            return
+
+        live_info = room["live_info"]
+        if live_info["live_status"] != STATUS_ONLINE:
+            self.logger.info("Stream currently unavailable.")
+            return
+
+        for item in live_info["stream_items"]:
+            quality = item["title"]
+            if quality == u"\u6700\u4f73":  # "Best" in Chinese
+                quality = "source"
+            yield quality, HTTPStream(self.session, item["video"])
+
+
+__plugin__ = Kingkong
diff --git a/src/streamlink/plugins/vidio.py b/src/streamlink/plugins/vidio.py
index fe5f61aa..a465dfb2 100644
--- a/src/streamlink/plugins/vidio.py
+++ b/src/streamlink/plugins/vidio.py
@@ -1,36 +1,61 @@
-'''
+"""
 Plugin for vidio.com
 - https://www.vidio.com/live/5075-dw-tv-stream
 - https://www.vidio.com/watch/766861-5-rekor-fantastis-zidane-bersama-real-madrid
-'''
+"""
 import re
 
 from streamlink.plugin import Plugin
-from streamlink.plugin.api import http
+from streamlink.plugin.api import http, useragents, validate
 from streamlink.stream import HLSStream
-
-_url_re = re.compile(r"https?://(?:www\.)?vidio\.com/(?:en/)?(?P<type>live|watch)/(?P<id>\d+)-(?P<name>[^/?#&]+)")
-_playlist_re = re.compile(r'''hls-url=["'](?P<url>[^"']+)["']''')
+from streamlink.utils import parse_json
 
 
 class Vidio(Plugin):
+    _url_re = re.compile(r"https?://(?:www\.)?vidio\.com/(?:en/)?(?P<type>live|watch)/(?P<id>\d+)-(?P<name>[^/?#&]+)")
+    _playlist_re = re.compile(r'''hls-url=["'](?P<url>[^"']+)["']''')
+    _data_id_re = re.compile(r'''meta\s+data-id=["'](?P<id>[^"']+)["']''')
+
+    csrf_tokens_url = "https://www.vidio.com/csrf_tokens"
+    tokens_url = "https://www.vidio.com/live/{id}/tokens"
+    token_schema = validate.Schema(validate.transform(parse_json),
+                                   {"token": str},
+                                   validate.get("token"))
+
     @classmethod
     def can_handle_url(cls, url):
-        return _url_re.match(url)
+        return cls._url_re.match(url)
+
+    def get_csrf_tokens(self):
+        return http.get(self.csrf_tokens_url,
+                        schema=self.token_schema)
+
+    def get_url_tokens(self, stream_id):
+        self.logger.debug("Getting stream tokens")
+        csrf_token = self.get_csrf_tokens()
+        return http.post(self.tokens_url.format(id=stream_id),
+                         files={"authenticity_token": (None, csrf_token)},
+                         headers={"User-Agent": useragents.CHROME,
+                                  "Referer": self.url},
+                         schema=self.token_schema)
 
     def _get_streams(self):
         res = http.get(self.url)
 
-        match = _playlist_re.search(res.text)
-        if match is None:
-            return
+        plmatch = self._playlist_re.search(res.text)
+        idmatch = self._data_id_re.search(res.text)
+
+        hls_url = plmatch and plmatch.group("url")
+        stream_id = idmatch and idmatch.group("id")
 
-        url = match.group('url')
+        tokens = self.get_url_tokens(stream_id)
 
-        if url:
-            self.logger.debug('HLS URL: {0}'.format(url))
-            for s in HLSStream.parse_variant_playlist(self.session, url).items():
-                yield s
+        if hls_url:
+            self.logger.debug("HLS URL: {0}".format(hls_url))
+            self.logger.debug("Tokens: {0}".format(tokens))
+            return HLSStream.parse_variant_playlist(self.session, hls_url+"?"+tokens,
+                                                    headers={"User-Agent": useragents.CHROME,
+                                                             "Referer": self.url})
 
 
 __plugin__ = Vidio


Test patch:
diff --git a/tests/test_plugin_kingkong.py b/tests/test_plugin_kingkong.py
new file mode 100644
index 00000000..311041bb
--- /dev/null
+++ b/tests/test_plugin_kingkong.py
@@ -0,0 +1,17 @@
+import unittest
+from streamlink.plugins.kingkong import Kingkong
+
+
+class TestPluginKingkong(unittest.TestCase):
+    def test_can_handle_url(self):
+        # Valid stream and VOD URLS
+        self.assertTrue(Kingkong.can_handle_url(
+            "https://www.kingkong.com.tw/600000"))
+        self.assertTrue(Kingkong.can_handle_url(
+            "https://www.kingkong.com.tw/video/2152350G38400MJTU"))
+
+        # Others
+        self.assertFalse(Kingkong.can_handle_url(
+            "https://www.kingkong.com.tw/category/lol"))
+        self.assertFalse(Kingkong.can_handle_url(
+            "https://www.kingkong.com.tw/videos/2152350"))