Repository: streamlink/streamlink
Pull request #821 (2017-04-20 22:53:49)

plugins.canalplus: add support for cnews.fr + Dailymotion support
This PR enables support for the French TV news channel CNews in the canalplus plugin, as requested in https://github.com/streamlink/streamlink/issues/820.
It supports both [live](http://www.cnews.fr/direct) and [VOD](http://www.cnews.fr/videos).

Since all CNews streams are available on Dailymotion (as well as some streams for other channels supported by the plugin), the plugin will also returns Dailymotion streams if available.



Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 2e7fc52b..8ff0e8b4 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -45,6 +45,7 @@ camsoda             camsoda.com          Yes   No
 canalplus           - canalplus.fr       Yes   Yes   Streams may be geo-restricted to France.
                     - c8.fr
                     - cstar.fr
+                    - cnews.fr
 canlitv             - canlitv.com        Yes   --
                     - canlitv.life
                     - canlitvlive.co
diff --git a/src/streamlink/plugins/canalplus.py b/src/streamlink/plugins/canalplus.py
index 3d0f1b4b..0d21a61f 100644
--- a/src/streamlink/plugins/canalplus.py
+++ b/src/streamlink/plugins/canalplus.py
@@ -1,6 +1,5 @@
 import re
 
-from streamlink.exceptions import PluginError
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http, useragents, validate
 from streamlink.stream import HDSStream, HLSStream, HTTPStream
@@ -8,7 +7,7 @@ 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'}
+    CHANNEL_MAP = {'canalplus': 'cplus', 'c8': 'd8', 'cstar': 'd17', 'cnews': 'itele'}
     HDCORE_VERSION = '3.1.0'
     # Secret parameter needed to download HTTP videos on canalplus.fr
     SECRET = 'pqzerjlsmdkjfoiuerhsdlfknaes'
@@ -16,13 +15,15 @@ class CanalPlus(Plugin):
     _url_re = re.compile(r'''
         http://
         (
-            www\.(?P<channel>canalplus|c8|cstar)\.fr/.*pid.+?\.html(\?(?P<video_id>[0-9]+))? |
-            replay\.(?P<replay_channel>c8|cstar)\.fr/video/(?P<replay_video_id>[0-9]+)
+            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\.cnews\.fr/.+
         )
 ''', re.VERBOSE)
-    _video_id_re = re.compile(r'\bdata-video="(?P<video_id>[0-9]+)"')
+    _video_id_re = re.compile(r'(\bdata-video="|<meta property="og:video" content=".+?&videoId=)(?P<video_id>[0-9]+)"')
     _mp4_bitrate_re = re.compile(r'.*_(?P<bitrate>[0-9]+k)\.mp4')
     _api_schema = validate.Schema({
+        'ID_DM': validate.text,
         'TYPE': validate.text,
         'MEDIA': validate.Schema({
             'VIDEOS': validate.Schema({
@@ -44,25 +45,26 @@ 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')
-        if channel is None:
-            # Replay website
-            channel = match.group('replay_channel')
-            video_id = match.group('replay_video_id')
-        else:
+        channel = match.group('channel') or match.group('replay_channel') or 'cnews'
+        video_id = match.group('video_id') or match.group('replay_video_id')
+        if video_id is None:
+            # Retrieve URL page and search for video ID
+            res = http.get(self.url)
+            match = self._video_id_re.search(res.text)
+            if match is None:
+                return
             video_id = match.group('video_id')
-            if video_id is None:
-                # Retrieve URL page and search for video ID
-                res = http.get(self.url)
-                match = self._video_id_re.search(res.text)
-                if match is None:
-                    return
-                video_id = match.group('video_id')
 
         res = http.get(self.API_URL.format(self.CHANNEL_MAP[channel], video_id))
         videos = http.json(res, schema=self._api_schema)
         parsed = []
         headers = {'User-Agent': self._user_agent}
+
+        # Some videos may be also available on Dailymotion (especially on CNews)
+        if videos['ID_DM'] != '':
+            for stream in self.session.streams('https://www.dailymotion.com/video/' + videos['ID_DM']).items():
+                yield stream
+
         for quality, video_url in list(videos['MEDIA']['VIDEOS'].items()):
             # Ignore empty URLs
             if video_url == '':
@@ -75,7 +77,7 @@ class CanalPlus(Plugin):
 
             try:
                 # HDS streams don't seem to work for live videos
-                if '.f4m' in video_url and videos['TYPE'] != 'CHAINE LIVE':
+                if '.f4m' in video_url and 'LIVE' not in videos['TYPE']:
                     for stream in HDSStream.parse_manifest(self.session,
                                                            video_url,
                                                            params={'hdcore': self.HDCORE_VERSION},
@@ -97,9 +99,9 @@ class CanalPlus(Plugin):
                                               video_url,
                                               params={'secret': self.SECRET},
                                               headers=headers)
-            except PluginError:
-                self.logger.error('Failed to access stream, may be due to geo-restriction')
-                raise
+            except IOError as err:
+                if '403 Client Error' in str(err):
+                    self.logger.error('Failed to access stream, may be due to geo-restriction')
 
 
 __plugin__ = CanalPlus


Test patch:
diff --git a/tests/test_plugin_canalplus.py b/tests/test_plugin_canalplus.py
index 7b841e26..ca2f499f 100644
--- a/tests/test_plugin_canalplus.py
+++ b/tests/test_plugin_canalplus.py
@@ -17,6 +17,9 @@ class TestPluginCanalPlus(unittest.TestCase):
         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("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/"))
@@ -24,5 +27,6 @@ class TestPluginCanalPlus(unittest.TestCase):
         self.assertFalse(CanalPlus.can_handle_url("http://replay.c8.fr/"))
         self.assertFalse(CanalPlus.can_handle_url("http://www.cstar.fr/"))
         self.assertFalse(CanalPlus.can_handle_url("http://replay.cstar.fr/"))
+        self.assertFalse(CanalPlus.can_handle_url("http://www.cnews.fr/"))
         self.assertFalse(CanalPlus.can_handle_url("http://www.tvcatchup.com/"))
         self.assertFalse(CanalPlus.can_handle_url("http://www.youtube.com/"))