Repository: streamlink/streamlink

Issue:
plugins.tvplayer: 'Unable to validate result'
<!--
Thanks for reporting a plugin issue!
USE THE TEMPLATE. Otherwise your plugin issue may be rejected.

First, see the contribution guidelines:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Plugin issues describe broken functionality within a plugin's code base, eg. the streaming site has made breaking changes, streams don't get resolved correctly, or authentication has stopped working, etc.

Also check the list of open and closed plugin issues:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Plugin Issue

<!-- Replace the space character between the square brackets with an x in order to check the boxes -->
- [x] This is a plugin issue and I have read the contribution guidelines.
- [x] I am using the latest development version from the master branch.


### Description

Any URL gives
[cli][info] Found matching plugin tvplayer for URL https://tvplayer.com/uk/watch/channel4
error: Unable to validate JSON: Unable to validate key 'response': Unable to validate result: Key 'stream' not found in {'error': 'Userid header must be provided for DRM streams.'}


### Reproduction steps / Explicit stream URLs to test

https://tvplayer.com/uk/watch/channel4

<!-- How can we reproduce this? Please note the exact steps below using the list format supplied. If you need more steps please add them. -->

1. Enter URL


### Log output

<!--
DEBUG LOG OUTPUT IS REQUIRED for a plugin issue!
INCLUDE THE ENTIRE COMMAND LINE and make sure to **remove usernames and passwords**

Use the `--loglevel debug` parameter and avoid using parameters which suppress log output.
Debug log includes important details about your platform. Don't remove it.
https://streamlink.github.io/latest/cli.html#cmdoption-loglevel

You can copy the output to https://gist.github.com/ or paste it below.

Don't post screenshots of the log output and instead copy the text from your terminal application.
-->

```
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.8.7
[cli][debug] Streamlink: 2.1.1
[cli][debug] Requests(2.25.1), Socks(1.7.1), Websocket(0.58.0)
[cli][debug] Arguments:
[cli][debug]  url=https://tvplayer.com/uk/watch/channel4
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --rtmpdump=C:\Program Files (x86)\Streamlink\rtmpdump\rtmpdump.exe
[cli][debug]  --ffmpeg-ffmpeg=C:\Program Files (x86)\Streamlink\ffmpeg\ffmpeg.exe
[cli][info] Found matching plugin tvplayer for URL https://tvplayer.com/uk/watch/channel4
[plugins.tvplayer][debug] Got stream attributes: {'key': '2Pw1Eg0Px7Ey9Jm3Ly8Dr5Ci5Vc5Mk', 'uvid': '752', 'token': 'c56ea19685895a44f85ce3e05ac048c2', 'expiry': '1617196874'}
error: Unable to validate JSON: Unable to validate key 'response': Unable to validate result: Key 'stream' not found in {'error': 'Userid header must be provided for DRM streams.'}
```


### Additional comments, etc.

[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)



Discussion:
Hmm, that's not good.

https://support-uk.tvplayer.com/hc/en-gb/articles/360018031719-I-can-t-watch-some-channels-on-my-browser-not-supported-

<details><summary>M3U8</summary>

