Repository: streamlink/streamlink

Issue:
SRG SSR plugin
Hello

Another challenge for beardypig. ;)
Swiss public TV includes channels in : 
- German : SRF 1, SRF 2, SRF Info : `http://www.srf.ch/play/tv/live`
- French : RTS Un, RTS Deux, RTS Info : `http://www.rts.ch/play/tv/direct`
- Italian : RSI La 1, RSI La 2 : `http://www.rsi.ch/play/tv/live`

Sometimes they air live sports events exclusively online on their website. 
Channels are of course geoblocked to CH but it can be resolved with proxy. 

Example address source for RTS Un can be found here :
`http://il.srgssr.ch/integrationlayer/1.0/ue/rts/video/play/3608506.json`

They propose HDS and HLS.

Comments:

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index e75e8d05..1555f7f3 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -135,6 +135,8 @@ streamupcom         streamup.com         Yes   --
 svtplay             - svtplay.se         Yes   Yes   Streams may be geo-restricted to Sweden.
                     - svtflow.se
                     - oppetarkiv.se
+swisstxt            - srf.ch             Yes   No    Streams are geo-restricted to Switzerland.
+                    - rsi.ch
 tf1                 - tf1.fr             Yes   No    Streams may be geo-restricted to France.
                     - lci.fr
 tga                 - star.plu.cn        Yes   No
diff --git a/src/streamlink/compat.py b/src/streamlink/compat.py
index b5d54136..bf2df266 100644
--- a/src/streamlink/compat.py
+++ b/src/streamlink/compat.py
@@ -21,14 +21,15 @@ elif is_py3:
 
 try:
     from urllib.parse import (
-        urlparse, urlunparse, urljoin, quote, unquote, parse_qsl
+        urlparse, urlunparse, urljoin, quote, unquote, parse_qsl, urlencode
     )
     import queue
 except ImportError:
     from urlparse import urlparse, urlunparse, urljoin, parse_qsl
-    from urllib import quote, unquote
+    from urllib import quote, unquote, urlencode
     import Queue as queue
 
+
 __all__ = ["is_py2", "is_py3", "is_py33", "is_win32", "str", "bytes",
            "urlparse", "urlunparse", "urljoin", "parse_qsl", "quote",
-           "unquote", "queue", "range"]
+           "unquote", "queue", "range", "urlencode"]
diff --git a/src/streamlink/plugins/srgssr.py b/src/streamlink/plugins/srgssr.py
index 4f160501..3444492d 100644
--- a/src/streamlink/plugins/srgssr.py
+++ b/src/streamlink/plugins/srgssr.py
@@ -1,32 +1,46 @@
 from __future__ import print_function
+
 import re
 
+from streamlink.compat import urlparse, parse_qsl
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http
 from streamlink.plugin.api import validate
-from streamlink.stream import HDSStream
 from streamlink.stream import HLSStream
-from streamlink.compat import urlparse, parse_qsl
 
 
 class SRGSSR(Plugin):
-    url_re = re.compile(r"https?://(?:www\.)?(srf|rts|rsi|rtr)\.ch/play/tv")
+    url_re = re.compile(r"""https?://(?:www\.)?
+            (srf|rts|rsi|rtr)\.ch/
+            (?:
+                play/tv|
+                livestream/player|
+                live-streaming|
+                sport/direct/(\d+)-
+            )""", re.VERBOSE)
     api_url = "http://il.srgssr.ch/integrationlayer/1.0/ue/{site}/video/play/{id}.json"
