Repository: streamlink/streamlink
Pull request #383 (2017-01-08 19:53:52)

plugins.vaughnlive: updated player version and info URL
Fixes the bug reported in #378. 

The Info URL and player version have been updated, I added some extra debug logging while updating it. 

The version and info url are hardcoded in the swf, is possible that we could decompress the swf and try to extract the version and info url. 


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 2fce94c8..fd3c8d39 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -101,7 +101,8 @@ periscope           periscope.tv         Yes   Yes   Replay/VOD is supported.
 picarto             picarto.tv           Yes   --
 pluzz               pluzz.francetv.fr    Yes   Yes   Streams may be geo-restricted to France, Andorra and Monaco.
 powerapp            powerapp.com.tr      Yes   No
-rtlxl               rtlxl.nl             No    Yes   Streams may be geo-restriced to The Netherlands. Livestreams not supported.
+raiplay             raiplay.it           Yes   No    Most streams are geo-restricted to Italy.
+rtlxl               rtlxl.nl             No    Yes   Streams may be geo-restricted to The Netherlands. Livestreams not supported.
 rtve                rtve.es              Yes   No
 ruv                 ruv.is               Yes   Yes   Streams may be geo-restricted to Iceland.
 seemeplay           seemeplay.ru         Yes   Yes
@@ -118,6 +119,8 @@ streamupcom         streamup.com         Yes   --
 svtplay             - svtplay.se         Yes   Yes   Streams may be geo-restricted to Sweden.
                     - svtflow.se
                     - oppetarkiv.se
+tf1                 - tf1.fr             Yes   No    Streams may be geo-restricted to France.
+                    - lci.fr
 tga                 - star.plu.cn        Yes   No
                     - star.tga.plu.cn
 tigerdile           tigerdile.com        Yes   --