```
#EXTM3U
#EXT-X-VERSION:4
## Created with Unified Streaming Platform(version=1.9.5)
#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES,URI="skd://expressplay_token",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"

# AUDIO groups
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aacl-64",NAME="English",LANGUAGE="en",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2"

# variants
#EXT-X-STREAM-INF:BANDWIDTH=658000,AVERAGE-BANDWIDTH=598000,CODECS="mp4a.40.2,avc1.64001E",RESOLUTION=640x360,FRAME-RATE=25,AUDIO="audio-aacl-64",CLOSED-CAPTIONS=NONE
bitrate1-audio_track_0_0_eng=64000-video_track=500000.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1241000,AVERAGE-BANDWIDTH=1128000,CODECS="mp4a.40.2,avc1.64001E",RESOLUTION=768x432,FRAME-RATE=25,AUDIO="audio-aacl-64",CLOSED-CAPTIONS=NONE
bitrate1-audio_track_0_0_eng=64000-video_track=1000000.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2174000,AVERAGE-BANDWIDTH=1976000,CODECS="mp4a.40.2,avc1.64001F",RESOLUTION=896x504,FRAME-RATE=25,AUDIO="audio-aacl-64",CLOSED-CAPTIONS=NONE
bitrate1-audio_track_0_0_eng=64000-video_track=1800000.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2990000,AVERAGE-BANDWIDTH=2718000,CODECS="mp4a.40.2,avc1.64001F",RESOLUTION=1024x576,FRAME-RATE=25,AUDIO="audio-aacl-64",CLOSED-CAPTIONS=NONE
bitrate1-audio_track_0_0_eng=64000-video_track=2500000.m3u8

# keyframes
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=73000,CODECS="avc1.64001E",RESOLUTION=640x360,URI="keyframes/bitrate1-video_track=500000.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=146000,CODECS="avc1.64001E",RESOLUTION=768x432,URI="keyframes/bitrate1-video_track=1000000.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=263000,CODECS="avc1.64001F",RESOLUTION=896x504,URI="keyframes/bitrate1-video_track=1800000.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=365000,CODECS="avc1.64001F",RESOLUTION=1024x576,URI="keyframes/bitrate1-video_track=2500000.m3u8"
```
</details>
<details><summary>MPD</summary>

