Repository: streamlink/streamlink
Pull request #442 (2017-01-17 19:03:15)

streamlink_cli.utils.progress: write new line at finish
without it:
```
[download][test.flv] Written 129.2 MB (5s @ 7.5 MB/s)
cli][info] Stream ended
```

with it:
```
[download][test.flv] Written 129.2 MB (5s @ 7.5 MB/s)
[cli][info] Stream ended
```


Patch:
diff --git a/docs/install.rst b/docs/install.rst
index 4a72c043..3804c1f4 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -18,6 +18,9 @@ Distribution                         Installing
                                         # pacaur -S streamlink-git
 
                                      `Installing AUR packages`_
+`Fedora`_                            .. code-block:: console
+
+                                        # dnf install streamlink
 `Gentoo Linux`_                      .. code-block:: console
 
                                         # emerge net-misc/streamlink
@@ -41,6 +44,7 @@ Distribution                         Installing
 
 .. _Arch Linux (aur): https://aur.archlinux.org/packages/streamlink/
 .. _Arch Linux (aur, git): https://aur.archlinux.org/packages/streamlink-git/
+.. _Fedora: https://apps.fedoraproject.org/packages/python-streamlink
 .. _Gentoo Linux: https://packages.gentoo.org/package/net-misc/streamlink
 .. _NetBSD (pkgsrc): http://pkgsrc.se/multimedia/streamlink
 .. _NixOS: https://github.com/NixOS/nixpkgs/tree/master/pkgs/applications/video/streamlink
@@ -76,6 +80,7 @@ Distribution/Platform                Maintainer
 ==================================== ===========================================
 Arch                                 Josip Ponjavic <josipponjavic at gmail.com>
 Chocolatey                           Scott Walters <me at scowalt.com>
+Fedora                               Mohamed El Morabity <melmorabity at fedoraproject.org>
 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 94b84d2b..e204f7ce 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -109,6 +109,7 @@ orf_tvthek          tvthek.orf.at        Yes   Yes
 pandatv             panda.tv             Yes   ?
 periscope           periscope.tv         Yes   Yes   Replay/VOD is supported.
 picarto             picarto.tv           Yes   --
+playtv              playtv.fr            Yes   --    Streams may be geo-restricted to France.
 pluzz               pluzz.francetv.fr    Yes   Yes   Streams may be geo-restricted to France, Andorra and Monaco.
 powerapp            powerapp.com.tr      Yes   No
 raiplay             raiplay.it           Yes   No    Most streams are geo-restricted to Italy.
diff --git a/src/streamlink/plugins/adultswim.py b/src/streamlink/plugins/adultswim.py
index e3a7e873..df79df68 100644
--- a/src/streamlink/plugins/adultswim.py
+++ b/src/streamlink/plugins/adultswim.py
@@ -24,10 +24,12 @@ class AdultSwim(Plugin):
     live_schema = validate.Schema({
         u"streams": {
             validate.text: {u"stream": validate.text,
+                            u"isLive": bool,
                             u"archiveEpisodes": [{
                                 u"id": validate.text,
-                                u"slug": validate.text
+                                u"slug": validate.text,
                             }]}}
+
     })
     vod_id_schema = validate.Schema({u"show": {u"sluggedVideo": {u"id": validate.text}}},
                                     validate.transform(lambda x: x["show"]["sluggedVideo"]["id"]))
@@ -79,12 +81,25 @@ class AdultSwim(Plugin):
         stream_info = parse_json(stream_data.group(1), schema=self.live_schema)
         # get the stream ID
         stream_id = None
+        show_info = stream_info[u"streams"][show]
+
         if episode:
-            for epi in stream_info[u"streams"][show][u"archiveEpisodes"]:
+            self.logger.debug("Loading replay of episode: {0}/{1}", show, episode)
+            for epi in show_info[u"archiveEpisodes"]:
                 if epi[u"slug"] == episode:
                     stream_id = epi[u"id"]
-        else:
-            stream_id = stream_info[u"streams"][show][u"stream"]
+        elif show_info["isLive"] or not len(show_info[u"archiveEpisodes"]):
+            self.logger.debug("Loading LIVE streams for: {0}", show)
+            stream_id = show_info[u"stream"]
+        else:  # off-air
+            if len(show_info[u"archiveEpisodes"]):
+                epi = show_info[u"archiveEpisodes"][0]
+                self.logger.debug("Loading replay of episode: {0}/{1}", show, epi[u"slug"])
+                stream_id = epi[u"id"]
+            else:
+                self.logger.error("This stream is currently offline")
+                return
+
 
         if stream_id:
             api_url = self.API_URL.format(id=stream_id)
