Repository: streamlink/streamlink
Pull request #977 (2017-06-02 20:12:46)

Add support for TV5Monde
This plugin adds support for the French-speaking channel network [TV5 Monde](http://www.tv5monde.com/).
It supports both [live](http://live.tv5monde.com/fbs.html) and VOD streams from:

* http://www.tv5monde.com/
* http://www.tv5mondeplus.com/
* http://www.tv5mondeplusafrique.com/

Notice that the live stream is geo-restricted to France, Belgium and Switzerland.




Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 3498ac02..df725172 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -206,6 +206,9 @@ tv3cat              tv3.cat              Yes   Yes   Streams may be geo-restrict
 tv4play             - tv4play.se         Yes   Yes   Streams may be geo-restricted to Sweden.
                                                      Only non-premium streams currently supported.
                     - fotbollskanalen.se
+tv5monde            - tv5monde.com       Yes   Yes   Streams may be geo-restricted to France, Belgium and Switzerland.
+                    - tv5mondeplus.com
+		    - tv5mondepl... [9]_
 tv8                 tv8.com.tr           Yes   No
 tv8cat              tv8.cat              Yes   No    Streams may be geo-restricted to Spain/Catalunya.
 tv360               tv360.com.tr         Yes   No
@@ -258,3 +261,4 @@ zhanqitv            zhanqi.tv            Yes   No
 .. [6] players.brightcove.net
 .. [7] player.theplatform.com
 .. [8] france3-regions.francetvinfo.fr
+.. [9] tv5mondeplusafrique.com
diff --git a/src/streamlink/plugins/tv5monde.py b/src/streamlink/plugins/tv5monde.py
new file mode 100644
index 00000000..d6a57f14
--- /dev/null
+++ b/src/streamlink/plugins/tv5monde.py
@@ -0,0 +1,78 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http, validate
+from streamlink.stream import HLSStream, HTTPStream, RTMPStream
+from streamlink.utils import parse_json
+from streamlink.plugins.common_jwplayer import _js_to_json
+
+
+class TV5Monde(Plugin):
+    _url_re = re.compile(r'http://(.+\.)?(tv|tivi)5monde(plus(afrique)?)?\.com')
+    _videos_re = re.compile(r'"?(?:files|sources)"?:\s*(?P<videos>\[.+?\])')
+    _videos_embed_re = re.compile(r'(?:file:\s*|src=)"(?P<embed>.+?\.mp4|.+?/embed/.+?)"')
+
+    _videos_schema = validate.Schema(
+        validate.transform(_js_to_json),
+        validate.transform(parse_json),
+        validate.all([
+            validate.any(
+                validate.Schema(
+                    {'url': validate.url()},
+                    validate.get('url')
+                ),
+                validate.Schema(
+                    {'file': validate.url()},
+                    validate.get('file')
+                ),
+            )
+        ])
+    )
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return TV5Monde._url_re.match(url)
+
+    def _get_non_embed_streams(self, page):
+        match = self._videos_re.search(page)
+        if match is not None:
+            videos = self._videos_schema.validate(match.group('videos'))
+            return videos
+
+        return []
+
+    def _get_embed_streams(self, page):
+        match = self._videos_embed_re.search(page)
+        if match is None:
+            return []
+
+        url = match.group('embed')
+        if '.mp4' in url:
+            return [url]
+
+        res = http.get(url)
+        videos = self._get_non_embed_streams(res.text)
+        if videos:
+            return videos
+
+        return []
+
+    def _get_streams(self):
+        res = http.get(self.url)
+        match = self._videos_re.search(res.text)
+        if match is not None:
+            videos = self._videos_schema.validate(match.group('videos'))
+        else:
+            videos = self._get_embed_streams(res.text)
+
+        for url in videos:
+            if '.m3u8' in url:
+                for stream in HLSStream.parse_variant_playlist(self.session, url).items():
+                    yield stream
+            elif 'rtmp' in url:
+                yield 'vod', RTMPStream(self.session, {'rtmp': url})
+            elif '.mp4' in url:
+                yield 'vod', HTTPStream(self.session, url)
+
+
+__plugin__ = TV5Monde


Test patch:
diff --git a/tests/test_plugin_tv5monde.py b/tests/test_plugin_tv5monde.py
new file mode 100644
index 00000000..1cf40712
--- /dev/null
+++ b/tests/test_plugin_tv5monde.py
@@ -0,0 +1,19 @@
+import unittest
+
+from streamlink.plugins.tv5monde import TV5Monde
+
+
+class TestPluginTV5Monde(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(TV5Monde.can_handle_url("http://live.tv5monde.com/fbs.html"))
+        self.assertTrue(TV5Monde.can_handle_url("http://information.tv5monde.com/les-jt/monde"))
+        self.assertTrue(TV5Monde.can_handle_url("http://information.tv5monde.com/info/legislatives-en-france-carnet-de-campagne-a-montreal-172631"))
+        self.assertTrue(TV5Monde.can_handle_url("http://www.tv5mondeplusafrique.com/video_karim_ben_khelifa_je_ne_suis_pas_votre_negre_oumou_sangare_4658602.html"))
+        self.assertTrue(TV5Monde.can_handle_url("http://www.tv5mondeplus.com/toutes-les-videos/information/tv5monde-le-journal-edition-du-02-06-17-11h00"))
+        self.assertTrue(TV5Monde.can_handle_url("http://focus.tv5monde.com/prevert/le-roi-et-loiseau/"))
+
+        # shouldn't match
+        self.assertFalse(TV5Monde.can_handle_url("http://www.tv5.ca/"))
+        self.assertFalse(TV5Monde.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(TV5Monde.can_handle_url("http://www.youtube.com/"))