```
<?xml version="1.0" encoding="utf-8"?>
<!-- Created with Unified Streaming Platform(version=1.9.5) -->
<MPD
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="urn:mpeg:dash:schema:mpd:2011"
  xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
  xmlns:cenc="urn:mpeg:cenc:2013"
  type="dynamic"
  availabilityStartTime="1970-01-01T00:00:00Z"
  publishTime="2021-03-30T16:39:17.628236Z"
  minimumUpdatePeriod="PT2S"
  timeShiftBufferDepth="PT30S"
  maxSegmentDuration="PT3S"
  minBufferTime="PT10S"
  profiles="urn:mpeg:dash:profile:isoff-live:2011,urn:com:dashif:dash264">
  <Period
    id="1"
    start="PT0S">
    <BaseURL>dash/</BaseURL>
    <AdaptationSet
      id="1"
      group="1"
      contentType="audio"
      lang="en"
      segmentAlignment="true"
      audioSamplingRate="48000"
      mimeType="audio/mp4"
      codecs="mp4a.40.2"
      startWithSAP="1">
      <AudioChannelConfiguration
        schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011"
        value="2">
      </AudioChannelConfiguration>
      <!-- Common Encryption -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        xmlns:cenc="urn:mpeg:cenc:2013"
        schemeIdUri="urn:mpeg:dash:mp4protection:2011"
        value="cenc"
        cenc:default_KID="4963656C-616E-6469-6320-54656C657669">
      </ContentProtection>
      <!-- PlayReady -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        schemeIdUri="urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95"
        value="MSPR 2.0">
      </ContentProtection>
      <!-- Widevine -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        schemeIdUri="urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED">
      </ContentProtection>
      <!-- Marlin -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        schemeIdUri="urn:uuid:5E629AF5-38DA-4063-8977-97FFBD9902D4">
        <MarlinContentIds xmlns="urn:marlin:mas:1-0:services:schemas:mpd">
          <MarlinContentId>urn:marlin:kid:4963656c616e6469632054656c657669</MarlinContentId>
        </MarlinContentIds>
      </ContentProtection>
      <Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
      <SegmentTemplate
        timescale="48000"
        initialization="bitrate1-$RepresentationID$.dash"
        media="bitrate1-$RepresentationID$-$Time$.dash">
        <!-- 2021-03-30T16:38:35.664000Z / 1617122315 - 2021-03-30T16:39:05.658687Z -->
        <SegmentTimeline>
          <S t="77621871151872" d="96257" />
          <S d="96256" />
          <S d="95232" />
          <S d="96255" />
          <S d="96257" />
          <S d="96256" />
          <S d="95232" />
          <S d="96255" />
          <S d="96257" />
          <S d="96256" />
          <S d="95232" />
          <S d="96255" />
          <S d="96257" />
          <S d="96256" />
          <S d="95232" />
        </SegmentTimeline>
      </SegmentTemplate>
      <Representation
        id="audio_track_0_0_eng=64000"
        bandwidth="64000">
      </Representation>
    </AdaptationSet>
    <AdaptationSet
      id="2"
      group="2"
      contentType="video"
      par="16:9"
      minBandwidth="500000"
      maxBandwidth="2500000"
      maxWidth="1024"
      maxHeight="576"
      segmentAlignment="true"
      sar="1:1"
      frameRate="25"
      mimeType="video/mp4"
      startWithSAP="1">
      <!-- Common Encryption -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        xmlns:cenc="urn:mpeg:cenc:2013"
        schemeIdUri="urn:mpeg:dash:mp4protection:2011"
        value="cenc"
        cenc:default_KID="4963656C-616E-6469-6320-54656C657669">
      </ContentProtection>
      <!-- PlayReady -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        schemeIdUri="urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95"
        value="MSPR 2.0">
      </ContentProtection>
      <!-- Widevine -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        schemeIdUri="urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED">
      </ContentProtection>
      <!-- Marlin -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        schemeIdUri="urn:uuid:5E629AF5-38DA-4063-8977-97FFBD9902D4">
        <MarlinContentIds xmlns="urn:marlin:mas:1-0:services:schemas:mpd">
          <MarlinContentId>urn:marlin:kid:4963656c616e6469632054656c657669</MarlinContentId>
        </MarlinContentIds>
      </ContentProtection>
      <Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
      <SegmentTemplate
        timescale="600"
        initialization="bitrate1-$RepresentationID$.dash"
        media="bitrate1-$RepresentationID$-$Time$.dash">
        <!-- 2021-03-30T16:38:37.655000Z / 1617122317 - 2021-03-30T16:39:07.655000Z -->
        <SegmentTimeline>
          <S t="970273390593" d="1200" r="14" />
        </SegmentTimeline>
      </SegmentTemplate>
      <Representation
        id="video_track=500000"
        bandwidth="500000"
        width="640"
        height="360"
        codecs="avc1.64001E"
        scanType="progressive">
      </Representation>
      <Representation
        id="video_track=1000000"
        bandwidth="1000000"
        width="768"
        height="432"
        codecs="avc1.64001E"
        scanType="progressive">
      </Representation>
      <Representation
        id="video_track=1800000"
        bandwidth="1800000"
        width="896"
        height="504"
        codecs="avc1.64001F"
        scanType="progressive">
      </Representation>
      <Representation
        id="video_track=2500000"
        bandwidth="2500000"
        width="1024"
        height="576"
        codecs="avc1.64001F"
        scanType="progressive">
      </Representation>
    </AdaptationSet>
    <AdaptationSet
      id="3"
      group="2"
      contentType="video"
      par="16:9"
      minBandwidth="500000"
      maxBandwidth="2500000"
      maxWidth="1024"
      maxHeight="576"
      segmentAlignment="true"
      sar="1:1"
      frameRate="1/2"
      mimeType="video/mp4"
      startWithSAP="1"
      maxPlayoutRate="50"
      codingDependency="false">
      <!-- Common Encryption -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        xmlns:cenc="urn:mpeg:cenc:2013"
        schemeIdUri="urn:mpeg:dash:mp4protection:2011"
        value="cenc"
        cenc:default_KID="4963656C-616E-6469-6320-54656C657669">
      </ContentProtection>
      <!-- PlayReady -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        schemeIdUri="urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95"
        value="MSPR 2.0">
      </ContentProtection>
      <!-- Widevine -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        schemeIdUri="urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED">
      </ContentProtection>
      <!-- Marlin -->
      <ContentProtection
        xmlns="urn:mpeg:dash:schema:mpd:2011"
        schemeIdUri="urn:uuid:5E629AF5-38DA-4063-8977-97FFBD9902D4">
        <MarlinContentIds xmlns="urn:marlin:mas:1-0:services:schemas:mpd">
          <MarlinContentId>urn:marlin:kid:4963656c616e6469632054656c657669</MarlinContentId>
        </MarlinContentIds>
      </ContentProtection>
      <EssentialProperty
        schemeIdUri="http://dashif.org/guidelines/trickmode"
        value="2">
      </EssentialProperty>
      <Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
      <SegmentTemplate
        timescale="600"
        initialization="bitrate1-$RepresentationID$.dash"
        media="bitrate1-$RepresentationID$-$Time$.dash">
        <!-- 2021-03-30T16:38:37.655000Z / 1617122317 - 2021-03-30T16:39:07.655000Z -->
        <SegmentTimeline>
          <S t="970273390593" d="1200" r="14" />
        </SegmentTimeline>
      </SegmentTemplate>
      <Representation
        id="video_track=500000(mode=trik)"
        bandwidth="500000"
        width="640"
        height="360"
        codecs="avc1.64001E"
        scanType="progressive">
      </Representation>
      <Representation
        id="video_track=1000000(mode=trik)"
        bandwidth="1000000"
        width="768"
        height="432"
        codecs="avc1.64001E"
        scanType="progressive">
      </Representation>
      <Representation
        id="video_track=1800000(mode=trik)"
        bandwidth="1800000"
        width="896"
        height="504"
        codecs="avc1.64001F"
        scanType="progressive">
      </Representation>
      <Representation
        id="video_track=2500000(mode=trik)"
        bandwidth="2500000"
        width="1024"
        height="576"
        codecs="avc1.64001F"
        scanType="progressive">
      </Representation>
    </AdaptationSet>
  </Period>
  <UTCTiming
    schemeIdUri="urn:mpeg:dash:utc:http-iso:2014"
    value="https://time.akamai.com/?iso" />
</MPD>
```
</details>