-    video_id_re = re.compile(r'urn:(srf|rts|rsi|rtr):(?:ais:)?video:([^&"]+)')
+    token_url = "http://tp.srgssr.ch/akahd/token"
+    video_id_re = re.compile(r'urn(?:%3A|:)(srf|rts|rsi|rtr)(?:%3A|:)(?:ais(?:%3A|:))?video(?:%3A|:)([^&"]+)')
     video_id_schema = validate.Schema(validate.transform(video_id_re.search))
     api_schema = validate.Schema(
-        {"Video":
-            {"Playlists":
-                {"Playlist": [{
-                    "@protocol": validate.text,
-                    "url": [{"@quality": validate.text, "text": validate.url()}]
-                }]
+        {
+            "Video":
+                {
+                    "Playlists":
+                        {
+                            "Playlist": [{
+                                "@protocol": validate.text,
+                                "url": [{"@quality": validate.text, "text": validate.url()}]
+                            }]
+                        }
                 }
-            }
         },
         validate.get("Video"),
         validate.get("Playlists"),
         validate.get("Playlist"))
+    token_schema = validate.Schema({"token": {"authparams": validate.text}},
+                                   validate.get("token"),
+                                   validate.get("authparams"))
 
     @classmethod
     def can_handle_url(cls, url):
@@ -37,11 +51,14 @@ class SRGSSR(Plugin):
         qinfo = dict(parse_qsl(parsed.query or parsed.fragment.lstrip("?")))
 
         site, video_id = None, None
+        url_m = self.url_re.match(self.url)
 
         # look for the video id in the URL, otherwise find it in the page
         if "tvLiveId" in qinfo:
             video_id = qinfo["tvLiveId"]
-            site = self.url_re.match(self.url).group(1)
+            site = url_m.group(1)
+        elif url_m.group(2):
+            site, video_id = url_m.group(1), url_m.group(2)
         else:
             video_id_m = http.get(self.url, schema=self.video_id_schema)
             if video_id_m:
@@ -49,8 +66,16 @@ class SRGSSR(Plugin):
 
         return site, video_id
 
+    def get_authparams(self, url):
+        parsed = urlparse(url)
+        path, _ = parsed.path.rsplit("/", 1)
+        token_res = http.get(self.token_url, params=dict(acl=path + "/*"))
+        authparams = http.json(token_res, schema=self.token_schema)
+        self.logger.debug("Found authparams: {0}", authparams)
+        return dict(parse_qsl(authparams))
+
     def _get_streams(self):
-        video_id, site = self.get_video_id()
+        site, video_id = self.get_video_id()
 
         if video_id and site:
             self.logger.debug("Found {0} video ID {1}", site, video_id)
@@ -59,11 +84,9 @@ class SRGSSR(Plugin):
 
             for stream_info in http.json(res, schema=self.api_schema):
                 for url in stream_info["url"]:
-                    if stream_info["@protocol"] == "HTTP-HDS":
-                        for s in HDSStream.parse_manifest(self.session, url["text"]).items():
-                            yield s
                     if stream_info["@protocol"] == "HTTP-HLS":
-                        for s in HLSStream.parse_variant_playlist(self.session, url["text"]).items():
+                        params = self.get_authparams(url["text"])
+                        for s in HLSStream.parse_variant_playlist(self.session, url["text"], params=params).items():
                             yield s
 
 
diff --git a/src/streamlink/plugins/swisstxt.py b/src/streamlink/plugins/swisstxt.py
new file mode 100644
index 00000000..cacf8c0b
--- /dev/null
+++ b/src/streamlink/plugins/swisstxt.py
@@ -0,0 +1,45 @@
+from __future__ import print_function
+
+import re
+
+from streamlink.compat import urlparse, parse_qsl, urlunparse
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.stream import HLSStream
+
+
+class Swisstxt(Plugin):
+    url_re = re.compile(r"""https?://(?:
+        live\.(rsi)\.ch/|
+        (?:www\.)?(srf)\.ch/sport/resultcenter
+    )""", re.VERBOSE)
+    api_url = "http://event.api.swisstxt.ch/v1/stream/{site}/byEventItemIdAndType/{id}/HLS"
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None and cls.get_event_id(url)
+
+    @classmethod
+    def get_event_id(cls, url):
+        return dict(parse_qsl(urlparse(url).query.lower())).get("eventid")
+
+    def get_stream_url(self, event_id):
+        url_m = self.url_re.match(self.url)
+        site = url_m.group(1) or url_m.group(2)
+        api_url = self.api_url.format(id=event_id, site=site.upper())
+        self.logger.debug("Calling API: {0}", api_url)
+
+        stream_url = http.get(api_url).text.strip("\"'")
+
+        parsed = urlparse(stream_url)
+        query = dict(parse_qsl(parsed.query))
+        return urlunparse(parsed._replace(query="")), query
+
+    def _get_streams(self):
+        stream_url, params = self.get_stream_url(self.get_event_id(self.url))
+        return HLSStream.parse_variant_playlist(self.session,
+                                                stream_url,
+                                                params=params)
+
+
+__plugin__ = Swisstxt


Test patch:
diff --git a/tests/test_plugin_srgssr.py b/tests/test_plugin_srgssr.py
index 3e5d5fc5..53d90488 100644
--- a/tests/test_plugin_srgssr.py
+++ b/tests/test_plugin_srgssr.py
@@ -3,7 +3,7 @@ import unittest
 from streamlink.plugins.srgssr import SRGSSR
 
 
-class TestPluginCrunchyroll(unittest.TestCase):
+class TestPluginSRGSSR(unittest.TestCase):
     def test_can_handle_url(self):
         # should match
         self.assertTrue(SRGSSR.can_handle_url("http://srf.ch/play/tv/live"))
@@ -13,6 +13,8 @@ class TestPluginCrunchyroll(unittest.TestCase):
         self.assertTrue(SRGSSR.can_handle_url("http://rtr.ch/play/tv/live"))
         self.assertTrue(SRGSSR.can_handle_url("http://rts.ch/play/tv/direct#?tvLiveId=3608506"))
         self.assertTrue(SRGSSR.can_handle_url("http://www.srf.ch/play/tv/live#?tvLiveId=c49c1d64-9f60-0001-1c36-43c288c01a10"))
+        self.assertTrue(SRGSSR.can_handle_url("http://www.rts.ch/sport/direct/8328501-tennis-open-daustralie.html"))
+        self.assertTrue(SRGSSR.can_handle_url("http://www.rts.ch/play/tv/tennis/video/tennis-open-daustralie?id=8328501"))
 
         # shouldn't match
         self.assertFalse(SRGSSR.can_handle_url("http://www.crunchyroll.com/gintama"))
diff --git a/tests/test_plugin_swisstxt.py b/tests/test_plugin_swisstxt.py
new file mode 100644
index 00000000..c394541c
--- /dev/null
+++ b/tests/test_plugin_swisstxt.py
@@ -0,0 +1,28 @@
+import unittest
+
+from streamlink.plugins.swisstxt import Swisstxt
+
+
+class TestPluginSwisstxt(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Swisstxt.can_handle_url("http://www.srf.ch/sport/resultcenter/tennis?eventId=338052"))
+        self.assertTrue(Swisstxt.can_handle_url("http://live.rsi.ch/tennis.html?eventId=338052"))
+        self.assertTrue(Swisstxt.can_handle_url("http://live.rsi.ch/sport.html?eventId=12345"))
+
+        # shouldn't match
+        # regular srgssr sites
+        self.assertFalse(Swisstxt.can_handle_url("http://srf.ch/play/tv/live"))
+        self.assertFalse(Swisstxt.can_handle_url("http://www.rsi.ch/play/tv/live#?tvLiveId=livestream_La1"))
+        self.assertFalse(Swisstxt.can_handle_url("http://rsi.ch/play/tv/live?tvLiveId=livestream_La1"))
+        self.assertFalse(Swisstxt.can_handle_url("http://www.rtr.ch/play/tv/live"))
+        self.assertFalse(Swisstxt.can_handle_url("http://rtr.ch/play/tv/live"))
+        self.assertFalse(Swisstxt.can_handle_url("http://rts.ch/play/tv/direct#?tvLiveId=3608506"))
+        self.assertFalse(Swisstxt.can_handle_url("http://www.srf.ch/play/tv/live#?tvLiveId=c49c1d64-9f60-0001-1c36-43c288c01a10"))
+        self.assertFalse(Swisstxt.can_handle_url("http://www.rts.ch/sport/direct/8328501-tennis-open-daustralie.html"))
+        self.assertFalse(Swisstxt.can_handle_url("http://www.rts.ch/play/tv/tennis/video/tennis-open-daustralie?id=8328501"))
+
+        # other sites
+        self.assertFalse(Swisstxt.can_handle_url("http://www.crunchyroll.com/gintama"))
+        self.assertFalse(Swisstxt.can_handle_url("http://www.crunchyroll.es/gintama"))
+        self.assertFalse(Swisstxt.can_handle_url("http://www.youtube.com/"))