Repository: streamlink/streamlink
Pull request #1687 (2018-05-24 11:10:03)

plugins.onetv: add support for 1tv.ru and a few other related sites
Support for 1tv.ru as requested in #1352, also supports a couple of other live streams and VOD from 1tv.ru.


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index b5116b60..6addf88d 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -140,6 +140,11 @@ ok_live                 ok.ru                Yes   --
 oldlivestream           - original.li.. [3]_ Yes   No    Only mobile streams are supported.
                         - cdn.livestream.com
 olympicchannel          olympicchannel.com   Yes   Yes   Only non-premium content is available.
+onetv                   - 1tv.ru             Yes   Yes   Streams may be geo-restricted to Russia. VOD only for 1tv.ru
+                        - ctc.ru
+                        - chetv.ru
+                        - ctclove.ru
+                        - domashny.ru
 openrectv               openrec.tv           Yes   Yes
 orf_tvthek              tvthek.orf.at        Yes   Yes
 ovvatv                  ovva.tv              Yes   No
diff --git a/src/streamlink/plugins/goodgame.py b/src/streamlink/plugins/goodgame.py
index 817cb5f2..3f64dc3d 100644
--- a/src/streamlink/plugins/goodgame.py
+++ b/src/streamlink/plugins/goodgame.py
@@ -14,7 +14,7 @@ QUALITIES = {
 }
 
 _url_re = re.compile(r"https?://(?:www\.)?goodgame.ru/channel/(?P<user>[^/]+)")
-_apidata_re = re.compile(r'''(?P<quote>["']?)channel(?P=quote)\s*:\s*(?P<data>{.*?})\s*,''')
+_apidata_re = re.compile(r'var\s+ApiData\s*=\s*({.*?})\s*;', re.MULTILINE)
 _ddos_re = re.compile(r'document.cookie="(__DDOS_[^;]+)')
 
 
@@ -41,19 +41,14 @@ class GoodGame(Plugin):
             res = http.get(self.url, headers=headers)
 
         match = _apidata_re.search(res.text)
-        channel_info = match and parse_json(match.group("data"))
-        if not channel_info:
-            self.logger.error("Could not find channel info")
+        if not match:
             return
 
-        self.logger.debug("Found channel info: channelkey={channelkey} pid={streamkey} online={status}",
-                          **channel_info)
-        if not channel_info['status']:
-            self.logger.debug("Channel appears to be offline")
-
+        stream_data = parse_json(match.group(1))
+        self.logger.debug("Found channel info for {channel_key}", **stream_data)
         streams = {}
         for name, url_suffix in QUALITIES.items():
-            url = HLS_URL_FORMAT.format(channel_info['streamkey'], url_suffix)
+            url = HLS_URL_FORMAT.format(stream_data["channel_id"], url_suffix)
             if not self._check_stream(url):
                 continue
 
