Repository: streamlink/streamlink
Pull request #1378 (2017-12-15 21:19:09)

[canalplus] Update plugin according to website changes
	C+ has centralized its replay services in mycanal.fr, and remove the old ones (c8.fr, cstar.fr).
	Due to changes, this plugin cannot handle live url (for the moment).


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index fee4b3a5..7b6be221 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -43,9 +43,7 @@ brittv              brittv.co.uk         Yes   --
 btv                 btv.bg               Yes   No    Requires login, and geo-restricted to Bulgaria.
 cam4                cam4.com             Yes   No
 camsoda             camsoda.com          Yes   No
-canalplus           - canalplus.fr       Yes   Yes   Streams may be geo-restricted to France.
-                    - c8.fr
-                    - cstar.fr
+canalplus           - mycanal.fr         No    Yes   Streams may be geo-restricted to France.
                     - cnews.fr
 canlitv             - canlitv.com        Yes   --
                     - canlitv.life
@@ -157,6 +155,7 @@ pluzz               - france.tv          Yes   Yes   Streams may be geo-restrict
                     - zouzous.fr
                     - france3-reg.. [8]_
 powerapp            powerapp.com.tr      Yes   No
+qq                  live.qq.com          Yes   No
 radionet            - radio.net          Yes   --
                     - radio.at
                     - radio.de
diff --git a/src/streamlink/plugins/canalplus.py b/src/streamlink/plugins/canalplus.py
index 0d21a61f..32c67713 100644
--- a/src/streamlink/plugins/canalplus.py
+++ b/src/streamlink/plugins/canalplus.py
@@ -6,17 +6,16 @@ from streamlink.stream import HDSStream, HLSStream, HTTPStream
 
 
 class CanalPlus(Plugin):
-    API_URL = 'http://service.canal-plus.com/video/rest/getVideos/{0}/{1}?format=json'
-    CHANNEL_MAP = {'canalplus': 'cplus', 'c8': 'd8', 'cstar': 'd17', 'cnews': 'itele'}
+    # NOTE : no live url for the moment
+    API_URL = 'https://secure-service.canal-plus.com/video/rest/getVideos/cplus/{0}?format=json'
     HDCORE_VERSION = '3.1.0'
     # Secret parameter needed to download HTTP videos on canalplus.fr
     SECRET = 'pqzerjlsmdkjfoiuerhsdlfknaes'
 
     _url_re = re.compile(r'''
-        http://
+        (https|http)://
         (
-            www\.(?P<channel>canalplus|c8|cstar)\.fr/(direct|.*pid.+?\.html(\?(?P<video_id>[0-9]+))?) |
-            replay\.(?P<replay_channel>c8|cstar)\.fr/video/(?P<replay_video_id>[0-9]+) |
+            www.mycanal.fr/(.*)/(.*)/p/(?P<video_id>[0-9]+) |
             www\.cnews\.fr/.+
         )
 ''', re.VERBOSE)
@@ -45,8 +44,7 @@ class CanalPlus(Plugin):
     def _get_streams(self):
         # Get video ID and channel from URL
         match = self._url_re.match(self.url)
-        channel = match.group('channel') or match.group('replay_channel') or 'cnews'
-        video_id = match.group('video_id') or match.group('replay_video_id')
+        video_id = match.group('video_id')
         if video_id is None:
             # Retrieve URL page and search for video ID
             res = http.get(self.url)
@@ -55,7 +53,7 @@ class CanalPlus(Plugin):
                 return
             video_id = match.group('video_id')
 
-        res = http.get(self.API_URL.format(self.CHANNEL_MAP[channel], video_id))
+        res = http.get(self.API_URL.format(video_id))
         videos = http.json(res, schema=self._api_schema)
         parsed = []
         headers = {'User-Agent': self._user_agent}
diff --git a/src/streamlink/plugins/qq.py b/src/streamlink/plugins/qq.py
new file mode 100644
index 00000000..ff291b84
--- /dev/null
+++ b/src/streamlink/plugins/qq.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+import re
+
+from streamlink.exceptions import NoStreamsError
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
+from streamlink.plugin.api.utils import parse_json
+from streamlink.stream import HLSStream
+
+
+class QQ(Plugin):
+    """Streamlink Plugin for live.qq.com"""
+
+    _data_schema = validate.Schema(
+        {
+            "data": {
+                "hls_url": validate.text
+            }
+        },
+        validate.get("data", {}),
+        validate.get("hls_url")
+    )
+
+    api_url = "http://live.qq.com/api/h5/room?room_id={0}"
+
+    _data_re = re.compile(r"""(?P<data>{.+})""")
+    _url_re = re.compile(r"""https?://(m\.)?live\.qq\.com/(?P<room_id>\d+)""")
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url)
+
+    def _get_streams(self):
+        match = self._url_re.match(self.url)
+        if not match:
+            return
+
+        room_id = match.group("room_id")
+        res = http.get(self.api_url.format(room_id))
+
+        data = self._data_re.search(res.text)
+        if not data:
+            return
+
+        try:
+            hls_url = parse_json(data.group("data"), schema=self._data_schema)
+        except Exception as e:
+            raise NoStreamsError(self.url)
+
+        self.logger.debug("URL={0}".format(hls_url))
+        return {"live": HLSStream(self.session, hls_url)}
+
+
+__plugin__ = QQ


