Repository: streamlink/streamlink
Pull request #631 (2017-02-27 15:53:51)

plugins.tvplayer: support for authenticated streams
Some streams require a login to view, and others are protected by DRM and cannot be viewed. Two new options were added to support authentication for tvplayer.com:

 - `--tvplayer-email` the email address for your tvplayer.com account
 - `--tvplayer-password` the password for your tvplayer.com account

Non-DRM streams that require a login:

 - tvplayer.com/watch/itv4
 - tvplayer.com/watch/itv2

DRM streams:
 - tvplayer.com/watch/spike
 - tvplayer.com/watch/5star

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


```diff
@@            Coverage Diff             @@
##           master     #631      +/-   ##
==========================================
+ Coverage    48.5%   48.55%   +0.05%     
==========================================
  Files          66       66              
  Lines        5567     5596      +29     
==========================================
+ Hits         2700     2717      +17     
- Misses       2867     2879      +12
```

------

[Continue to review full report at Codecov](https://codecov.io/gh/streamlink/streamlink/pull/631?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/631?src=pr&el=footer). Last update [3fcc84c...8d4cce0](https://codecov.io/gh/streamlink/streamlink/compare/3fcc84c34fcbf52da5f6ccca197355706407444d...8d4cce09c73f90a3e0457ce2aa5e2780010ae83b?el=footer&src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).
gravyboat: Couple more conflicts here then I'll merge, looks good to me.
beardypig: Yeah messing with `argparse` in multiple PRs causes conflicts :-)
gravyboat: Yep, not a big deal. They're easy resolutions!

Patch:
diff --git a/src/streamlink/plugins/tvplayer.py b/src/streamlink/plugins/tvplayer.py
index e2fd0bcb..83ebcc6f 100644
--- a/src/streamlink/plugins/tvplayer.py
+++ b/src/streamlink/plugins/tvplayer.py
@@ -1,49 +1,83 @@
 #!/usr/bin/env python
 import re
 
-from streamlink.plugin import Plugin
+from streamlink.plugin import Plugin, PluginOptions
 from streamlink.plugin.api import http, validate
+from streamlink.plugin.api import useragents
 from streamlink.stream import HLSStream
 
 
 class TVPlayer(Plugin):
-    _user_agent = (
-        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) "
-        "Chrome/43.0.2357.65 Safari/537.36")
     API_URL = "http://api.tvplayer.com/api/v2/stream/live"
-    _url_re = re.compile(r"https?://(?:www.)?tvplayer.com/(:?watch/?|watch/(.+)?)")
-    _stream_attrs_ = re.compile(r'var\s+(validate|platform|resourceId)\s+=\s*(.*?);', re.S)
-    _stream_schema = validate.Schema({
+    LOGIN_URL = "https://tvplayer.com/account/login"
+
+    url_re = re.compile(r"https?://(?:www.)?tvplayer.com/(:?watch/?|watch/(.+)?)")
+    stream_attrs_re = re.compile(r'var\s+(validate|platform|resourceId|token)\s+=\s*(.*?);', re.S)
+    login_token_re = re.compile(r'input.*?name="token".*?value="(\w+)"')
+    stream_schema = validate.Schema({
         "tvplayer": validate.Schema({
             "status": u'200 OK',
             "response": validate.Schema({
-                "stream": validate.url(scheme=validate.any("http"))
+                    "stream": validate.url(scheme=validate.any("http")),
+                    validate.optional("drmToken"): validate.any(None, validate.text)
+                })
             })
-        })
+        },
+        validate.get("tvplayer"),
+        validate.get("response"))
+    options = PluginOptions({
+        "email": None,
+        "password": None
     })
 
     @classmethod
     def can_handle_url(cls, url):
-        match = TVPlayer._url_re.match(url)
+        match = TVPlayer.url_re.match(url)
         return match is not None
 
+    def __init__(self, url):
+        super(TVPlayer, self).__init__(url)
+        http.headers.update({"User-Agent": useragents.CHROME})
+
+    def authenticate(self, username, password):
+        res = http.get(self.LOGIN_URL)
+        match = self.login_token_re.search(res.text)
+        token = match and match.group(1)
+        res2 = http.post(self.LOGIN_URL, data=dict(email=username, password=password, token=token),
+                         allow_redirects=False)
+        # there is a 302 redirect on a successful login
+        return res2.status_code == 302
+
     def _get_streams(self):
+        if self.get_option("email") and self.get_option("password"):
+            self.authenticate(self.get_option("email"), self.get_option("password"))
+
         # find the list of channels from the html in the page
         self.url = self.url.replace("https", "http")  # https redirects to http
-        res = http.get(self.url, headers={"User-Agent": TVPlayer._user_agent})
-        stream_attrs = dict((k, v.strip('"')) for k, v in TVPlayer._stream_attrs_.findall(res.text))
+        res = http.get(self.url)
+
+        stream_attrs = dict((k, v.strip('"')) for k, v in self.stream_attrs_re.findall(res.text))
 
         if "resourceId" in stream_attrs and "validate" in stream_attrs and "platform" in stream_attrs:
             # get the stream urls
