Repository: streamlink/streamlink
Pull request #382 (2017-01-08 18:49:20)

plugins.raiplay: added plugin to support raiplay.it
As requested in #375.

Example URLs:
- raiplay.it/dirette/rai1
- raiplay.it/dirette/rai3
- raiplay.it/dirette/rainews24
- raiplay.it/dirette/raisport1


Patch:
diff --git a/docs/install.rst b/docs/install.rst
index 76beec4b..1cae0039 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -61,13 +61,8 @@ Mac OS X                             .. code-block:: console
 
                                         # easy_install -U streamlink
 Microsoft Windows                    See `Windows binaries`_ and `Windows portable version`_.
-
-`Chocolatey`_                        .. code-block:: powershell
-
-                                        C:\> choco install streamlink
 ==================================== ===========================================
 
-.. _Chocolatey: https://chocolatey.org/packages/streamlink
 
 Package maintainers
 -------------------
@@ -75,7 +70,6 @@ Package maintainers
 Distribution/Platform                Maintainer
 ==================================== ===========================================
 Arch                                 Josip Ponjavic <josipponjavic at gmail.com>
-Chocolatey                           Scott Walters <me at scowalt.com>
 Gentoo                               soredake <fdsfgs at krutt.org>
 NetBSD                               Maya Rashish <maya at netbsd.org>
 NixOS                                Tuomas Tynkkynen <tuomas.tynkkynen at iki.fi>
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 2fce94c8..4d87416c 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -25,12 +25,11 @@ atresplayer         atresplayer.com      Yes   No    Streams are geo-restricted
 azubutv             azubu.tv             Yes   No
 bambuser            bambuser.com         Yes   Yes
 beam                beam.pro             Yes   No
-beattv              be-at.tv             Yes   Yes   Playlist not implemented yet.
 bigo                - live.bigo.tv       Yes   --
                     - bigoweb.co
+beattv              be-at.tv             Yes   Yes   Playlist not implemented yet.
 bilibili            live.bilibili.com    Yes   ?
 bliptv              blip.tv              --    Yes
-bongacams           bongacams.com        Yes   No    Only RTMP streams are available.
 chaturbate          chaturbate.com       Yes   No
 cinergroup          - showtv.com.tr      Yes   No
                     - haberturk.com