Resolving patch:
diff --git a/src/streamlink/plugins/tvplayer.py b/src/streamlink/plugins/tvplayer.py
deleted file mode 100644
--- a/src/streamlink/plugins/tvplayer.py
+++ /dev/null
@@ -1,141 +0,0 @@
-import logging
-import re
-
-from streamlink.plugin import Plugin, PluginArgument, PluginArguments
-from streamlink.plugin.api import useragents, validate
-from streamlink.stream import HLSStream
-
-log = logging.getLogger(__name__)
-
-
-class TVPlayer(Plugin):
-    api_url = "https://v1-streams-elb.tvplayer-cdn.com/api/live/stream/"
-    stream_url = "https://live.tvplayer.com/stream-live.php"
-    login_url = "https://v1-auth.tvplayer-cdn.com/login?responseType=redirect&redirectUri=https://tvplayer.com/login&lang=en"
-    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/(:?uk/)?(:?watch/?|watch/(.+)?)")
-    stream_attrs_re = re.compile(r'data-player-(expiry|key|token|uvid)\s*=\s*"(.*?)"', re.S)
-    login_token_re = re.compile(r'input.*?name="_token".*?value="(\w+)"')
-    stream_schema = validate.Schema({
-        "response": validate.Schema({
-            "stream": validate.any(None, validate.text),
-            "drm": validate.any(None, validate.text)
-        })
-    })
-    arguments = PluginArguments(
-        PluginArgument(
-            "email",
-            help="The email address used to register with tvplayer.com.",
-            metavar="EMAIL",
-            requires=["password"]
-        ),
-        PluginArgument(
-            "password",
-            sensitive=True,
-            help="The password for your tvplayer.com account.",
-            metavar="PASSWORD"
-        )
-    )
-
-    @classmethod
-    def can_handle_url(cls, url):
-        match = TVPlayer.url_re.match(url)
-        return match is not None
-
-    def __init__(self, url):
-        super().__init__(url)
-        self.session.http.headers.update({"User-Agent": useragents.CHROME})
-
-    def authenticate(self, username, password):
-        res = self.session.http.get(self.login_url)
-        match = self.login_token_re.search(res.text)
-        token = match and match.group(1)
-        res2 = self.session.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_stream_data(self, expiry, key, token, uvid):
-        res = self.session.http.get(
-            self.api_url + uvid,
-            params=dict(key=key, platform="chrome"),
-            headers={
-                "Token": token,
-                "Token-Expiry": expiry,
-                "Uvid": uvid
-            }
-        )
-
-        res_schema = self.session.http.json(res, schema=self.stream_schema)
-
-        if res_schema["response"]["stream"] is None:
-            res = self.session.http.get(
-                self.stream_url,
-                params=dict(key=key, platform="chrome"),
-                headers={
-                    "Token": token,
-                    "Token-Expiry": expiry,
-                    "Uvid": uvid
-                }
-            ).json()
-            res_schema["response"]["stream"] = res["Streams"]["Adaptive"]
-
-        return res_schema
-
-    def _get_stream_attrs(self, page):
-        stream_attrs = {k.replace("-", "_"): v.strip('"') for k, v in self.stream_attrs_re.findall(page.text)}
-
-        log.debug(f"Got stream attributes: {str(stream_attrs)}")
-        valid = True
-        for a in ("expiry", "key", "token", "uvid"):
-            if a not in stream_attrs:
-                log.debug(f"Missing '{a}' from stream attributes")
-                valid = False
-
-        return stream_attrs if valid else {}
-
-    def _get_streams(self):
-        if self.get_option("email") and self.get_option("password"):
-            log.debug("Logging in as {0}".format(self.get_option("email")))
-            if not self.authenticate(self.get_option("email"), self.get_option("password")):
-                log.warning("Failed to login as {0}".format(self.get_option("email")))
-
-        # find the list of channels from the html in the page
-        self.url = self.url.replace("https", "http")  # https redirects to http
-        res = self.session.http.get(self.url)
-
-        if "enter your postcode" in res.text:
-            log.info(
-                f"Setting your postcode to: {self.dummy_postcode}. "
-                f"This can be changed in the settings on tvplayer.com"
-            )
-            res = self.session.http.post(
-                self.update_url,
-                data=dict(postcode=self.dummy_postcode),
-                params=dict(return_url=self.url)
-            )
-
-        stream_attrs = self._get_stream_attrs(res)
-        if stream_attrs:
-            stream_data = self._get_stream_data(**stream_attrs)
-
-            if stream_data:
-                if stream_data["response"]["drm"] is not None:
-                    log.error("This stream is protected by DRM can cannot be played")
-                    return
-                else:
-                    return HLSStream.parse_variant_playlist(
-                        self.session, stream_data["response"]["stream"]
-                    )
-        else:
-            if "need to login" in res.text:
-                log.error(
-                    "You need to login using --tvplayer-email/--tvplayer-password to view this stream")
-
-
-__plugin__ = TVPlayer


