Repository: streamlink/streamlink
Pull request #792 (2017-04-14 10:57:12)

Add support for BFMTV group channels
This plugin adds support for the French BFMTV group channels:

* [BFMTV](http://www.bfmtv.com/mediaplayer/live-video/)
* [BFM Business](http://bfmbusiness.bfmtv.com/mediaplayer/live-video/)
* [BFM Paris](http://www.bfmtv.com/mediaplayer/live-bfm-paris/)
* [RMC](http://rmc.bfmtv.com/mediaplayer/live-audio/) (radio)
* [BFM Sport](http://rmcsport.bfmtv.com/mediaplayer/live-bfm-sport/)
* [RMC Découverte](http://rmcdecouverte.bfmtv.com/mediaplayer-direct/)
* [01net.com](http://www.01net.com/mediaplayer/) (no live stream, only VOD)

Both [live](http://mediaplayer.bfmtv.com/directs/) and [VOD](http://www.bfmtv.com/mediaplayer/replay/) are supported, as well as any bfmtv.com/01net.com page containing a video.

Streams don't seem to be georestricted. Except for the [BFMTV live stream](http://www.bfmtv.com/mediaplayer/live-video/), all videos rely on the Brightcove player.


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 6efe7ef9..716038e9 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -31,6 +31,8 @@ bambuser            bambuser.com         Yes   Yes
 bbciplayer          bbc.co.uk/iplayer    Yes   Yes   Streams may be geo-restricted to the United Kingdom.
 beam                beam.pro             Yes   Yes
 beattv              be-at.tv             Yes   Yes   Playlist not implemented yet.
+bfmtv               bfmtv.com            Yes   Yes
+                    01net.com
 bigo                - live.bigo.tv       Yes   --
                     - bigoweb.co
 bilibili            live.bilibili.com    Yes   ?
diff --git a/src/streamlink/plugins/bfmtv.py b/src/streamlink/plugins/bfmtv.py
new file mode 100644
index 00000000..8b910d2a
--- /dev/null
+++ b/src/streamlink/plugins/bfmtv.py
@@ -0,0 +1,39 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugins.brightcove import BrightcovePlayer
+from streamlink.stream import HLSStream
+
+
+class BFMTV(Plugin):
+    _url_re = re.compile(r'http://.+\.(?:bfmtv|01net)\.com')
+    _brightcove_video_re = re.compile(r'data-holder="video(?P<video_id>[0-9]+)" data-account="(?P<account_id>[0-9]+)"')
+    _brightcove_video_alt_re = re.compile(r'data-account="(?P<account_id>[0-9]+).*?data-video-id="(?P<video_id>[0-9]+)"')
+    _embed_video_url_re = re.compile(r"\$YOPLAYER\('liveStitching', {.+?file: '(?P<video_url>[^\"]+?)'.+?}\);", re.DOTALL)
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return BFMTV._url_re.match(url)
+
+    def _get_streams(self):
+        # Retrieve URL page and search for Brightcove video data
+        res = http.get(self.url)
+        match = self._brightcove_video_re.search(res.text) or self._brightcove_video_alt_re.search(res.text)
+        if match is not None:
+            account_id = match.group('account_id')
+            video_id = match.group('video_id')
+            player = BrightcovePlayer(self.session, account_id)
+            for stream in player.get_streams(video_id):
+                yield stream
+        else:
+            # Try to get the stream URL in the page
+            match = self._embed_video_url_re.search(res.text)
+            if match is not None:
+                video_url = match.group('video_url')
+                if '.m3u8' in video_url:
+                    for stream in HLSStream.parse_variant_playlist(self.session, video_url).items():
+                        yield stream
+
+
+__plugin__ = BFMTV


Test patch:
diff --git a/tests/test_plugin_bfmtv.py b/tests/test_plugin_bfmtv.py
new file mode 100644
index 00000000..38fa226a
--- /dev/null
+++ b/tests/test_plugin_bfmtv.py
@@ -0,0 +1,25 @@
+import unittest
+
+from streamlink.plugins.bfmtv import BFMTV
+
+
+class TestPluginBFMTV(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(BFMTV.can_handle_url("http://www.bfmtv.com/mediaplayer/live-video/"))
+        self.assertTrue(BFMTV.can_handle_url("http://bfmbusiness.bfmtv.com/mediaplayer/live-video/"))
+        self.assertTrue(BFMTV.can_handle_url("http://www.bfmtv.com/mediaplayer/live-bfm-paris/"))
+        self.assertTrue(BFMTV.can_handle_url("http://rmc.bfmtv.com/mediaplayer/live-audio/"))
+        self.assertTrue(BFMTV.can_handle_url("http://rmcsport.bfmtv.com/mediaplayer/live-bfm-sport/"))
+        self.assertTrue(BFMTV.can_handle_url("http://rmcdecouverte.bfmtv.com/mediaplayer-direct/"))
+        self.assertTrue(BFMTV.can_handle_url("http://www.bfmtv.com/mediaplayer/replay/premiere-edition/"))
+        self.assertTrue(BFMTV.can_handle_url("http://bfmbusiness.bfmtv.com/mediaplayer/replay/good-morning-business/"))
+        self.assertTrue(BFMTV.can_handle_url("http://rmc.bfmtv.com/mediaplayer/replay/les-grandes-gueules/"))
+        self.assertTrue(BFMTV.can_handle_url("http://rmcdecouverte.bfmtv.com/mediaplayer-replay/?id=6714&title=TOP%20GEAR%20:PASSION%20VINTAGE"))
+        self.assertTrue(BFMTV.can_handle_url("http://rmc.bfmtv.com/mediaplayer/replay/after-foot/"))
+        self.assertTrue(BFMTV.can_handle_url("http://www.01net.com/mediaplayer/replay/jtech/"))
+        self.assertTrue(BFMTV.can_handle_url("http://www.bfmtv.com/politique/macron-et-le-pen-talonnes-par-fillon-et-melenchon-a-l-approche-du-premier-tour-1142070.html"))
+
+        # shouldn't match
+        self.assertFalse(BFMTV.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(BFMTV.can_handle_url("http://www.youtube.com/"))