@@ -101,7 +100,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
diff --git a/src/streamlink/plugins/bongacams.py b/src/streamlink/plugins/bongacams.py
deleted file mode 100644
index 1df258bd..00000000
--- a/src/streamlink/plugins/bongacams.py
+++ /dev/null
@@ -1,153 +0,0 @@
-import re
-import time
-
-from io import BytesIO
-from hashlib import md5
-
-from streamlink.compat import bytes, urljoin, urlparse, urlunparse
-from streamlink.exceptions import PluginError, NoStreamsError
-from streamlink.packages.flashmedia.types import AMF0String, AMF0Value, U32BE
-from streamlink.packages.flashmedia import AMFPacket, AMFMessage
-from streamlink.plugin.api import validate, http
-from streamlink.plugin import Plugin
-from streamlink.stream import RTMPStream
-
-
-CONST_FLASH_VER = "WIN 24,0,0,186"
-CONST_DEFAULT_SWF_LOCATION = '/swf/chat/BCamChat.swf?cache=20161226150'
-CONST_AMF_GATEWAY_LOCATION = '/tools/amf.php'
-CONST_AMF_GATEWAY_PARAM = 'x-country'
-CONST_DEFAULT_COUNTRY_CODE = 'en'
-
-CONST_HEADERS = {}
-CONST_HEADERS['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) '
-CONST_HEADERS['User-Agent'] += 'Chrome/54.0.2840.99 Safari/537.36 OPR/41.0.2353.69'
-
-url_re = re.compile("(http(s)?://)?(\w{2}.)?(bongacams.com)/([\w\d_-]+)")
-swf_re = re.compile("/swf/\w+/\w+.swf\?cache=\d+")
-
-amf_msg_schema = validate.Schema({
-    "status" : "success",
-    "userData" : {
-        "username": validate.text
-    },
-    "localData" : {
-        "NC_ConnUrl" : validate.url(scheme="rtmp"),
-        "NC_AccessKey" : validate.length(32),
-        "dataKey" : validate.length(32),
-    },
-    "performerData" : {
-        "username" : validate.text,
-    }
-})
-
-
-class AMFMessage2(AMFMessage):
-    def _serialize(self, packet):
-        packet += AMF0String(self.target_uri)
-        packet += AMF0String(self.response_uri)
-        packet += U32BE(AMF0Value.size(self.value))
-        packet += AMF0Value.pack(self.value)
-
-
-class bongacams(Plugin):
-    @classmethod
-    def can_handle_url(self, url):
-        return url_re.match(url)
-
-    def _get_stream_uid(self, username):
-        m = md5(username.encode('utf-8') + str(time.time()).encode('utf-8'))
-        return m.hexdigest()
-
-    def _get_streams(self):
-        match = url_re.match(self.url)
-
-        stream_page_scheme = 'https'
-        stream_page_domain = match.group(4)
-        stream_page_path = match.group(5)
-        country_code = CONST_DEFAULT_COUNTRY_CODE
-        is_paid_show = False
-
-        # create http session and set headers
-        http_session = http
-        http_session.headers.update(CONST_HEADERS)
-
-        # get swf url and cookies
-        r = http_session.get(urlunparse((stream_page_scheme, stream_page_domain, stream_page_path, '', '', '')))
-
-        # redirect to profile page means stream is offlie
-        if '/profile/' in r.url:
-            raise NoStreamsError(self.url)
-        if not r.ok:
-            self.logger.debug("Status code for {}: {}", r.url, r.status_code)
-            raise NoStreamsError(self.url)
-        if len(http_session.cookies) == 0:
-            raise PluginError("Can't get a cookies")
-
-        if urlparse(r.url).netloc != stream_page_domain:
-            # then redirected to regional subdomain
-            country_code = urlparse(r.url).netloc.split('.')[0].lower()
-
-        # time to set variables
-        baseurl = urlunparse((stream_page_scheme, urlparse(r.url).netloc, '', '', '', ''))
-        amf_gateway_url = urljoin(baseurl, CONST_AMF_GATEWAY_LOCATION)
-        stream_page_url = urljoin(baseurl, stream_page_path)
-
-        match = swf_re.search(r.text)
-        if match:
-            swf_url = urljoin(baseurl, match.group())
-            self.logger.debug("swf url found: {}", swf_url)
-        else:
-            # most likely it means that country/region banned
-            # can try use default swf-url
-            swf_url = urljoin(baseurl, CONST_DEFAULT_SWF_LOCATION)
-            self.logger.debug("swf url not found. Will try {}", swf_url)
-
-        # create amf query
-        amf_message = AMFMessage2("svDirectAmf.getRoomData", "/1", [stream_page_path, is_paid_show])
-        amf_packet = AMFPacket(version=0)
-        amf_packet.messages.append(amf_message)
-
-        # send request and close http-session
-        r = http_session.post(url=amf_gateway_url,
-                              params={CONST_AMF_GATEWAY_PARAM : country_code},
-                              data=bytes(amf_packet.serialize()))
-        http_session.close()
-
-        if r.status_code != 200:
-            raise PluginError("unexpected status code for {}: {}", r.url, r.status_code)
-
-        amf_response = AMFPacket.deserialize(BytesIO(r.content))
-
-        if len(amf_response.messages) != 1 or amf_response.messages[0].target_uri != "/1/onResult":
-            raise PluginError("unexpected response from amf gate")
-
-        stream_source_info = amf_msg_schema.validate(amf_response.messages[0].value)
-        self.logger.debug("source stream info:\n{}", stream_source_info)
-
-        stream_params = {
-            "live": True,
-            "realtime": True,
-            "verbose": True,
-            "flashVer": CONST_FLASH_VER,
-            "swfUrl": swf_url,
-            "tcUrl": stream_source_info['localData']['NC_ConnUrl'],
-            "rtmp": stream_source_info['localData']['NC_ConnUrl'],
-            "pageUrl": stream_page_url,
-            "playpath": "%s?uid=%s" % (''.join(('stream_', stream_page_path)),
-                                       self._get_stream_uid(stream_source_info['userData']['username'])),
-            # Multiple args with same name not supported.
-            # Details: https://github.com/streamlink/streamlink/issues/321
-            "conn" : "S:{username} --conn=S:{access_key} --conn=B:0 --conn=S:{data_key}".format(
-                username=stream_source_info['userData']['username'],
-                access_key=stream_source_info['localData']['NC_AccessKey'],
-                data_key=stream_source_info['localData']['dataKey']
-            )
-        }
-
-        self.logger.debug("Stream params:\n{}", stream_params)
-        stream = RTMPStream(self.session, stream_params)
-
-        return {'live': stream}
-
-__plugin__ = bongacams
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


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/"))