Repository: streamlink/streamlink
Pull request #1886 (2018-06-30 21:53:19)

Handle unicode log message in Python 2
Using the `maybe_{en,de}code` methods from #1576 to handle log messages that might be unicode in Python 2.7.  


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index edf47b74..b2b81939 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -239,6 +239,7 @@ tvp                     tvpstream.vod.tvp.pl Yes   No    Streams may be geo-rest
 tvplayer                tvplayer.com         Yes   No    Streams may be geo-restricted to Great Britain. Premium streams are not supported.
 tvrby                   tvr.by               Yes   No    Streams may be geo-restricted to Belarus.
 tvrplus                 tvrplus.ro           Yes   No    Streams may be geo-restricted to Romania.
+tvtoya                  tvtoya.pl            Yes   --
 twitch                  twitch.tv            Yes   Yes   Possible to authenticate for access to
                                                          subscription streams.
 ustreamtv               ustream.tv           Yes   Yes
diff --git a/src/streamlink/logger.py b/src/streamlink/logger.py
index 945956fa..4e13997c 100644
--- a/src/streamlink/logger.py
+++ b/src/streamlink/logger.py
@@ -1,11 +1,11 @@
 import logging
-import warnings
-
 import sys
+import warnings
+from logging import NOTSET, ERROR, WARN, INFO, DEBUG, CRITICAL
 from threading import Lock
 
 from streamlink.compat import is_py2