diff --git a/src/streamlink/plugins/onetv.py b/src/streamlink/plugins/onetv.py
new file mode 100644
index 00000000..ad479d8f
--- /dev/null
+++ b/src/streamlink/plugins/onetv.py
@@ -0,0 +1,89 @@
+import random
+import re
+
+from streamlink.compat import urlencode, unquote, urljoin
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugin.plugin import stream_weight
+from streamlink.stream import HLSStream, HTTPStream
+from streamlink.utils import update_scheme
+
+
+class OneTV(Plugin):
+    _url_re = re.compile(r"https?://(?:www\.)?(?P<channel>1tv|ctc|chetv|ctclove|domashny).(?:com|ru)/(?P<live>live|online)?")
+    _vod_re = re.compile(r"""/video_materials\.json[^'"]*""")
+    _vod_id_re = re.compile(r'''data-video-material-id="(\d+)"''')
+
+    _1tv_api = "//stream.1tv.ru/api/playlist/1tvch_as_array.json"
+    _ctc_api = "//media.1tv.ru/api/v1/ctc/playlist/{channel}_as_array.json"
+    _session_api = "//stream.1tv.ru/get_hls_session"
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    @classmethod
+    def stream_weight(cls, stream):
+        return dict(ld=(140, "pixels"), sd=(360, "pixels"), hd=(720, "pixels")).get(stream, stream_weight(stream))
+
+    @property
+    def channel(self):
+        match = self._url_re.match(self.url)
+        return match and match.group("channel")
+
+    @property
+    def live_api_url(self):
+        channel = self.channel
+        if channel == "1tv":
+            url = self._1tv_api
+        else:
+            url = self._ctc_api.format(channel=channel)
+
+        return update_scheme(self.url, url)
+
+    def hls_session(self):
+        res = http.get(update_scheme(self.url, self._session_api))
+        data = http.json(res)
+        # the values are already quoted, we don't want them quoted
+        return {k: unquote(v) for k, v in data.items()}
+
+    @property
+    def is_live(self):
+        m = self._url_re.match(self.url)
+        return m and m.group("live") is not None
+
+    def vod_data(self, vid=None):
+        """
+        Get the VOD data path and the default VOD ID
+        :return:
+        """
+        page = http.get(self.url)
+        m = self._vod_re.search(page.text)
+        vod_data_url = m and urljoin(self.url, m.group(0))
+        if vod_data_url:
+            self.logger.debug("Found VOD data url: {0}", vod_data_url)
+            res = http.get(vod_data_url)
+            return http.json(res)
+
+
+    def _get_streams(self):
+        if self.is_live:
+            self.logger.debug("Loading live stream for {0}...", self.channel)
+
+            res = http.get(self.live_api_url, data={"r": random.randint(1, 100000)})
+            live_data = http.json(res)
+
+            for url in live_data.get("hls", [])[:1]:  # only take the first, they are all the same streams
+                url = url + '&' + urlencode(self.hls_session())  # TODO: use update_qsd
+                for s in HLSStream.parse_variant_playlist(self.session, url, name_fmt="{pixels}_{bitrate}").items():
+                    yield s
+        elif self.channel == "1tv":
+            self.logger.debug("Attempting to find VOD stream...", self.channel)
+            vod_data = self.vod_data()
+            if vod_data:
+                self.logger.info(u"Found VOD: {0}".format(vod_data[0]['title']))
+                for stream in vod_data[0]['mbr']:
+                    yield stream['name'], HTTPStream(self.session, update_scheme(self.url, stream['src']))
+
+
+__plugin__ = OneTV


Test patch:
diff --git a/tests/test_plugin_onetv.py b/tests/test_plugin_onetv.py
new file mode 100644
index 00000000..8c74d096
--- /dev/null
+++ b/tests/test_plugin_onetv.py
@@ -0,0 +1,46 @@
+import unittest
+
+from streamlink.plugins.onetv import OneTV
+
+
+class TestPluginOneTV(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            "https://www.1tv.ru/live",
+            "http://www.1tv.ru/live",
+            "http://www.1tv.ru/some-show/some-programme-2018-03-10",
+            "https://www.ctc.ru/online",
+            "http://www.ctc.ru/online",
+            "https://www.chetv.ru/online",
+            "http://www.chetv.ru/online",
+            "https://www.ctclove.ru/online",
+            "http://www.ctclove.ru/online",
+            "https://www.domashny.ru/online"
+            "http://www.domashny.ru/online"
+        ]
+        for url in should_match:
+            self.assertTrue(OneTV.can_handle_url(url))
+
+        should_not_match = [
+            "https://www.youtube.com",
+        ]
+        for url in should_not_match:
+            self.assertFalse(OneTV.can_handle_url(url))
+
+    def test_channel(self):
+        self.assertEqual(OneTV("http://1tv.ru/live").channel,
+                         "1tv")
+        self.assertEqual(OneTV("http://www.ctclove.ru/online").channel,
+                         "ctclove")
+        self.assertEqual(OneTV("http://domashny.ru/online").channel,
+                         "domashny")
+
+    def test_live_api_url(self):
+        self.assertEqual(OneTV("http://1tv.ru/live").live_api_url,
+                         "http://stream.1tv.ru/api/playlist/1tvch_as_array.json")
+
+        self.assertEqual(OneTV("https://www.ctclove.ru/online").live_api_url,
+                         "https://media.1tv.ru/api/v1/ctc/playlist/ctclove_as_array.json")
+
+        self.assertEqual(OneTV("http://domashny.ru/online").live_api_url,
+                         "http://media.1tv.ru/api/v1/ctc/playlist/domashny_as_array.json")