Repository: streamlink/streamlink
Pull request #850 (2017-04-26 11:32:42)

plugin.tvplayer: update to support new site layout
As pointed out in #849 @nicr4wks the tvplayer.com website has been updated. This PR updates the plugin to support the changes. 

beardypig: Ooops forgot about the tests :-)

Patch:
diff --git a/src/streamlink/plugins/tvplayer.py b/src/streamlink/plugins/tvplayer.py
index a7feeda7..13464272 100644
--- a/src/streamlink/plugins/tvplayer.py
+++ b/src/streamlink/plugins/tvplayer.py
@@ -8,25 +8,32 @@ from streamlink.stream import HLSStream
 
 
 class TVPlayer(Plugin):
+    context_url = "http://tvplayer.com/watch/context"
     api_url = "http://api.tvplayer.com/api/v2/stream/live"
     login_url = "https://tvplayer.com/account/login"
     update_url = "https://tvplayer.com/account/update-detail"
     dummy_postcode = "SE1 9LT"  # location of ITV HQ in London
 
     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)
+    stream_attrs_re = re.compile(r'data-(resource|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")),
-                    validate.optional("drmToken"): validate.any(None, validate.text)
-                })
+                "stream": validate.url(scheme=validate.any("http")),
+                validate.optional("drmToken"): validate.any(None, validate.text)
             })
-        },
+        })
+    },
         validate.get("tvplayer"),
         validate.get("response"))
+    context_schema = validate.Schema({
+        "validate": validate.text,
+        "platform": {
+            "key": validate.text
+        }
+    })
     options = PluginOptions({
         "email": None,
         "password": None
@@ -50,6 +57,21 @@ class TVPlayer(Plugin):
         # there is a 302 redirect on a successful login
         return res2.status_code == 302
 
+    def _get_stream_data(self, resource, token, service=1):
+        # Get the context info (validation token and platform)
+        context_res = http.get(self.context_url, params={"resource": resource,
+                                                         "nonce": token})
+        context_data = http.json(context_res, schema=self.context_schema)
+
+        # get the stream urls
+        res = http.post(self.api_url, data=dict(
+            service=service,
+            id=resource,
+            validate=context_data["validate"],
+            platform=context_data["platform"]["key"]))
+
+        return http.json(res, schema=self.stream_schema)
+
     def _get_streams(self):
         if self.get_option("email") and self.get_option("password"):
             self.authenticate(self.get_option("email"), self.get_option("password"))
@@ -67,25 +89,19 @@ class TVPlayer(Plugin):
 
         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(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=self.stream_schema)
-
-            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"])
+        if "resource" in stream_attrs and "token" in stream_attrs:
+            stream_data = self._get_stream_data(**stream_attrs)
+
+            if stream_data:
+                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")
+                self.logger.error(
+                    "You need to login using --tvplayer-email/--tvplayer-password to view this stream")
 
 
 __plugin__ = TVPlayer


Test patch:
diff --git a/tests/test_plugin_tvplayer.py b/tests/test_plugin_tvplayer.py
index 13c7c363..52f27dc0 100644
--- a/tests/test_plugin_tvplayer.py
+++ b/tests/test_plugin_tvplayer.py
@@ -1,4 +1,3 @@
-import json
 import unittest
 
 from streamlink import Streamlink
@@ -31,28 +30,28 @@ class TestPluginTVPlayer(unittest.TestCase):
         self.assertFalse(TVPlayer.can_handle_url("http://www.tvcatchup.com/"))
         self.assertFalse(TVPlayer.can_handle_url("http://www.youtube.com/"))
 
+    @patch('streamlink.plugins.tvplayer.TVPlayer._get_stream_data')
     @patch('streamlink.plugins.tvplayer.http')
     @patch('streamlink.plugins.tvplayer.HLSStream')
-    def test_get_streams(self, hlsstream, mock_http):
-        api_data = {
-            "tvplayer": {
-                "status": "200 OK",
-                "response": {
+    def test_get_streams(self, hlsstream, mock_http, mock_get_stream_data):
+        mock_get_stream_data.return_value = {
                     "stream": "http://test.se/stream1"
                 }
-            }
-        }
+
         page_resp = Mock()
         page_resp.text = u"""
-        var validate = "foo";
-        var resourceId = "1234";
-        var platform = "test";
+                    <div class="video-js theoplayer-skin theo-seekbar-above-controls content-box vjs-fluid"
+                 data-resource= "89"
+                 data-token = "1324567894561268987948596154656418448489159"
+                                    data-content-type="live"
+                    data-environment="live"
+                    data-subscription="free"
+                    data-channel-id="89">
+                <div id="channel-info" class="channel-info">
+                    <div class="row visible-xs visible-sm">
         """
-        api_resp = Mock()
-        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["tvplayer"]["response"]
         hlsstream.parse_variant_playlist.return_value = {"test": HLSStream(self.session, "http://test.se/stream1")}
 
         plugin = TVPlayer("http://tvplayer.com/watch/dave")
@@ -64,11 +63,7 @@ class TestPluginTVPlayer(unittest.TestCase):
         # test the url is used correctly
         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(service=1,
-                                                                                                  id=u"1234",
-                                                                                                  validate=u"foo",
-                                                                                                  platform=u"test",
-                                                                                                  token=None))
+        mock_get_stream_data.assert_called_with(resource="89", token="1324567894561268987948596154656418448489159")
         # test that the correct URL is used for the HLSStream
         hlsstream.parse_variant_playlist.assert_called_with(ANY, "http://test.se/stream1")