Repository: streamlink/streamlink (python)

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.

Pull request description:
Swiss SRG SSR Fix 
Here's a fixed version of the srgssr plugin along with support for the swisstxt hosted sports streams. 

The PR fixes #403 

Should now support the sports live streams. You can find the sports streams on the following URLs (however, there are not always any streams available):

- srf.ch/sport/resultcenter/results
- rts.ch/sport/programmes/
- live.rsi.ch/results.html

In addition to the sports streams, there are some extra live streams listed on rsi.ch/live-streaming/


@karlo2105 also confirmed that the plugins were working as expected :-)

Patch:
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
deleted file mode 100644
index 9ec38ad0..00000000
--- a/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,36 +0,0 @@
-*Thanks for reporting an issue!*
-*Please read the contribution guidelines first!*
-
-*Feel free to use the following template. Be as detailed as possible.*
-*Don't forget to remove this text before submitting.*
-
-----
-
-### Checklist
-
-- [x] This is a bug report.
-- [ ] This is a plugin request.
-- [ ] This is a feature request.
-- [ ] I used the search function to find already opened/closed issues or pull requests.
-
-### Description
-
-...
-
-### Expected / Actual behavior
-
-...
-
-### Reproduction steps / Stream URLs to test
-
-1. ...
-2. ...
-3. ...
-
-### Environment details (operating system, python version, etc.)
-
-...
-
-### Comments, logs, screenshots, etc.
-
-...
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/chaturbate.py b/src/streamlink/plugins/chaturbate.py
index 1e59482b..7ab93396 100644
--- a/src/streamlink/plugins/chaturbate.py
+++ b/src/streamlink/plugins/chaturbate.py
@@ -5,7 +5,7 @@ from streamlink.plugin.api import http, validate
 from streamlink.stream import HLSStream
 
 _url_re = re.compile(r"http(s)?://(\w+.)?chaturbate.com/[^/?&]+")
-_playlist_url_re = re.compile(r"var hlsSource\w+ = '(?P<url>[^']+)';")
+_playlist_url_re = re.compile(r"html \+= \"src='(?P<url>[^']+)'\";")
 _schema = validate.Schema(
     validate.transform(_playlist_url_re.search),
     validate.any(
diff --git a/src/streamlink/plugins/pluzz.py b/src/streamlink/plugins/pluzz.py
index 52899872..04db1ff9 100644
--- a/src/streamlink/plugins/pluzz.py
+++ b/src/streamlink/plugins/pluzz.py
@@ -11,7 +11,7 @@ from streamlink.stream import HDSStream, HLSStream
 class Pluzz(Plugin):
     GEO_URL = 'http://geo.francetv.fr/ws/edgescape.json'
     API_URL = 'http://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/?idDiffusion={0}&catalogue=Pluzz'
-    TOKEN_URL = 'http://hdfauthftv-a.akamaihd.net/esi/TA?url={0}'
+    HDS_TOKEN_URL = 'http://hdfauthftv-a.akamaihd.net/esi/TA?url={0}'
 
     _url_re = re.compile(r'http://pluzz\.francetv\.fr/(videos/.+\.html|[\w-]+)')
     _video_id_re = re.compile(r'id="current_video" href="http://.+?\.(?:francetv|francetelevisions)\.fr/(?:video/|\?id-video=)(?P<video_id>.+?)"')
@@ -88,12 +88,6 @@ class Pluzz(Plugin):
         now = time.time()
 
         for video in videos['videos']:
-            video_url = video['url']
-
-            # TODO: add DASH streams once supported
-            if '.mpd' in video_url:
-                continue
-
             # Check whether video format is available
             if video['statut'] != 'ONLINE':
                 continue
@@ -111,10 +105,11 @@ class Pluzz(Plugin):
             if not available:
                 continue
 
-            res = http.get(self.TOKEN_URL.format(video_url))
-            video_url = res.text
-
+            video_url = video['url']
+            # TODO: add DASH streams once supported
             if '.f4m' in video_url and swf_url is not None:
+                res = http.get(self.HDS_TOKEN_URL.format(video_url))
+                video_url = res.text
                 for bitrate, stream in HDSStream.parse_manifest(self.session, video_url, pvswf=swf_url).items():
                     # HDS videos with data in their manifest fragment token
                     # doesn't seem to be supported by HDSStream. Ignore such
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
diff --git a/src/streamlink_cli/output.py b/src/streamlink_cli/output.py
index eb781a90..5c29d24b 100644
--- a/src/streamlink_cli/output.py
+++ b/src/streamlink_cli/output.py
@@ -67,8 +67,6 @@ class FileOutput(Output):
 
 
 class PlayerOutput(Output):
-    PLAYER_TERMINATE_TIMEOUT = 10.0
-
     def __init__(self, cmd, args=DEFAULT_PLAYER_ARGUMENTS, filename=None, quiet=True, kill=True, call=False, http=False,
                  namedpipe=None):
         super(PlayerOutput, self).__init__()
@@ -162,15 +160,7 @@ class PlayerOutput(Output):
 
         if self.kill:
             with ignored(Exception):
-                self.player.terminate()
-                if not is_win32:
-                    t, timeout = 0.0, self.PLAYER_TERMINATE_TIMEOUT
-                    while not self.player.poll() and t < timeout:
-                        sleep(0.5)
-                        t += 0.5
-
-                    if not self.player.returncode:
-                        self.player.kill()
+                self.player.kill()
         self.player.wait()
 
     def _write(self, data):


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