-            res = http.post(TVPlayer.API_URL, data=dict(id=stream_attrs["resourceId"],
-                                                        validate=stream_attrs["validate"],
-                                                        platform=stream_attrs["platform"]))
+            res = http.post(self.API_URL, data=dict(
+                service=1,
+                id=stream_attrs["resourceId"],
+                validate=stream_attrs["validate"],
+                platform=stream_attrs["platform"],
+                token=stream_attrs.get("token")))
 
-            stream_data = http.json(res, schema=TVPlayer._stream_schema)
+            stream_data = http.json(res, schema=self.stream_schema)
+            print(stream_data)
 
-            return HLSStream.parse_variant_playlist(self.session,
-                                                    stream_data["tvplayer"]["response"]["stream"],
-                                                    headers={'user-agent': TVPlayer._user_agent})
+            if stream_data.get("drmToken"):
+                self.logger.error("This stream is protected by DRM can cannot be played")
+                return
+            else:
+                return HLSStream.parse_variant_playlist(self.session, stream_data["stream"])
+        else:
+            if "need to login" in res.text:
+                self.logger.error("You need to login using --tvplayer-email/--tvplayer-password to view this stream")
 
 
 __plugin__ = TVPlayer
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index 4dfdfc1a..a298b3bf 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -1128,6 +1128,20 @@ plugin.add_argument(
     Enable automatically including available subtitles in to the output stream
     """
 )
+plugin.add_argument(
+    "--tvplayer-email",
+    metavar="EMAIL",
+    help="""
+    The email address used to register with tvplayer.com.
+    """
+)
+plugin.add_argument(
+    "--tvplayer-password",
+    metavar="PASSWORD",
+    help="""
+    A TVPlayer account password to use with --tvplayer-email.
+    """
+)
 
 # Deprecated options
 stream.add_argument(
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 68c5aed8..1c583d83 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -870,6 +870,17 @@ def setup_plugin_options():
                                                        args.funimation_language.lower())
         streamlink.set_plugin_option("funimationnow", "language", lang)
 
+    if args.tvplayer_email:
+        streamlink.set_plugin_option("tvplayer", "email", args.tvplayer_email)
+
+    if args.tvplayer_email and not args.tvplayer_password:
+        tvplayer_password = console.askpass("Enter TVPlayer password: ")
+    else:
+        tvplayer_password = args.tvplayer_password
+
+    if tvplayer_password:
+        streamlink.set_plugin_option("tvplayer", "password", tvplayer_password)
+
     # Deprecated options
     if args.jtv_legacy_names:
         console.logger.warning("The option --jtv/twitch-legacy-names is "


Test patch:
diff --git a/tests/test_plugin_tvplayer.py b/tests/test_plugin_tvplayer.py
index 7aadb1fd..13c7c363 100644
--- a/tests/test_plugin_tvplayer.py
+++ b/tests/test_plugin_tvplayer.py
@@ -52,7 +52,7 @@ class TestPluginTVPlayer(unittest.TestCase):
         api_resp.text = json.dumps(api_data)
         mock_http.get.return_value = page_resp
         mock_http.post.return_value = api_resp
-        mock_http.json.return_value = api_data
+        mock_http.json.return_value = api_data["tvplayer"]["response"]
         hlsstream.parse_variant_playlist.return_value = {"test": HLSStream(self.session, "http://test.se/stream1")}
 
         plugin = TVPlayer("http://tvplayer.com/watch/dave")
@@ -62,13 +62,15 @@ class TestPluginTVPlayer(unittest.TestCase):
         self.assertTrue("test" in streams)
 
         # test the url is used correctly
-        mock_http.get.assert_called_with("http://tvplayer.com/watch/dave", headers=ANY)
+        mock_http.get.assert_called_with("http://tvplayer.com/watch/dave")
         # test that the correct API call is made
-        mock_http.post.assert_called_with("http://api.tvplayer.com/api/v2/stream/live", data=dict(id=u"1234",
+        mock_http.post.assert_called_with("http://api.tvplayer.com/api/v2/stream/live", data=dict(service=1,
+                                                                                                  id=u"1234",
                                                                                                   validate=u"foo",
-                                                                                                  platform=u"test"))
+                                                                                                  platform=u"test",
+                                                                                                  token=None))
         # test that the correct URL is used for the HLSStream
-        hlsstream.parse_variant_playlist.assert_called_with(ANY, "http://test.se/stream1", headers=ANY)
+        hlsstream.parse_variant_playlist.assert_called_with(ANY, "http://test.se/stream1")
 
     @patch('streamlink.plugins.tvplayer.http')
     def test_get_invalid_page(self, mock_http):
@@ -86,4 +88,5 @@ class TestPluginTVPlayer(unittest.TestCase):
         self.assertEqual({}, streams)
 
         # test the url is used correctly
-        mock_http.get.assert_called_with("http://tvplayer.com/watch/dave", headers=ANY)
+
+        mock_http.get.assert_called_with("http://tvplayer.com/watch/dave")