Test patch:
diff --git a/tests/plugins/test_tvplayer.py b/tests/plugins/test_tvplayer.py
deleted file mode 100644
--- a/tests/plugins/test_tvplayer.py
+++ /dev/null
@@ -1,143 +0,0 @@
-import unittest
-from unittest.mock import ANY, MagicMock, Mock, call, patch
-
-from streamlink import Streamlink
-from streamlink.plugin.api import HTTPSession
-from streamlink.plugins.tvplayer import TVPlayer
-from streamlink.stream import HLSStream
-from tests.plugins import PluginCanHandleUrl
-
-
-class TestPluginCanHandleUrlTVPlayer(PluginCanHandleUrl):
-    __plugin__ = TVPlayer
-
-    should_match = [
-        "http://tvplayer.com/watch/",
-        "http://www.tvplayer.com/watch/",
-        "http://tvplayer.com/watch",
-        "http://www.tvplayer.com/watch",
-        "http://www.tvplayer.com/uk/watch",
-        "http://tvplayer.com/watch/dave",
-        "http://tvplayer.com/uk/watch/dave",
-        "http://www.tvplayer.com/watch/itv",
-        "http://www.tvplayer.com/uk/watch/itv",
-        "https://www.tvplayer.com/watch/itv",
-        "https://www.tvplayer.com/uk/watch/itv",
-        "https://tvplayer.com/watch/itv",
-        "https://tvplayer.com/uk/watch/itv",
-    ]
-
-    should_not_match = [
-        "http://www.tvplayer.com/"
-    ]
-
-
-class TestPluginTVPlayer(unittest.TestCase):
-    def setUp(self):
-        self.session = Streamlink()
-        self.session.http = MagicMock(HTTPSession)
-        self.session.http.headers = {}
-
-    @patch('streamlink.plugins.tvplayer.TVPlayer._get_stream_data')
-    @patch('streamlink.plugins.tvplayer.HLSStream')
-    def test_get_streams(self, hlsstream, mock_get_stream_data):
-        mock_get_stream_data.return_value = {
-            "response": {"stream": "http://test.se/stream1", "drm": None}
-        }
-
-        page_resp = Mock()
-        page_resp.text = """
-            <div class="col-xs-12">
-                <div id="live-player-root"
-                    data-player-library="videojs"
-                    data-player-id="ILWxqLKV91Ql8kF"
-                    data-player-key="2Pw1Eg0Px3Gy9Jm3Ry8Ar5Bi5Vc5Nk"
-                    data-player-uvid="139"
-                    data-player-token="275c808a685a09d6e36d0253ab3765af"
-                    data-player-expiry="1531958189"
-                    data-player-poster=""
-                    data-requested-channel-id="139"
-                    data-play-button="/assets/tvplayer/images/dist/play-button-epg.svg"
-                    data-update-url="/now-ajax"
-                    data-timezone-name="Europe/London"
-                    data-thumb-link-classes=""
-                    data-theme-name="tvplayer"
-                    data-base-entitlements="%5B%22free%22%5D"
-                    data-has-identity="0"
-                    data-require-identity-to-watch-free="1"
-                    data-stream-type="live"
-                    data-live-route="/watch/%25live_channel_id%25"
-                >
-                </div>
-            </div>
-        """
-
-        self.session.http.get.return_value = page_resp
-        hlsstream.parse_variant_playlist.return_value = {
-            "test": HLSStream(self.session, "http://test.se/stream1")
-        }
-
-        TVPlayer.bind(self.session, "test.tvplayer")
-        plugin = TVPlayer("http://tvplayer.com/watch/dave")
-
-        streams = plugin.streams()
-
-        self.assertTrue("test" in streams)
-
-        # test the url is used correctly
-        self.session.http.get.assert_called_with(
-            "http://tvplayer.com/watch/dave"
-        )
-        # test that the correct API call is made
-        mock_get_stream_data.assert_called_with(
-            expiry="1531958189",
-            key="2Pw1Eg0Px3Gy9Jm3Ry8Ar5Bi5Vc5Nk",
-            token="275c808a685a09d6e36d0253ab3765af",
-            uvid="139"
-        )
-        # test that the correct URL is used for the HLSStream
-        hlsstream.parse_variant_playlist.assert_called_with(
-            ANY,
-            "http://test.se/stream1"
-        )
-
-    def test_get_invalid_page(self):
-        page_resp = Mock()
-        page_resp.text = """
-            var validate = "foo";
-            var resourceId = "1234";
-        """
-        self.session.http.get.return_value = page_resp
-
-        TVPlayer.bind(self.session, "test.tvplayer")
-        plugin = TVPlayer("http://tvplayer.com/watch/dave")
-
-        streams = plugin.streams()
-
-        self.assertEqual({}, streams)
-
-        # test the url is used correctly
-
-        self.session.http.get.assert_called_with(
-            "http://tvplayer.com/watch/dave"
-        )
-
-    def test_arguments(self):
-        from streamlink_cli.main import setup_plugin_args
-        session = Streamlink()
-        parser = MagicMock()
-        group = parser.add_argument_group("Plugin Options").add_argument_group("TVPlayer")
-
-        session.plugins = {
-            'tvplayer': TVPlayer
-        }
-
-        setup_plugin_args(session, parser)
-
-        self.assertSequenceEqual(
-            group.add_argument.mock_calls,
-            [
-                call('--tvplayer-email', metavar="EMAIL", help=ANY),
-                call('--tvplayer-password', metavar="PASSWORD", help=ANY)
-            ],
-        )