Repository: streamlink/streamlink
Pull request #2760 (2020-01-06 14:48:25)

plugins.galatasaraytv: Add support for GALATASARAY SK TV live streams 
Added free live stream [GALATASARAY SK](https://galatasaray.com/) support.
Tested. 360p (worst), 480p, 720p (best) quality is also broadcast continuously.

codecov[bot]: # [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/2760?src=pr&el=h1) Report
> Merging [#2760](https://codecov.io/gh/streamlink/streamlink/pull/2760?src=pr&el=desc) into [master](https://codecov.io/gh/streamlink/streamlink/commit/f90d560511523d67c71387eb67bb91fd577ec1bb?src=pr&el=desc) will **increase** coverage by `0.02%`.
> The diff coverage is `68.42%`.


```diff
@@            Coverage Diff             @@
##           master    #2760      +/-   ##
==========================================
+ Coverage   52.86%   52.88%   +0.02%     
==========================================
  Files         246      247       +1     
  Lines       15428    15446      +18     
==========================================
+ Hits         8156     8169      +13     
- Misses       7272     7277       +5
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e72a6036..24616892 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,40 @@
 # Changelog
 
+## streamlink 1.3.1 (2020-01-27)
+
+A small patch release that addresses the removal of [MPV's legacy option syntax](https://mpv.io/manual/master/#legacy-option-syntax), also with fixes of several plugins, the addition of the `--twitch-disable-reruns` parameter and dropped support for Python 3.4.
+
+
+```text
+Hunter Peavey <krathalan@disroot.org> (4):
+      Add wtwitch to list of thirdparty programs
+      Try adding an image
+      Move image position
+      Make requested changes
+
+Vladimir Stavrinov <9163352+vstavrinov@users.noreply.github.com> (1):
+      plugins.nhkworld: the site migrates from xml to json stream data
+
+back-to <backto@protonmail.ch> (6):
+      docs/tests: remove python 3.4, use 3.8 and nightly for travis-ci
+      plugins.bilibili: fix Livestreams with status 1 (set Referer)
+      plugins.youtube: Remove itag 303
+      plugins.ustream: Added support for video.ibm.com
+      plugins.bbciplayer: Fixed login params
+      plugins.bbciplayer: remove test_extract_nonce
+
+bastimeyer <mail@bastimeyer.de> (5):
+      plugins.twitch: use python logging module
+      plugins.twitch: fix rerun detection
+      cli.output: fix mpv player parameter format
+      2020
+      docs: fix MPV parameters on common issues page
+
+skulblakka <pascal.romahn@mailbox.org> (1):
+      Allow to disable twitch reruns (#2722)
+```
+
+
 ## streamlink 1.3.0 (2019-11-22)
 
 A new release with plugin updates and fixes, including Twitch.tv (see [#2680](https://github.com/streamlink/streamlink/issues/2680)), which had to be delayed due to back and forth API changes.
diff --git a/LICENSE b/LICENSE
index b38cbab5..32d52548 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
 Copyright (c) 2011-2016, Christopher Rosell
-Copyright (c) 2016-2019, Streamlink Team
+Copyright (c) 2016-2020, Streamlink Team
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/docs/_themes/sphinx_rtd_theme_violet/LICENSE b/docs/_themes/sphinx_rtd_theme_violet/LICENSE
index 9b33112e..98663405 100644
--- a/docs/_themes/sphinx_rtd_theme_violet/LICENSE
+++ b/docs/_themes/sphinx_rtd_theme_violet/LICENSE
@@ -2,7 +2,7 @@ The MIT License (MIT)
 
 Copyright (c) 2013 Dave Snider
 Copyright (c) 2014 Christopher Rosell
-Copyright (c) 2015-2019 Streamlink
+Copyright (c) 2015-2020 Streamlink
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of
 this software and associated documentation files (the "Software"), to deal in
diff --git a/docs/conf.py b/docs/conf.py
index 56c68923..7b05bb31 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -37,7 +37,7 @@ master_doc = 'index'
 
 # General information about the project.
 project = 'Streamlink'
-copyright = '2019, Streamlink'
+copyright = '2020, Streamlink'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
diff --git a/docs/issues.rst b/docs/issues.rst
index 1a88df73..4ebcb7b4 100644
--- a/docs/issues.rst
+++ b/docs/issues.rst
@@ -15,17 +15,18 @@ By default most players do not cache the data they receive from Streamlink.
 Caching can reduce the amount of buffering you run into because the player will
 have some breathing room between receiving the data and playing it.
 
-============= ======================== ======================================
-Player        Parameter                Note
-============= ======================== ======================================
-MPC-HC        --                       Currently no way of configuring the cache
-MPlayer       ``-cache <kbytes>``      Between 1024 and 8192 is recommended
-mpv           ``--cache <kbytes>``     Between 1024 and 8192 is recommended
-VLC           ``--file-caching <ms>    Between 1000 and 10000 is recommended
+============= ============================== ======================================
+Player        Parameter                      Note
+============= ============================== ======================================
+MPC-HC        --                             Currently no way of configuring the cache
+MPlayer       ``-cache <kbytes>``            Between 1024 and 8192 is recommended
+mpv           ``--cache=yes                  Between 1024 and 8192 is recommended
+              --demuxer-max-bytes=<kbytes>``
+VLC           ``--file-caching <ms>          Between 1000 and 10000 is recommended
               --network-caching <ms>``
-============= ======================== ======================================
+============= ============================== ======================================
 
-Use the :option:`--player` option to pass these options to your player.
+Use the :option:`--player-args` or :option:`--player` option to pass these options to your player.
 
 
 Multi-threaded streaming
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 7f02e8a1..362c8325 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -92,6 +92,7 @@ filmon                  filmon.com           Yes   Yes   Only SD quality streams
 foxtr                   fox.com.tr           Yes   No
 funimationnow           - funimation.com     --    Yes   :ref:`Requires session cookies <cli-funimationnow>`
                         - funimationnow.uk
+galatasaraytv           galatasaray.com      Yes   No
 gardenersworld          gardenersworld.com   --    Yes
 garena                  garena.live          Yes   --
 goltelevision           goltelevision.com    Yes   No    Streams may be geo-restricted to Spain.
@@ -110,6 +111,7 @@ itvplayer               itv.com/itvplayer    Yes   Yes   Streams may be geo-rest
 kanal7                  - kanal7.com         Yes   No
                         - tvt.tv.tr
 kingkong                kingkong.com.tw      Yes   --
+latina                  latina.pe            Yes   No    Streams may be geo-restricted to Peru.
 linelive                live.line.me         Yes   Yes
 live_russia_tv          live.russia.tv       Yes   --
 liveedu                 - liveedu.tv         Yes   --    Some streams require a login.
diff --git a/src/streamlink/__init__.py b/src/streamlink/__init__.py
index 82e8f712..ffd85651 100644
--- a/src/streamlink/__init__.py
+++ b/src/streamlink/__init__.py
@@ -15,7 +15,7 @@ del get_versions
 __title__ = "streamlink"
 __license__ = "Simplified BSD"
 __author__ = "Streamlink"
-__copyright__ = "Copyright 2019 Streamlink"
+__copyright__ = "Copyright 2020 Streamlink"
 __credits__ = [
     "Agustín Carrasco (@asermax)",
     "Andrew Bashore (@bashtech)",
diff --git a/src/streamlink/plugins/crunchyroll.py b/src/streamlink/plugins/crunchyroll.py
index a1d649da..bd09f44c 100644
--- a/src/streamlink/plugins/crunchyroll.py
+++ b/src/streamlink/plugins/crunchyroll.py
@@ -105,7 +105,7 @@ class CrunchyrollAPI(object):
     _user_agent = "Dalvik/1.6.0 (Linux; U; Android 4.4.2; Android SDK built for x86 Build/KK)"
     _version_code = 444
     _version_name = "2.1.10"
-    _access_token = "Scwg9PRRZ19iVwD"
+    _access_token = "WveH9VkPLrXvuNm"
     _access_type = "com.crunchyroll.crunchyroid"
 
     def __init__(self, cache, session, session_id=None, locale=_default_locale):
diff --git a/src/streamlink/plugins/galatasaraytv.py b/src/streamlink/plugins/galatasaraytv.py
new file mode 100644
index 00000000..87189fdb
--- /dev/null
+++ b/src/streamlink/plugins/galatasaraytv.py
@@ -0,0 +1,30 @@
+import logging
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+class GalatasarayTV(Plugin):
+    url_re = re.compile(r"https?://(?:www.)?galatasaray\.com")
+    playervars_re = re.compile(r"sources\s*:\s*\[\s*\{\s*type\s*:\s*\"(.*?)\",\s*src\s*:\s*\"(.*?)\"", re.DOTALL)
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def get_title(self):
+        return "Galatasaray TV"
+
+    def _get_streams(self):
+        res = self.session.http.get(self.url)
+        match = self.playervars_re.search(res.text)
+        if match:
+            stream_url = match.group(2)
+            log.debug("URL={0}".format(stream_url))
+            return HLSStream.parse_variant_playlist(self.session, stream_url)
+
+
+__plugin__ = GalatasarayTV
diff --git a/src/streamlink/plugins/latina.py b/src/streamlink/plugins/latina.py
new file mode 100644
index 00000000..4fcc9cd2
--- /dev/null
+++ b/src/streamlink/plugins/latina.py
@@ -0,0 +1,40 @@
+import logging
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import useragents
+from streamlink.plugin.api.utils import itertags
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+class Latina(Plugin):
+    _url_re = re.compile(r"https?://(?:www\.)?latina\.pe/tvenvivo")
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def get_title(self):
+        return "Latina"
+
+    def _get_streams(self):
+        self.session.http.headers.update({
+            "User-Agent": useragents.CHROME,
+            "Referer": self.url})
+
+        self.session.http.get(self.url)
+        stream_url = None
+        for div in itertags(self.session.http.get(self.url).text, "div"):
+            if div.attributes.get("id") == "player":
+                stream_url = div.attributes.get("data-stream")
+
+        if stream_url:
+            log.debug("URL={0}".format(stream_url))
+            return HLSStream.parse_variant_playlist(self.session,
+                                                    stream_url,
+                                                    name_fmt="{pixels}_{bitrate}")
+
+
+__plugin__ = Latina
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
index 80a6ad4e..969709d2 100644
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -99,8 +99,11 @@ _user_schema = validate.Schema(
 _stream_schema = validate.Schema(
     {
         "stream": validate.any(None, {
-            "stream_type": validate.any("live", "rerun"),
-            "broadcast_platform": validate.any("live", "rerun")
+            "stream_type": validate.text,
+            "broadcast_platform": validate.text,
+            "channel": validate.any(None, {
+                "broadcaster_software": validate.text
+            })
         })
     },
     validate.get("stream")
@@ -709,7 +712,11 @@ class Twitch(Plugin):
     def _check_for_rerun(self):
         stream = self.api.streams(self.channel_id, schema=_stream_schema)
 
-        return stream and (stream["stream_type"] == "rerun" or stream["broadcast_platform"] == "rerun")
+        return stream and (
+            stream["stream_type"] != "live"
+            or stream["broadcast_platform"] == "rerun"
+            or stream["channel"] and stream["channel"]["broadcaster_software"] == "watch_party_rerun"
+        )
 
     def _get_hls_streams(self, stream_type="live"):
         log.debug("Getting {0} HLS streams for {1}".format(stream_type, self.channel))
diff --git a/src/streamlink_cli/output.py b/src/streamlink_cli/output.py
index e04471be..c8f2b3f0 100644
--- a/src/streamlink_cli/output.py
+++ b/src/streamlink_cli/output.py
@@ -190,7 +190,7 @@ class PlayerOutput(Output):
             if self.player_name == "mpv":
                 # see https://mpv.io/manual/stable/#property-expansion, allow escaping with \$, respect mpv's $>
                 self.title = self._mpv_title_escape(self.title)
-                extra_args.extend(["--title", self.title])
+                extra_args.append("--title={}".format(self.title))
 
             # potplayer
             if self.player_name == "potplayer":
@@ -202,7 +202,7 @@ class PlayerOutput(Output):
 
         args = self.args.format(filename=filename)
         cmd = self.cmd
-        
+
         # player command
         if is_win32:
             eargs = maybe_decode(subprocess.list2cmdline(extra_args))


Test patch:
diff --git a/tests/plugins/test_galatasaraytv.py b/tests/plugins/test_galatasaraytv.py
new file mode 100644
index 00000000..be354541
--- /dev/null
+++ b/tests/plugins/test_galatasaraytv.py
@@ -0,0 +1,22 @@
+import unittest
+
+from streamlink.plugins.galatasaraytv import GalatasarayTV
+
+
+class TestPluginGalatasarayTV(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'http://galatasaray.com/',
+            'https://galatasaray.com',
+            'https://galatasaray.com/',
+            'https://www.galatasaray.com/',
+        ]
+        for url in should_match:
+            self.assertTrue(GalatasarayTV.can_handle_url(url))
+
+    def test_can_handle_url_negative(self):
+        should_not_match = [
+            'https://example.com/index.html',
+        ]
+        for url in should_not_match:
+            self.assertFalse(GalatasarayTV.can_handle_url(url))
diff --git a/tests/plugins/test_latina.py b/tests/plugins/test_latina.py
new file mode 100644
index 00000000..6a312996
--- /dev/null
+++ b/tests/plugins/test_latina.py
@@ -0,0 +1,19 @@
+import unittest
+
+from streamlink.plugins.latina import Latina
+
+
+class TestPluginLatina(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'http://www.latina.pe/tvenvivo/',
+        ]
+        for url in should_match:
+            self.assertTrue(Latina.can_handle_url(url), url)
+
+    def test_can_handle_url_negative(self):
+        should_not_match = [
+            'http://www.latina.pe/',
+        ]
+        for url in should_not_match:
+            self.assertFalse(Latina.can_handle_url(url), url)
diff --git a/tests/plugins/test_twitch.py b/tests/plugins/test_twitch.py
index 849d2533..3c74d2db 100644
--- a/tests/plugins/test_twitch.py
+++ b/tests/plugins/test_twitch.py
@@ -208,8 +208,11 @@ class TestTwitchReruns(unittest.TestCase):
             mocked_stream = mock.get(
                 "https://api.twitch.tv/kraken/streams/1234.json",
                 json={"stream": None} if params.pop("offline", False) else {"stream": {
-                    "stream_type": params.pop("stream_type", "rerun"),
-                    "broadcast_platform": params.pop("broadcast_platform", "rerun")
+                    "stream_type": params.pop("stream_type", "live"),
+                    "broadcast_platform": params.pop("broadcast_platform", "live"),
+                    "channel": {
+                        "broadcaster_software": params.pop("broadcaster_software", "")
+                    }
                 }}
             )
 
@@ -220,13 +223,21 @@ class TestTwitchReruns(unittest.TestCase):
             try:
                 streams = plugin.streams()
             except TestTwitchReruns.StopError:
-                streams = None
+                streams = True
                 pass
 
             return streams, mocked_users, mocked_stream, mocked[0]
 
-    def test_disable_reruns(self, mock_log):
-        streams, api_users, api_stream, access_token = self.start(stream_type="rerun", broadcast_platform="rerun")
+    def test_disable_reruns_live(self, mock_log):
+        streams, api_users, api_stream, access_token = self.start()
+        self.assertTrue(api_users.called_once)
+        self.assertTrue(api_stream.called_once)
+        self.assertTrue(access_token.called_once)
+        self.assertTrue(streams)
+        self.assertNotIn(self.log_call, mock_log.info.call_args_list)
+
+    def test_disable_reruns_not_live(self, mock_log):
+        streams, api_users, api_stream, access_token = self.start(stream_type="rerun")
         self.assertTrue(api_users.called_once)
         self.assertTrue(api_stream.called_once)
         self.assertFalse(access_token.called)
@@ -249,20 +260,20 @@ class TestTwitchReruns(unittest.TestCase):
         self.assertDictEqual(streams, {})
         self.assertIn(self.log_call, mock_log.info.call_args_list)
 
-    def test_disable_reruns_live(self, mock_log):
-        streams, api_users, api_stream, access_token = self.start(stream_type="live", broadcast_platform="live")
+    def test_disable_reruns_broadcaster_software(self, mock_log):
+        streams, api_users, api_stream, access_token = self.start(broadcaster_software="watch_party_rerun")
         self.assertTrue(api_users.called_once)
         self.assertTrue(api_stream.called_once)
         self.assertTrue(access_token.called_once)
-        self.assertIsNone(streams)
-        self.assertNotIn(self.log_call, mock_log.info.call_args_list)
+        self.assertDictEqual(streams, {})
+        self.assertIn(self.log_call, mock_log.info.call_args_list)
 
     def test_disable_reruns_offline(self, mock_log):
         streams, api_users, api_stream, access_token = self.start(offline=True)
         self.assertTrue(api_users.called_once)
         self.assertTrue(api_stream.called_once)
         self.assertTrue(access_token.called_once)
-        self.assertIsNone(streams)
+        self.assertTrue(streams)
         self.assertNotIn(self.log_call, mock_log.info.call_args_list)
 
     def test_enable_reruns(self, mock_log):
@@ -270,5 +281,5 @@ class TestTwitchReruns(unittest.TestCase):
         self.assertFalse(api_users.called)
         self.assertFalse(api_stream.called)
         self.assertTrue(access_token.called_once)
-        self.assertIsNone(streams)
+        self.assertTrue(streams)
         self.assertNotIn(self.log_call, mock_log.info.call_args_list)
diff --git a/tests/test_cmdline_title.py b/tests/test_cmdline_title.py
index 2c6a9fac..61cb53b8 100644
--- a/tests/test_cmdline_title.py
+++ b/tests/test_cmdline_title.py
@@ -26,7 +26,7 @@ class TestCommandLineWithTitlePOSIX(CommandLineTestCase):
 
     def test_open_player_with_title_mpv(self):
         self._test_args(["streamlink", "-p", "/usr/bin/mpv", "--title", "{title}", "http://test.se", "test"],
-                        ["/usr/bin/mpv", "--title", 'Test Title', "-"])
+                        ["/usr/bin/mpv", "--title=Test Title", "-"])
 
 
 @unittest.skipIf(not is_win32, "test only applicable on Windows")