-from logging import NOTSET, ERROR, WARN, INFO, DEBUG, CRITICAL
+from streamlink.utils.encoding import maybe_encode
 
 TRACE = 5
 _levelToName = dict([(CRITICAL, "critical"), (ERROR, "error"), (WARN, "warning"), (INFO, "info"), (DEBUG, "debug"),
@@ -40,10 +40,10 @@ class _LogRecord(_CompatLogRecord):
         Return the message for this LogRecord after merging any user-supplied
         arguments with the message.
         """
-        msg = str(self.msg)
+        msg = self.msg
         if self.args:
             msg = msg.format(*self.args)
-        return msg
+        return maybe_encode(msg)
 
 
 class StreamlinkLogger(logging.getLoggerClass(), object):
diff --git a/src/streamlink/plugins/tvtoya.py b/src/streamlink/plugins/tvtoya.py
new file mode 100644
index 00000000..a5b04583
--- /dev/null
+++ b/src/streamlink/plugins/tvtoya.py
@@ -0,0 +1,35 @@
+import logging
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http, useragents
+from streamlink.stream import HLSStream
+from streamlink.utils import update_scheme
+
+log = logging.getLogger(__name__)
+
+
+class TVToya(Plugin):
+    _url_re = re.compile(r"https?://tvtoya.pl/live")
+    _playlist_re = re.compile(r'data-stream="([^"]+)"')
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def _get_streams(self):
+        self.session.set_option('hls-live-edge', 10)
+        res = http.get(self.url)
+        playlist_m = self._playlist_re.search(res.text)
+
+        if playlist_m:
+            return HLSStream.parse_variant_playlist(
+                self.session,
+                update_scheme(self.url, playlist_m.group(1)),
+                headers={'Referer': self.url, 'User-Agent': useragents.ANDROID}
+            )
+        else:
+            log.debug("Could not find stream data")
+
+
+__plugin__ = TVToya
\ No newline at end of file
diff --git a/src/streamlink/utils/encoding.py b/src/streamlink/utils/encoding.py
new file mode 100644
index 00000000..31ab1f75
--- /dev/null
+++ b/src/streamlink/utils/encoding.py
@@ -0,0 +1,18 @@
+from streamlink.compat import is_py2
+
+
+def maybe_encode(text, encoding="utf8"):
+    if is_py2:
+        return text.encode(encoding)
+    else:
+        return text
+
+
+def maybe_decode(text, encoding="utf8"):
+    if is_py2 and isinstance(text, str):
+        return text.decode(encoding)
+    else:
+        return text
+
+
+__all__ = ["maybe_decode", "maybe_encode"]
\ No newline at end of file
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 24da2fe1..ff909d59 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -1,3 +1,4 @@
+import argparse
 import errno
 import logging
 import os
@@ -860,19 +861,20 @@ def setup_plugin_options(session, plugin):
     pname = plugin.module
     required = OrderedDict({})
     for parg in plugin.arguments:
-        if parg.required:
-            required[parg.name] = parg
-        value = getattr(args, parg.namespace_dest(pname))
-        session.set_plugin_option(pname, parg.dest, value)
-        # if the value is set, check to see if any of the required arguments are not set
-        if parg.required or value:
-            try:
-                for rparg in plugin.arguments.requires(parg.name):
-                    required[rparg.name] = rparg
-            except RuntimeError:
-                console.logger.error("{0} plugin has a configuration error and the arguments "
-                                     "cannot be parsed".format(pname))
-                break
+        if parg.options.get("help") != argparse.SUPPRESS:
+            if parg.required:
+                required[parg.name] = parg
+            value = getattr(args, parg.namespace_dest(pname))
+            session.set_plugin_option(pname, parg.dest, value)
+            # if the value is set, check to see if any of the required arguments are not set
+            if parg.required or value:
+                try:
+                    for rparg in plugin.arguments.requires(parg.name):
+                        required[rparg.name] = rparg
+                except RuntimeError:
+                    console.logger.error("{0} plugin has a configuration error and the arguments "
+                                         "cannot be parsed".format(pname))
+                    break
     if required:
         for req in required.values():
             if not session.get_plugin_option(pname, req.dest):


Test patch:
diff --git a/tests/plugins/test_tvtoya.py b/tests/plugins/test_tvtoya.py
new file mode 100644
index 00000000..e55bc11c
--- /dev/null
+++ b/tests/plugins/test_tvtoya.py
@@ -0,0 +1,16 @@
+import unittest
+
+from streamlink.plugins.tvtoya import TVToya
+
+
+class TestPluginTVRPlus(unittest.TestCase):
+    def test_can_handle_url(self):
+        self.assertTrue(TVToya.can_handle_url("https://tvtoya.pl/live"))
+        self.assertTrue(TVToya.can_handle_url("http://tvtoya.pl/live"))
+
+    def test_can_handle_url_negative(self):
+        self.assertFalse(TVToya.can_handle_url("https://tvtoya.pl"))
+        self.assertFalse(TVToya.can_handle_url("http://tvtoya.pl"))
+        self.assertFalse(TVToya.can_handle_url("http://tvtoya.pl/other-page"))
+        self.assertFalse(TVToya.can_handle_url("http://tvtoya.pl/"))
+        self.assertFalse(TVToya.can_handle_url("https://tvtoya.pl/"))
diff --git a/tests/test_log.py b/tests/test_log.py
index 2196ae6a..99f13ce2 100644
--- a/tests/test_log.py
+++ b/tests/test_log.py
@@ -1,17 +1,18 @@
+# encoding=utf8
 import logging
 import unittest
 import warnings
 
 from streamlink import logger, Streamlink
 from streamlink.compat import is_py2
+from streamlink.utils.encoding import maybe_decode
+from tests import catch_warnings
 
 if is_py2:
     from io import BytesIO as StringIO
 else:
     from io import StringIO
 
-from tests import catch_warnings
-
 
 
 class TestLogging(unittest.TestCase):
@@ -55,7 +56,11 @@ class TestLogging(unittest.TestCase):
         log.debug("test")
         self.assertEqual(output.getvalue(), "[test][debug] test\n")
 
-
+    def test_log_unicode(self):
+        log, output = self._new_logger()
+        logger.root.setLevel("info")
+        log.info(u"Special Character: Ѩ")
+        self.assertEqual(maybe_decode(output.getvalue()), u"[test][info] Special Character: Ѩ\n")
 
 
 class TestDeprecatedLogger(unittest.TestCase):
diff --git a/tests/test_utils_encoding.py b/tests/test_utils_encoding.py
new file mode 100644
index 00000000..123b6195
--- /dev/null
+++ b/tests/test_utils_encoding.py
@@ -0,0 +1,24 @@
+# encoding=utf8
+import unittest
+
+from streamlink.compat import is_py3, is_py2
+from streamlink.utils.encoding import maybe_decode, maybe_encode
+
+
+class TestUtilsEncoding(unittest.TestCase):
+
+    @unittest.skipUnless(is_py2, "only applicable in Python 2")
+    def test_maybe_encode_py2(self):
+        self.assertEqual(maybe_encode(u"test \u07f7"), "test \xdf\xb7")
+
+    @unittest.skipUnless(is_py2, "only applicable in Python 2")
+    def test_maybe_decode_py2(self):
+        self.assertEqual(maybe_decode("test \xdf\xb7"), u"test \u07f7")
+
+    @unittest.skipUnless(is_py3, "only applicable in Python 3")
+    def test_maybe_encode_py3(self):
+        self.assertEqual(maybe_encode(u"test \u07f7"), u"test \u07f7")
+
+    @unittest.skipUnless(is_py3, "only applicable in Python 3")
+    def test_maybe_decode_py3(self):
+        self.assertEqual(maybe_decode(u"test \u07f7"), u"test \u07f7")