Test patch:
diff --git a/tests/test_plugin_canalplus.py b/tests/test_plugin_canalplus.py
index ca2f499f..62daf272 100644
--- a/tests/test_plugin_canalplus.py
+++ b/tests/test_plugin_canalplus.py
@@ -6,21 +6,13 @@ from streamlink.plugins.canalplus import CanalPlus
 class TestPluginCanalPlus(unittest.TestCase):
     def test_can_handle_url(self):
         # should match
-        self.assertTrue(CanalPlus.can_handle_url("http://www.canalplus.fr/pid3580-live-tv-clair.html"))
-        self.assertTrue(CanalPlus.can_handle_url("http://www.canalplus.fr/emissions/pid8596-the-tonight-show.html"))
-        self.assertTrue(CanalPlus.can_handle_url("http://www.canalplus.fr/c-divertissement/pid1787-c-groland.html?vid=1430239"))
-        self.assertTrue(CanalPlus.can_handle_url("http://www.c8.fr/pid5323-c8-live.html"))
-        self.assertTrue(CanalPlus.can_handle_url("http://www.c8.fr/c8-divertissement/pid8758-c8-la-folle-histoire-de-sophie-marceau.html"))
-        self.assertTrue(CanalPlus.can_handle_url("http://www.c8.fr/c8-sport/pid5224-c8-direct-auto.html?vid=1430292"))
-        self.assertTrue(CanalPlus.can_handle_url("http://replay.c8.fr/video/1431076"))
-        self.assertTrue(CanalPlus.can_handle_url("http://www.cstar.fr/pid5322-cstar-live.html"))
-        self.assertTrue(CanalPlus.can_handle_url("http://www.cstar.fr/emissions/pid8754-wild-transport.html"))
-        self.assertTrue(CanalPlus.can_handle_url("http://www.cstar.fr/musique/pid6282-les-tops.html?vid=1430143"))
-        self.assertTrue(CanalPlus.can_handle_url("http://replay.cstar.fr/video/1430245"))
+        self.assertTrue(CanalPlus.can_handle_url("https://www.mycanal.fr/docus-infos/l-info-du-vrai-du-13-12-politique-les-affaires-reprennent/p/1473830"))
+        self.assertTrue(CanalPlus.can_handle_url("https://www.mycanal.fr/sport/infosport-laurey-et-claudia/p/1473752"))
+        self.assertTrue(CanalPlus.can_handle_url("https://www.mycanal.fr/docus-infos/ses-debuts-a-madrid-extrait-le-k-benzema/p/1469050"))
+        self.assertTrue(CanalPlus.can_handle_url("https://www.mycanal.fr/d8-docs-mags/au-revoir-johnny-hallyday-le-doc/p/1473054"))
         self.assertTrue(CanalPlus.can_handle_url("http://www.cnews.fr/direct"))
         self.assertTrue(CanalPlus.can_handle_url("http://www.cnews.fr/politique/video/des-electeurs-toujours-autant-indecis-174769"))
         self.assertTrue(CanalPlus.can_handle_url("http://www.cnews.fr/magazines/plus-de-recul/de-recul-du-14042017-174594"))
-
         # shouldn't match
         self.assertFalse(CanalPlus.can_handle_url("http://www.canalplus.fr/"))
         self.assertFalse(CanalPlus.can_handle_url("http://www.c8.fr/"))
diff --git a/tests/test_plugin_qq.py b/tests/test_plugin_qq.py
new file mode 100644
index 00000000..261740a4
--- /dev/null
+++ b/tests/test_plugin_qq.py
@@ -0,0 +1,41 @@
+import unittest
+
+from streamlink.plugins.qq import QQ
+
+
+class TestPluginQQ(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            "http://live.qq.com/10003715",
+            "http://live.qq.com/10007266",
+            "http://live.qq.com/10039165",
+            "http://m.live.qq.com/10003715",
+            "http://m.live.qq.com/10007266",
+            "http://m.live.qq.com/10039165"
+        ]
+        for url in should_match:
+            self.assertTrue(QQ.can_handle_url(url))
+
+        should_not_match = [
+            "http://live.qq.com/",
+            "http://qq.com/",
+            "http://www.qq.com/"
+        ]
+        for url in should_not_match:
+            self.assertFalse(QQ.can_handle_url(url))
+
+    def test_url_re(self):
+        regex_match_list = [
+            {
+                "data": "http://live.qq.com/10003715",
+                "result": "10003715"
+            },
+            {
+                "data": "http://m.live.qq.com/10039165",
+                "result": "10039165"
+            }
+        ]
+        for m_test in regex_match_list:
+            m = QQ._url_re.match(m_test.get("data"))
+            self.assertIsNotNone(m)
+            self.assertEqual(m_test.get("result"), m.group("room_id"))