diff --git a/src/streamlink/plugin/api/useragents.py b/src/streamlink/plugin/api/useragents.py
new file mode 100644
index 00000000..30ee477c
--- /dev/null
+++ b/src/streamlink/plugin/api/useragents.py
@@ -0,0 +1,22 @@
+ANDROID = ("Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) "
+           "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36")
+CHROME_OS = ("Mozilla/5.0 (X11; CrOS armv7l 4537.56.0) "
+             "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.38 Safari/537.36")
+IE_11 = "Mozilla/5.0 (MSIE 10.0; Windows NT 6.1; Trident/5.0)"
+IE_6 = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; WOW64; Trident/4.0; SLCC1)"
+IE_7 = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; Trident/4.0; SLCC1)"
+IE_8 = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; WOW64; Trident/4.0; SLCC1)"
+IE_9 = "Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; Trident/5.0)"
+IPHONE_6 = ("Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) "
+            "AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25")
+IPAD = ("Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) "
+        "AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25")
+WINDOWS_PHONE_8 = ("Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; "
+                   "Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)")
+SAFARI_8 = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) "
+            "AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12")
+SAFARI_7 = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) "
+            "AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A")
+FIREFOX = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:33.0) Gecko/20120101 Firefox/33.0"
+FIREFOX_MAC = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0"
+OPERA = "Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14"
diff --git a/src/streamlink/plugins/raiplay.py b/src/streamlink/plugins/raiplay.py
new file mode 100644
index 00000000..373db0f5
--- /dev/null
+++ b/src/streamlink/plugins/raiplay.py
@@ -0,0 +1,35 @@
+from __future__ import print_function
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
+from streamlink.stream import HLSStream
+
+
+class RaiPlay(Plugin):
+    url_re = re.compile(r"https?://(?:www\.)?raiplay\.it/dirette/(\w+)/?")
+    stream_re = re.compile(r"data-video-url.*?=.*?\"([^\"]+)\"")
+    stream_schema = validate.Schema(
+        validate.all(
+            validate.transform(stream_re.search),
+            validate.any(
+                None,
+                validate.all(validate.get(1), validate.url())
+            )
+        )
+    )
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def _get_streams(self):
+        channel = self.url_re.match(self.url).group(1)
+        self.logger.debug("Found channel: {0}", channel)
+        stream_url = http.get(self.url, schema=self.stream_schema)
+        if stream_url:
+            return HLSStream.parse_variant_playlist(self.session, stream_url)
+
+
+__plugin__ = RaiPlay
diff --git a/src/streamlink/plugins/tf1.py b/src/streamlink/plugins/tf1.py
new file mode 100644
index 00000000..337677e6
--- /dev/null
+++ b/src/streamlink/plugins/tf1.py
@@ -0,0 +1,60 @@
+from __future__ import print_function
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http, useragents
+from streamlink.stream import HDSStream
+from streamlink.stream import HLSStream
+
+
+class TF1(Plugin):
+    url_re = re.compile(r"https?://(?:www\.)?(?:tf1\.fr/(\w+)/direct|(lci).fr/direct)/?")
+    embed_url = "http://www.wat.tv/embedframe/live{0}"
+    embed_re = re.compile(r"urlLive.*?:.*?\"(http.*?)\"", re.MULTILINE)
+    api_url = "http://www.wat.tv/get/androidlive{0}/591997"
+    swf_url = "http://www.wat.tv/images/v70/PlayerWat.swf?rev=04.00.861"
+    hds_channel_remap = {"tf1": "connect"}
+    hls_channel_remap = {"lci": "LCI", "tf1": "V4"}
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def _get_hds_streams(self, channel):
+        channel = self.hds_channel_remap.get(channel, channel)
+        manifest_url = http.get(self.api_url.format(channel),
+                                params={"getURL": 1}).text
+
+        for s in HDSStream.parse_manifest(self.session,
+                                          manifest_url,
+                                          pvswf=self.swf_url).items():
+            yield s
+
+    def _get_hls_streams(self, channel):
+        channel = self.hls_channel_remap.get(channel, channel)
+        embed_url = self.embed_url.format(channel)
+        self.logger.debug("Found embed URL: {0}", embed_url)
+        # page needs to have a mobile user agent
+        embed_page = http.get(embed_url, headers={"User-Agent": useragents.ANDROID})
+
+        m = self.embed_re.search(embed_page.text)
+        if m:
+            hls_stream_url = m.group(1)
+
+            for s in HLSStream.parse_variant_playlist(self.session, hls_stream_url).items():
+                yield s
+
+    def _get_streams(self):
+        m = self.url_re.match(self.url)
+        if m:
+            channel = m.group(1) or m.group(2)
+            self.logger.debug("Found channel {0}", channel)
+            for s in self._get_hds_streams(channel):
+                yield s
+
+            for s in self._get_hls_streams(channel):
+                yield s
+
+
+
+__plugin__ = TF1
diff --git a/src/streamlink/plugins/vaughnlive.py b/src/streamlink/plugins/vaughnlive.py
index bb32626e..28708707 100644
--- a/src/streamlink/plugins/vaughnlive.py
+++ b/src/streamlink/plugins/vaughnlive.py
@@ -5,8 +5,8 @@ from streamlink.plugin import Plugin
 from streamlink.plugin.api import http, validate
 from streamlink.stream import RTMPStream
 