@@ -116,10 +131,6 @@ class AdultSwim(Plugin):
 
         if stream_data:
             if live_stream:
-                if episode_name:
-                    self.logger.debug("Loading replay of LIVE streams for: {0}/{1}", show_name, episode_name)
-                else:
-                    self.logger.debug("Loading LIVE streams for: {0}", show_name)
                 streams = self._get_live_stream(stream_data, show_name, episode_name)
             else:
                 self.logger.debug("Loading VOD streams for: {0}/{1}", show_name, episode_name)
diff --git a/src/streamlink/plugins/playtv.py b/src/streamlink/plugins/playtv.py
new file mode 100644
index 00000000..8df9b75f
--- /dev/null
+++ b/src/streamlink/plugins/playtv.py
@@ -0,0 +1,71 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http, validate
+from streamlink.stream import HDSStream, HLSStream
+
+
+class PlayTV(Plugin):
+    FORMATS_URL = 'http://playtv.fr/player/initialize/{0}/'
+    API_URL = 'http://playtv.fr/player/play/{0}/?format={1}&language={2}&bitrate={3}'
+
+    _url_re = re.compile(r'http://playtv\.fr/television/(?P<channel>[^/]+)/?')
+
+    _formats_schema = validate.Schema({
+        'streams': validate.any(
+            [],
+            {
+                validate.text: validate.Schema({
+                    validate.text: {
+                        'bitrates': validate.all([
+                            validate.Schema({
+                                'value': int
+                            })
+                        ])
+                    }
+                })
+            }
+        )
+    })
+    _api_schema = validate.Schema({
+        'url': validate.url()
+    })
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return PlayTV._url_re.match(url)
+
+    def _get_streams(self):
+        match = self._url_re.match(self.url)
+        channel = match.group('channel')
+
+        res = http.get(self.FORMATS_URL.format(channel))
+        streams = http.json(res, schema=self._formats_schema)['streams']
+        if streams == []:
+            self.logger.error('Channel may be geo-restricted, not directly provided by PlayTV or not freely available')
+            return
+
+        for language in streams:
+            for protocol, bitrates in list(streams[language].items()):
+                # - Ignore non-supported protocols (RTSP, DASH)
+                # - Ignore deprecated Flash (RTMPE) streams (PlayTV doesn't provide anymore a Flash player)
+                if protocol in ['rtsp', 'flash', 'dash']:
+                    continue
+
+                for bitrate in bitrates['bitrates']:
+                    if bitrate['value'] == 0:
+                        continue
+                    api_url = self.API_URL.format(channel, protocol, language, bitrate['value'])
+                    res = http.get(api_url)
+                    video_url = http.json(res, schema=self._api_schema)['url']
+                    bs = '{0}k'.format(bitrate['value'])
+
+                    if protocol == 'hls':
+                        for _, stream in HLSStream.parse_variant_playlist(self.session, video_url).items():
+                            yield bs, stream
+                    elif protocol == 'hds':
+                        for _, stream in HDSStream.parse_manifest(self.session, video_url).items():
+                            yield bs, stream
+
+
+__plugin__ = PlayTV
diff --git a/src/streamlink_cli/utils/progress.py b/src/streamlink_cli/utils/progress.py
index fb12333b..dc7a0687 100644
--- a/src/streamlink_cli/utils/progress.py
+++ b/src/streamlink_cli/utils/progress.py
@@ -121,3 +121,5 @@ def progress(iterator, prefix):
                 speed=format_filesize(speed)
             )
             print_inplace(status)
+    sys.stderr.write("\n")
+    sys.stderr.flush()


Test patch:
diff --git a/tests/test_plugin_playtv.py b/tests/test_plugin_playtv.py
new file mode 100644
index 00000000..06e089e1
--- /dev/null
+++ b/tests/test_plugin_playtv.py
@@ -0,0 +1,18 @@
+import unittest
+
+from streamlink.plugins.playtv import PlayTV
+
+
+class TestPluginPlayTV(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(PlayTV.can_handle_url("http://playtv.fr/television/arte"))
+        self.assertTrue(PlayTV.can_handle_url("http://playtv.fr/television/arte/"))
+        self.assertTrue(PlayTV.can_handle_url("http://playtv.fr/television/tv5-monde"))
+        self.assertTrue(PlayTV.can_handle_url("http://playtv.fr/television/france-24-english/"))
+
+        # shouldn't match
+        self.assertFalse(PlayTV.can_handle_url("http://playtv.fr/television/"))
+        self.assertFalse(PlayTV.can_handle_url("http://playtv.fr/replay-tv/"))
+        self.assertFalse(PlayTV.can_handle_url("http://tvcatchup.com/"))
+        self.assertFalse(PlayTV.can_handle_url("http://youtube.com/"))