-PLAYER_VERSION = "0.1.1.767"
-INFO_URL = "http://mvn.vaughnsoft.net/video/edge/mnt-{domain}_{channel}?{version}_{ms}-{ms}-{random}"
+PLAYER_VERSION = "0.1.1.782"
+INFO_URL = "http://mvn.vaughnsoft.net/video/edge/soon_depricated_Q2_2017-{domain}_{channel}?{version}_{ms}-{ms}-{random}"
 
 DOMAIN_MAP = {
     "breakers": "btv",
@@ -24,24 +24,29 @@ _url_re = re.compile(r"""
 _swf_player_re = re.compile(r'swfobject.embedSWF\("(/\d+/swf/[0-9A-Za-z]+\.swf)"')
 
 _schema = validate.Schema(
-    validate.transform(lambda s: s.split(";")),
-    validate.length(3),
-    validate.union({
-        "server": validate.all(
-            validate.get(0),
-            validate.text
-        ),
-        "token": validate.all(
-            validate.get(1),
-            validate.text,
-            validate.startswith(":mvnkey-"),
-            validate.transform(lambda s: s[len(":mvnkey-"):])
-        ),
-        "ingest": validate.all(
-            validate.get(2),
-            validate.text
+    validate.any(
+        validate.all(u"<error></error>", validate.transform(lambda x: None)),
+        validate.all(
+            validate.transform(lambda s: s.split(";")),
+            validate.length(3),
+            validate.union({
+                "server": validate.all(
+                    validate.get(0),
+                    validate.text
+                ),
+                "token": validate.all(
+                    validate.get(1),
+                    validate.text,
+                    validate.startswith(":mvnkey-"),
+                    validate.transform(lambda s: s[len(":mvnkey-"):])
+                ),
+                "ingest": validate.all(
+                    validate.get(2),
+                    validate.text
+                )
+            })
         )
-    })
+    )
 )
 
 
@@ -56,6 +61,7 @@ class VaughnLive(Plugin):
         if match is None:
             return
         swfUrl = "http://vaughnlive.tv" + match.group(1)
+        self.logger.debug("Using swf url: {0}", swfUrl)
 
         match = _url_re.match(self.url)
         params = {}
@@ -64,7 +70,12 @@ class VaughnLive(Plugin):
         params["version"] = PLAYER_VERSION
         params["ms"] = random.randint(0, 999)
         params["random"] = random.random()
-        info = http.get(INFO_URL.format(**params), schema=_schema)
+        info_url = INFO_URL.format(**params)
+        self.logger.debug("Loading info url: {0}", INFO_URL.format(**params))
+        info = http.get(info_url, schema=_schema)
+        if not info:
+            self.logger.info("This stream is currently available")
+            return
 
         app = "live"
         if info["server"] in ["198.255.17.18:1337", "198.255.17.66:1337", "50.7.188.2:1337"]:


Test patch:
diff --git a/tests/test_plugin_raiplay.py b/tests/test_plugin_raiplay.py
new file mode 100644
index 00000000..6ace2414
--- /dev/null
+++ b/tests/test_plugin_raiplay.py
@@ -0,0 +1,20 @@
+import unittest
+
+from streamlink.plugins.raiplay import RaiPlay
+
+
+class TestPluginRaiPlay(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(RaiPlay.can_handle_url("http://www.raiplay.it/dirette/rai1"))
+        self.assertTrue(RaiPlay.can_handle_url("http://www.raiplay.it/dirette/rai2"))
+        self.assertTrue(RaiPlay.can_handle_url("http://www.raiplay.it/dirette/rai3"))
+        self.assertTrue(RaiPlay.can_handle_url("http://raiplay.it/dirette/rai3"))
+        self.assertTrue(RaiPlay.can_handle_url("https://raiplay.it/dirette/rai3"))
+        self.assertTrue(RaiPlay.can_handle_url("http://www.raiplay.it/dirette/rainews24"))
+        self.assertTrue(RaiPlay.can_handle_url("https://www.raiplay.it/dirette/rainews24"))
+
+        # shouldn't match
+        self.assertFalse(RaiPlay.can_handle_url("http://www.adultswim.com/videos/streams/toonami"))
+        self.assertFalse(RaiPlay.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(RaiPlay.can_handle_url("http://www.youtube.com/"))
diff --git a/tests/test_plugin_tf1.py b/tests/test_plugin_tf1.py
new file mode 100644
index 00000000..e8783242
--- /dev/null
+++ b/tests/test_plugin_tf1.py
@@ -0,0 +1,18 @@
+import unittest
+
+from streamlink.plugins.tf1 import TF1
+
+
+class TestPluginTF1(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(TF1.can_handle_url("http://tf1.fr/tf1/direct/"))
+        self.assertTrue(TF1.can_handle_url("http://lci.fr/direct"))
+        self.assertTrue(TF1.can_handle_url("http://www.lci.fr/direct"))
+        self.assertTrue(TF1.can_handle_url("http://tf1.fr/tmc/direct"))
+
+        # shouldn't match
+        self.assertFalse(TF1.can_handle_url("http://tf1.fr/direct"))
+        self.assertFalse(TF1.can_handle_url("http://www.tf1.fr/direct"))
+        self.assertFalse(TF1.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(TF1.can_handle_url("http://www.youtube.com/"))