Repository: streamlink/streamlink
Pull request #653 (2017-03-02 22:45:59)

plugins.zdf_mediathek Added missing headers for http.get
- Removed unused import
- Removed "match" it was not used
- Added missing "name" it was not defined
- Added an **info message** for how to get a **valid link** for the tv channels, if someone uses
`streamlink https://www.zdf.de/live-tv`
- Fixed 403 Client Error (**Added headers for http.get**)

```
[cli][info] Found matching plugin zdf_mediathek for URL https://www.zdf.de/comedy/heute-show/heute-show-vom-24-februar-2017-100.html
error: Unable to open URL: https://api.zdf.de/tmd/2/portal/vod/ptmd/mediathek/170224_sendung_hsh (403 Client Error: Forbidden for url: https://api.zdf.de/tmd/2/portal/vod/ptmd/mediathek/170224_sendung_hsh)
```


Patch:
diff --git a/docs/donate.rst b/docs/donate.rst
index d80eba63..e0acc40e 100644
--- a/docs/donate.rst
+++ b/docs/donate.rst
@@ -14,22 +14,25 @@ bounty on `Bountysource <https://www.bountysource.com/teams/streamlink>`_.
 Streamlink Team
 ---------------
 
-`Bastimeyer <https://github.com/bastimeyer>`_
----------------------------------------------
+Bastimeyer 
+^^^^^^^^^^
 
+- `Github <https://github.com/bastimeyer>`__
 - `Bitcoin <https://blockchain.info/qr?data=1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8>`__ (`1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8`)
 - `Dogecoin <https://blockchain.info/qr?data=DGkVP7AMUHr2TbVnWy6QyZqSMvCkeEo1ab>`__ (`DGkVP7AMUHr2TbVnWy6QyZqSMvCkeEo1ab`)
 - `Flattr <https://flattr.com/thing/3956088>`__
 - `Paypal <https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YUCGRLVALHS8C&item_name=Streamlink%20Twitch%20GUI>`__
 
-`Beardypig <https://github.com/beardypig>`_
--------------------------------------------
+Beardypig 
+^^^^^^^^^
 
+- `Github <https://github.com/beardypig>`__
 - `Bitcoin <https://blockchain.info/qr?data=1Ey3KZ9SwTj6QyASE6vgJVebUiJsW1HuRh>`__ (`1Ey3KZ9SwTj6QyASE6vgJVebUiJsW1HuRh`)
 
-`Gravyboat <https://github.com/gravyboat>`_
--------------------------------------------
+Gravyboat
+^^^^^^^^^
 
+- `Github <https://github.com/gravyboat>`__
 - `Bitcoin <https://blockchain.info/qr?data=1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj>`__ (`1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj`)
 - `Flattr <https://flattr.com/profile/gravyboat>`__
 - `Gratipay <https://gratipay.com/~gravyboat/>`__
diff --git a/docs/install.rst b/docs/install.rst
index 4a36b41e..3a3362a0 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -228,7 +228,7 @@ Windows binaries
 
 You can download the latest stable Windows installer from the `GitHub Releases Page <https://github.com/streamlink/streamlink/releases/latest>`__.
 
-You can download the latest nightly Windows installer `here <https://streamlink-builds.s3.amazonaws.com/nightly/windows/streamlink-latest.exe>`__.
+Alternatively, you can download the latest `nightly Windows installer <https://streamlink-builds.s3.amazonaws.com/nightly/windows/streamlink-latest.exe>`__.
 
 This is a installer which contains:
 
@@ -243,7 +243,7 @@ and performs the following tasks:
   :command:`streamlink` directly from the command prompt without specifying
   its directory)
 
-To build the installer, you need to have NSIS and pynsist installed on your
+To build the installer, you need to have ``NSIS`` and ``pynsist`` installed on your
 system.
 
 
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index a3807211..0da4fee6 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -17,10 +17,12 @@ afreeca             afreecatv.com        Yes   No
 afreecatv           afreeca.tv           Yes   No
 aftonbladet         aftonbladet.se       Yes   Yes
 alieztv             aliez.tv             Yes   Yes
+animelab            animelab.com         --    Yes   Requires a login. Streams may be geo-restricted to Australia and New Zealand.
 antenna             antenna.gr           --    Yes
 app17               17app.co             Yes   --
-ard_live            live.daserste.de     Yes   --    Streams may be geo-restricted to Germany.
-ard_mediathek       ardmediathek.de      Yes   Yes   Streams may be geo-restricted to Germany.
+ard_live            daserste.de          Yes   Yes   Streams may be geo-restricted to Germany.
+ard_mediathek       - ardmediathek.de    Yes   Yes   Streams may be geo-restricted to Germany.
+                    - mediathek... [5]_
 artetv              arte.tv              Yes   Yes
 atresplayer         atresplayer.com      Yes   No    Streams are geo-restricted to Spain.
 azubutv             azubu.tv             Yes   No
@@ -110,7 +112,9 @@ mlgtv               mlg.tv               Yes   --
 nhkworld            nhk.or.jp/nhkworld   Yes   No
 nineanime           9anime.to            --    Yes
 nos                 nos.nl               Yes   Yes   Streams may be geo-restricted to Netherlands.
-npo                 npo.nl               Yes   Yes   Streams may be geo-restricted to Netherlands.
+npo                 - npo.nl             Yes   Yes   Streams may be geo-restricted to Netherlands.
+                    - zapp.nl
+                    - zappelin.nl
 nrk                 - tv.nrk.no          Yes   Yes   Streams may be geo-restricted to Norway.
                     - radio.nrk.no
 oldlivestream       original.liv... [3]_ Yes   No    Only mobile streams are supported.
@@ -210,7 +214,7 @@ wwenetwork          network.wwe.com      Yes   Yes   Requires an account to acce
 younow              younow.com           Yes   --
 youtube             - youtube.com        Yes   Yes   Protected videos are not supported.
                     - youtu.be
-zdf_mediathek       zdf.de               Yes   Yes
+zdf_mediathek       zdf.de               Yes   Yes   Streams may be geo-restricted to Germany.
 zhanqitv            zhanqi.tv            Yes   No
 =================== ==================== ===== ===== ===========================
 
@@ -219,3 +223,4 @@ zhanqitv            zhanqi.tv            Yes   No
 .. [2] streamingvideoprovider.co.uk
 .. [3] original.livestream.com
 .. [4] streaming.media.ccc.de
+.. [5] mediathek.daserste.de
\ No newline at end of file
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
index 3fe7ac0e..067b6257 100644
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -7,6 +7,10 @@ from ..cache import Cache
 from ..exceptions import PluginError, NoStreamsError
 from ..options import Options
 
+# FIXME: This is a crude attempt at making a bitrate's
+# weight end up similar to the weight of a resolution.
+# Someone who knows math, please fix.
+BIT_RATE_WEIGHT_RATIO = 2.8
 
 QUALITY_WEIGTHS_EXTRA = {
     "other": {
@@ -37,28 +41,28 @@ def stream_weight(stream):
         if stream in weights:
             return weights[stream], group
 
-    match = re.match("^(\d+)([k]|[p])?(\d*)([\+])?$", stream)
+    match = re.match(r"^(\d+)(k|p)?(\d+)?(\+)?(?:_(\d+)k)?$", stream)
 
     if match:
-        if match.group(2) == "k":
+        name_type = match.group(2)
+        if name_type == "k":  # bit rate
             bitrate = int(match.group(1))
-
-            # FIXME: This is a crude attempt at making a bitrate's
-            # weight end up similar to the weight of a resolution.
-            # Someone who knows math, please fix.
-            weight = bitrate / 2.8
+            weight = bitrate / BIT_RATE_WEIGHT_RATIO
 
             return weight, "bitrate"
 
-        elif match.group(2) == "p":
+        elif name_type == "p":  # resolution
             weight = int(match.group(1))
 
-            if match.group(3):
+            if match.group(3):  # fps eg. 60p or 50p
                 weight += int(match.group(3))
 
             if match.group(4) == "+":
                 weight += 1
 
+            if match.group(5):  # bit rate classifier for resolution
+                weight += int(match.group(5)) / BIT_RATE_WEIGHT_RATIO
+
             return weight, "pixels"
 
     return 0, "none"
@@ -85,7 +89,7 @@ def stream_type_priority(stream_types, stream):
 
 
 def stream_sorting_filter(expr, stream_weight):
-    match = re.match(r"(?P<op><=|>=|<|>)?(?P<value>[\w\+]+)", expr)
+    match = re.match(r"(?P<op><=|>=|<|>)?(?P<value>[\w+]+)", expr)
 
     if not match:
         raise PluginError("Invalid filter expression: {0}".format(expr))
diff --git a/src/streamlink/plugins/animelab.py b/src/streamlink/plugins/animelab.py
new file mode 100644
index 00000000..13535160
--- /dev/null
+++ b/src/streamlink/plugins/animelab.py
@@ -0,0 +1,90 @@
+from __future__ import print_function
+import re
+from pprint import pprint
+
+from streamlink.plugin import Plugin, PluginOptions
+from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
+from streamlink.stream import HTTPStream
+from streamlink.utils import parse_json
+
+
+class AnimeLab(Plugin):
+    url_re = re.compile(r"https?://(?:www\.)?animelab\.com/player/")
+    login_url = "https://www.animelab.com/login"
+    video_collection_re = re.compile(r"VideoCollection\((\[.*?\])\);")
+    playlist_position_re = re.compile(r"playlistPosition\s*=\s*(\d+);")
+    video_collection_schema = validate.Schema(
+        validate.union({
+            "position": validate.all(
+                validate.transform(playlist_position_re.search),
+                validate.any(
+                    None,
+                    validate.all(validate.get(1), validate.transform(int))
+                )
+            ),
+            "playlist": validate.all(
+                validate.transform(video_collection_re.search),
+                validate.any(
+                    None,
+                    validate.all(
+                        validate.get(1),
+                        validate.transform(parse_json)
+                    )
+                )
+            )
+        })
+    )
+    options = PluginOptions({
+        "email": None,
+        "password": None
+    })
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def login(self, email, password):
+        self.logger.debug("Attempting to log in as {0}", email)
+        res = http.post(self.login_url,
+                        data=dict(email=email, password=password),
+                        allow_redirects=False,
+                        raise_for_status=False)
+        loc = res.headers.get("Location", "")
+        if "geoblocked" in loc.lower():
+            self.logger.error("AnimeLab is not available in your territory")
+        elif res.status_code >= 400:
+            self.logger.error("Failed to login to AnimeLab, check your email/password combination")
+        else:
+            return True
+
+        return False
+
+    def _get_streams(self):
+        email, password = self.get_option("email"), self.get_option("password")
+        if not email or not password:
+            self.logger.error("AnimeLab requires authentication, use --animelab-email "
+                              "and --animelab-password to set your email/password combination")
+            return
+
+        if self.login(email, password):
+            self.logger.info("Successfully logged in as {0}", email)
+            video_collection = http.get(self.url, schema=self.video_collection_schema)
+            if video_collection["playlist"] is None or video_collection["position"] is None:
+                return
+
+            data = video_collection["playlist"][video_collection["position"]]
+
+            self.logger.debug("Found {0} version {1} hard-subs",
+                              data["language"]["name"],
+                              "with" if data["hardSubbed"] else "without")
+
+            for video in data["videoInstances"]:
+                if video["httpUrl"]:
+                    q = video["videoQuality"]["description"]
+                    s = HTTPStream(self.session, video["httpUrl"])
+                    yield q, s
+
+
+
+__plugin__ = AnimeLab
diff --git a/src/streamlink/plugins/ard_live.py b/src/streamlink/plugins/ard_live.py
index e6ce529a..070ae640 100644
--- a/src/streamlink/plugins/ard_live.py
+++ b/src/streamlink/plugins/ard_live.py
@@ -1,52 +1,55 @@
 import re
 
-from functools import partial
-
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http, validate
 from streamlink.stream import HLSStream, HDSStream
+from streamlink.compat import urljoin
+from streamlink.stream import HTTPStream
 
 
-STREAM_INFO_URL = "http://live.daserste.de/{0}/livestream.xml"
-SWF_URL = "http://live.daserste.de/lib/br-player/swf/main.swf"
-STREAMING_TYPES = {
-    "streamingUrlLive": (
-        "HDS", partial(HDSStream.parse_manifest, pvswf=SWF_URL)
-    ),
-    "streamingUrlIPhone": (
-        "HLS", HLSStream.parse_variant_playlist
+class ard_live(Plugin):
+    swf_url = "http://live.daserste.de/lib/br-player/swf/main.swf"
+    _url_re = re.compile(r"https?://(www.)?daserste.de/", re.I)
+    _player_re = re.compile(r'''dataURL\s*:\s*(?P<q>['"])(?P<url>.*?)(?P=q)''')
+    _player_url_schema = validate.Schema(
+        validate.transform(_player_re.search),
+        validate.any(
+            None,
+            validate.all(validate.get("url"), validate.text)
+        )
     )
-}
-
-_url_re = re.compile(r"http(s)?://live.daserste.de/(?P<channel>[^/?]+)?")
-
-_livestream_schema = validate.Schema(
-    validate.xml_findall("video/*"),
-    validate.filter(lambda e: e.tag in STREAMING_TYPES),
-    validate.map(lambda e: (STREAMING_TYPES.get(e.tag), e.text)),
-    validate.transform(dict),
-)
-
+    _livestream_schema = validate.Schema(
+        validate.xml_findall(".//assets"),
+        validate.filter(lambda x: x.attrib.get("type") != "subtitles"),
+        validate.get(0),
+        validate.xml_findall(".//asset"),
+        [validate.union({
+            "url": validate.xml_findtext("./fileName"),
+            "bitrate": validate.xml_findtext("./bitrateVideo")
+        })])
 
-class ard_live(Plugin):
     @classmethod
     def can_handle_url(cls, url):
-        return _url_re.match(url)
+        return cls._url_re.match(url) is not None
 
     def _get_streams(self):
-        match = _url_re.match(self.url)
-        channel = match.group("channel")
-        res = http.get(STREAM_INFO_URL.format(channel))
-        urls = http.xml(res, schema=_livestream_schema)
-
-        streams = {}
-        for (name, parser), url in urls.items():
-            try:
-                streams.update(parser(self.session, url))
-            except IOError as err:
-                self.logger.warning("Unable to extract {0} streams: {1}", name, err)
-
-        return streams
-
+        data_url = http.get(self.url, schema=self._player_url_schema)
+        if data_url:
+            res = http.get(urljoin(self.url, data_url))
+            stream_info = http.xml(res, schema=self._livestream_schema)
+
+            for stream in stream_info:
+                url = stream["url"]
+                try:
+                    if ".m3u8" in url:
+                        for s in HLSStream.parse_variant_playlist(self.session, url, name_key="bitrate").items():
+                            yield s
+                    elif ".f4m" in url:
+                        for s in HDSStream.parse_manifest(self.session, url, pvswf=self.swf_url, is_akamai=True).items():
+                            yield s
+                    elif ".mp4" in url:
+                        yield "{0}k".format(stream["bitrate"]), HTTPStream(self.session, url)
+                except IOError as err:
+                    self.logger.warning("Error parsing stream: {0}", err)
 
 __plugin__ = ard_live
diff --git a/src/streamlink/plugins/ard_mediathek.py b/src/streamlink/plugins/ard_mediathek.py
index 4c605370..eb6495a7 100644
--- a/src/streamlink/plugins/ard_mediathek.py
+++ b/src/streamlink/plugins/ard_mediathek.py
@@ -2,7 +2,7 @@ import re
 
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http, validate
-from streamlink.stream import HTTPStream, HDSStream, HLSStream
+from streamlink.stream import HDSStream, HLSStream, HTTPStream
 
 MEDIA_URL = "http://www.ardmediathek.de/play/media/{0}"
 SWF_URL = "http://www.ardmediathek.de/ard/static/player/base/flash/PluginFlash.swf"
@@ -15,7 +15,7 @@ QUALITY_MAP = {
     0: "144p"
 }
 
-_url_re = re.compile(r"http(s)?://(\w+\.)?ardmediathek.de/tv")
+_url_re = re.compile(r"http(s)?://(?:(\w+\.)?ardmediathek.de/tv|mediathek.daserste.de/)")
 _media_id_re = re.compile(r"/play/(?:media|config)/(\d+)")
 _media_schema = validate.Schema({
     "_mediaArray": [{
@@ -76,7 +76,6 @@ class ard_mediathek(Plugin):
             url = "{0}/{1}{2}".format(smil["base"], video, HDCORE_PARAMETER)
             streams = HDSStream.parse_manifest(self.session, url, pvswf=SWF_URL, is_akamai=smil["cdn"] == "akamai")
 
-            # TODO: Replace with "yield from" when dropping Python 2.
             for stream in streams.items():
                 yield stream
 
@@ -95,7 +94,6 @@ class ard_mediathek(Plugin):
 
         for media in media["_mediaArray"]:
             for stream in media["_mediaStreamArray"]:
-                server = stream.get("_server", "").strip()
                 stream_ = stream["_stream"]
                 if isinstance(stream_, list):
                     if not stream_:
diff --git a/src/streamlink/plugins/douyutv.py b/src/streamlink/plugins/douyutv.py
index 975990e4..b085089f 100644
--- a/src/streamlink/plugins/douyutv.py
+++ b/src/streamlink/plugins/douyutv.py
@@ -1,24 +1,26 @@
 import hashlib
 import re
 import time
-import uuid
 
 from requests.adapters import HTTPAdapter
 
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http, validate, useragents
-from streamlink.stream import HTTPStream
+from streamlink.stream import HTTPStream, HLSStream
 
+#new API and key from https://gist.github.com/spacemeowx2/629b1d131bd7e240a7d28742048e80fc by spacemeowx2
 MAPI_URL = "https://m.douyu.com/html5/live?roomId={0}"
-LAPI_URL = "https://www.douyu.com/lapi/live/getPlay/{0}"
-LAPI_SECRET = "A12Svb&%1UUmf@hC"
+LAPI_URL = "http://coapi.douyucdn.cn/lapi/live/thirdPart/getPlay/{0}?rate={1}"
+
+LAPI_SECRET = "9TUk5fjjUjg9qIMH3sdnh"
+
 SHOW_STATUS_ONLINE = 1
 SHOW_STATUS_OFFLINE = 2
 STREAM_WEIGHTS = {
     "low": 540,
-    "middle": 720,
+    "medium": 720,
     "source": 1080
-}
+    }
 
 _url_re = re.compile(r"""
     http(s)?://(www\.)?douyu.com
@@ -69,8 +71,8 @@ _room_schema = validate.Schema(
 _lapi_schema = validate.Schema(
     {
         "data": validate.any(None, {
-            "rtmp_url": validate.text,
-            "rtmp_live": validate.text
+            "hls_url": validate.text,
+            "live_url": validate.text
         })
     },
     validate.get("data")
@@ -79,16 +81,30 @@ _lapi_schema = validate.Schema(
 
 class Douyutv(Plugin):
     @classmethod
-    def can_handle_url(self, url):
+    def can_handle_url(cls, url):
         return _url_re.match(url)
 
     @classmethod
     def stream_weight(cls, stream):
         if stream in STREAM_WEIGHTS:
             return STREAM_WEIGHTS[stream], "douyutv"
-
         return Plugin.stream_weight(stream)
 
+    #quality:
+    # 0:source 2:medium 1:low
+    def _get_room_json(self, channel, quality):
+        ts = int(time.time())
+        sign = hashlib.md5("lapi/live/thirdPart/getPlay/{0}?aid=pcclient&rate={1}&time={2}{3}".format(channel, quality, ts, LAPI_SECRET).encode('ascii')).hexdigest()
+        data = {
+            "auth": sign,
+            "time": str(ts),
+            "aid": 'pcclient'
+        }
+
+        res = http.get(LAPI_URL.format(channel, quality), headers=data)
+        room = http.json(res, schema=_lapi_schema)
+        return room
+
     def _get_streams(self):
         match = _url_re.match(self.url)
         channel = match.group("channel")
@@ -115,54 +131,15 @@ class Douyutv(Plugin):
             self.logger.info("Stream currently unavailable.")
             return
 
-        ts = int(time.time() / 60)
-        did = uuid.uuid4().hex.upper()
-        sign = hashlib.md5(("{0}{1}{2}{3}".format(channel, did, LAPI_SECRET, ts)).encode("utf-8")).hexdigest()
-
-        data = {
-            "cdn": "ws",
-            "rate": "0",
-            "tt": ts,
-            "did": did,
-            "sign": sign
-        }
-
-        res = http.post(LAPI_URL.format(channel), data=data)
-        room = http.json(res, schema=_lapi_schema)
-
-        url = "{room[rtmp_url]}/{room[rtmp_live]}".format(room=room)
-        stream = HTTPStream(self.session, url)
-        yield "source", stream
-
-        data = {
-            "cdn": "ws",
-            "rate": "2",
-            "tt": ts,
-            "did": did,
-            "sign": sign
-        }
-
-        res = http.post(LAPI_URL.format(channel), data=data)
-        room = http.json(res, schema=_lapi_schema)
-
-        url = "{room[rtmp_url]}/{room[rtmp_live]}".format(room=room)
-        stream = HTTPStream(self.session, url)
-        yield "middle", stream
-
-        data = {
-            "cdn": "ws",
-            "rate": "1",
-            "tt": ts,
-            "did": did,
-            "sign": sign
-        }
-
-        res = http.post(LAPI_URL.format(channel), data=data)
-        room = http.json(res, schema=_lapi_schema)
-
-        url = "{room[rtmp_url]}/{room[rtmp_live]}".format(room=room)
-        stream = HTTPStream(self.session, url)
-        yield "low", stream
+        room_source = self._get_room_json(channel, 0)
+        yield "source", HTTPStream(self.session, room_source['live_url'])
+        yield "source", HLSStream(self.session, room_source['hls_url'])
 
+        room_medium = self._get_room_json(channel, 2)
+        yield "medium", HTTPStream(self.session, room_medium['live_url'])
+        yield "medium", HLSStream(self.session, room_medium['hls_url'])
 
+        room_low = self._get_room_json(channel, 1)
+        yield "low", HTTPStream(self.session, room_low['live_url'])
+        yield "low", HLSStream(self.session, room_low['hls_url'])
 __plugin__ = Douyutv
diff --git a/src/streamlink/plugins/goodgame.py b/src/streamlink/plugins/goodgame.py
index ad3188bc..60a39570 100644
--- a/src/streamlink/plugins/goodgame.py
+++ b/src/streamlink/plugins/goodgame.py
@@ -12,14 +12,14 @@ QUALITIES = {
     "240p": "_240"
 }
 
-_url_re = re.compile(r"https://(?:www\.)?goodgame.ru/channel/(?P<user>\w+)")
+_url_re = re.compile(r"https?://(?:www\.)?goodgame.ru/channel/(?P<user>\w+)")
 _stream_re = re.compile(r'var src = "([^"]+)";')
 _ddos_re = re.compile(r'document.cookie="(__DDOS_[^;]+)')
 
 
 class GoodGame(Plugin):
     @classmethod
-    def can_handle_url(self, url):
+    def can_handle_url(cls, url):
         return _url_re.match(url)
 
     def _check_stream(self, url):
diff --git a/src/streamlink/plugins/npo.py b/src/streamlink/plugins/npo.py
index 2571b293..f04aaade 100644
--- a/src/streamlink/plugins/npo.py
+++ b/src/streamlink/plugins/npo.py
@@ -6,90 +6,104 @@ Supports:
 """
 
 import re
-import json
 
-from streamlink.compat import quote
-from streamlink.plugin import Plugin
+from streamlink.plugin import Plugin, PluginOptions
 from streamlink.plugin.api import http
-from streamlink.stream import HTTPStream, HLSStream
-
-_url_re = re.compile(r"http(s)?://(\w+\.)?npo.nl/")
-HTTP_HEADERS = {
-    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.9 Safari/537.36"
-}
+from streamlink.plugin.api import useragents
+from streamlink.plugin.api import validate
+from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
 
 
 class NPO(Plugin):
+    api_url = "http://ida.omroep.nl/app.php/{endpoint}"
+    url_re = re.compile(r"https?://(\w+\.)?(npo.nl|zapp.nl|zappelin.nl)/")
+    prid_re = re.compile(r'''(?:data(-alt)?-)?prid\s*[=:]\s*(?P<q>["'])(\w+)(?P=q)''')
+    react_re = re.compile(r'''data-react-props\s*=\s*(?P<q>["'])(?P<data>.*?)(?P=q)''')
+
+    auth_schema = validate.Schema({"token": validate.text}, validate.get("token"))
+    streams_schema = validate.Schema({
+        "items": [
+            [{
+                "label": validate.text,
+                "contentType": validate.text,
+                "url": validate.url(),
+                "format": validate.text
+            }]
+        ]
+    }, validate.get("items"), validate.get(0))
+    stream_info_schema = validate.Schema(validate.any(
+        validate.url(),
+        validate.all({"errorcode": 0, "url": validate.url()},
+                     validate.get("url"))
+    ))
+    options = PluginOptions({
+        "subtitles": False
+    })
+
     @classmethod
     def can_handle_url(cls, url):
-        return _url_re.match(url)
-
-    def get_token(self):
-        url = 'http://ida.omroep.nl/npoplayer/i.js?s={0}'.format(quote(self.url))
-        token = http.get(url, headers=HTTP_HEADERS).text
-        token = re.compile(r'token.*?"(.*?)"', re.DOTALL + re.IGNORECASE).search(token).group(1)
-
-        # Great the have a ['en','ok','t'].reverse() decurity option in npoplayer.js
-        secured = list(token)
-        token = list(token)
-
-        first = -1
-        second = -1
-        for i, c in enumerate(token):
-            if c.isdigit() and 4 < i < len(token):
-                if first == -1:
-                    first = i
-                else:
-                    second = i
-                    break
-
-        if first == -1:
-            first = 12
-        if second == -1:
-            second = 13
-
-        secured[first] = token[second]
-        secured[second] = token[first]
-        return ''.join(secured)
-
-    def _get_meta(self):
-        html = http.get('http://www.npo.nl/live/{0}'.format(self.npo_id), headers=HTTP_HEADERS).text
-        program_id = re.compile(r'data-prid="(.*?)"', re.DOTALL + re.IGNORECASE).search(html).group(1)
-        meta = http.get('http://e.omroep.nl/metadata/{0}'.format(program_id), headers=HTTP_HEADERS).text
-        meta = re.compile(r'({.*})', re.DOTALL + re.IGNORECASE).search(meta).group(1)
-        return json.loads(meta)
-
-    def _get_vod_streams(self):
-        url = 'http://ida.omroep.nl/odi/?prid={0}&puboptions=adaptive,h264_bb,h264_sb,h264_std&adaptive=no&part=1&token={1}'\
-            .format(quote(self.npo_id), quote(self.get_token()))
-        res = http.get(url, headers=HTTP_HEADERS)
-
-        data = res.json()
-
-        streams = {}
-        stream = http.get(data['streams'][0].replace('jsonp', 'json'), headers=HTTP_HEADERS).json()
-        streams['best'] = streams['high'] = HTTPStream(self.session, stream['url'])
-        return streams
-
-    def _get_live_streams(self):
-        meta = self._get_meta()
-        stream = [x for x in meta['streams'] if x['type'] == 'hls'][0]['url']
-
-        url = 'http://ida.omroep.nl/aapi/?type=jsonp&stream={0}&token={1}'.format(stream, self.get_token())
-        streamdata = http.get(url, headers=HTTP_HEADERS).json()
-        deeplink = http.get(streamdata['stream'], headers=HTTP_HEADERS).text
-        deeplink = re.compile(r'"(.*?)"', re.DOTALL + re.IGNORECASE).search(deeplink).group(1)
-        playlist_url = deeplink.replace("\\/", "/")
-        return HLSStream.parse_variant_playlist(self.session, playlist_url)
+        return cls.url_re.match(url) is not None
+
+    def __init__(self, url):
+        super(NPO, self).__init__(url)
+        self._token = None
+        http.headers.update({"User-Agent": useragents.CHROME})
+
+    def api_call(self, endpoint, schema=None, params=None):
+        url = self.api_url.format(endpoint=endpoint)
+        res = http.get(url, params=params)
+        return http.json(res, schema=schema)
+
+    @property
+    def token(self):
+        if not self._token:
+            self._token = self.api_call("auth", schema=self.auth_schema)
+        return self._token
+
+    def _get_prid(self, subtitles=False):
+        res = http.get(self.url)
+        bprid = None
+
+        # Locate the asset id for the content on the page
+        for alt, _, prid in self.prid_re.findall(res.text):
+            if alt and subtitles:
+                bprid = prid
+            elif bprid is None:
+                bprid = prid
+
+        if bprid is None:
+            m = self.react_re.search(res.text)
+            if m:
+                data = parse_json(m.group("data").replace("&quot;", '"'))
+                bprid = data.get("mid")
+
+        return bprid
 
     def _get_streams(self):
-        urlparts = self.url.split('/')
-        self.npo_id = urlparts[-1]
-
-        if (urlparts[-2] == 'live'):
-            return self._get_live_streams()
-        else:
-            return self._get_vod_streams()
+        asset_id = self._get_prid(self.get_option("subtitles"))
+
+        if asset_id:
+            self.logger.debug("Found asset id: {0}", asset_id)
+            streams = self.api_call(asset_id,
+                                    params=dict(adaptive="yes",
+                                                token=self.token),
+                                    schema=self.streams_schema)
+
+            for stream in streams:
+                if stream["format"] in ("adaptive", "hls"):
+                    if stream["contentType"] == "url":
+                        stream_url = stream["url"]
+                    else:
+                        # using type=json removes the javascript function wrapper
+                        info_url = stream["url"].replace("type=jsonp", "type=json")
+
+                        # find the actual stream URL
+                        stream_url = http.json(http.get(info_url),
+                                               schema=self.stream_info_schema)
+
+                    for s in HLSStream.parse_variant_playlist(self.session, stream_url).items():
+                        yield s
 
 
 __plugin__ = NPO
diff --git a/src/streamlink/plugins/trt.py b/src/streamlink/plugins/trt.py
index 7724c888..8dff9847 100644
--- a/src/streamlink/plugins/trt.py
+++ b/src/streamlink/plugins/trt.py
@@ -14,7 +14,7 @@ class TRT(Plugin):
     """
     Support for the live TV streams on http://www.trt.net.tr/, some streams may be geo-locked
     """
-    url_re = re.compile(r"http://www.trt.net.tr/Anasayfa/canli.aspx.*")
+    url_re = re.compile(r"http://www.trt.net.tr/anasayfa/canli.aspx.*", re.I)
     stream_data_re = re.compile(r'<script>eval\(dcm1\("(.*?)"\)\);')
     f4mm_re = re.compile(r'''(?P<q>["'])(?P<url>http[^"']+?.f4m)(?P=q);''')
     m3u8_re = re.compile(r'''(?P<q>["'])(?P<url>http[^"']+?.m3u8)(?P=q);''')
@@ -32,7 +32,7 @@ class TRT(Plugin):
             res = http.get(self.url)
             stream_data_m = self.stream_data_re.search(res.text)
             if stream_data_m:
-                script_vars = b64decode(stream_data_m.group(1))
+                script_vars = b64decode(stream_data_m.group(1)).decode("utf8")
                 url_m = self.m3u8_re.search(script_vars)
 
                 hls_url = url_m and url_m.group("url")
diff --git a/src/streamlink/plugins/tvplayer.py b/src/streamlink/plugins/tvplayer.py
index 83ebcc6f..a7feeda7 100644
--- a/src/streamlink/plugins/tvplayer.py
+++ b/src/streamlink/plugins/tvplayer.py
@@ -8,8 +8,10 @@ from streamlink.stream import HLSStream
 
 
 class TVPlayer(Plugin):
-    API_URL = "http://api.tvplayer.com/api/v2/stream/live"
-    LOGIN_URL = "https://tvplayer.com/account/login"
+    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)
@@ -40,10 +42,10 @@ class TVPlayer(Plugin):
         http.headers.update({"User-Agent": useragents.CHROME})
 
     def authenticate(self, username, password):
-        res = http.get(self.LOGIN_URL)
+        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),
+        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
@@ -56,11 +58,18 @@ class TVPlayer(Plugin):
         self.url = self.url.replace("https", "http")  # https redirects to http
         res = http.get(self.url)
 
+        if "enter your postcode" in res.text:
+            self.logger.info("Setting your postcode to: {0}. "
+                             "This can be changed in the settings on tvplayer.com", self.dummy_postcode)
+            res = http.post(self.update_url,
+                            data=dict(postcode=self.dummy_postcode),
+                            params=dict(return_url=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(self.API_URL, data=dict(
+            res = http.post(self.api_url, data=dict(
                 service=1,
                 id=stream_attrs["resourceId"],
                 validate=stream_attrs["validate"],
@@ -68,7 +77,6 @@ class TVPlayer(Plugin):
                 token=stream_attrs.get("token")))
 
             stream_data = http.json(res, schema=self.stream_schema)
-            print(stream_data)
 
             if stream_data.get("drmToken"):
                 self.logger.error("This stream is protected by DRM can cannot be played")
diff --git a/src/streamlink/plugins/vidio.py b/src/streamlink/plugins/vidio.py
index e55119f3..3f280811 100644
--- a/src/streamlink/plugins/vidio.py
+++ b/src/streamlink/plugins/vidio.py
@@ -16,7 +16,7 @@ def html_unescape(s):
     return parser.unescape(s)
 
 
-_url_re = re.compile(r"https?://(?:www\.)?vidio\.com/(?P<type>live|watch)/(?P<id>\d+)-(?P<name>[^/?#&]+)")
+_url_re = re.compile(r"https?://(?:www\.)?vidio\.com/(?:en/)?(?P<type>live|watch)/(?P<id>\d+)-(?P<name>[^/?#&]+)")
 _clipdata_re = re.compile(r"""data-json-clips\s*=\s*(['"])(.*?)\1""")
 
 _schema = validate.Schema(
diff --git a/src/streamlink/plugins/wwenetwork.py b/src/streamlink/plugins/wwenetwork.py
index b7e971e0..25e0f246 100644
--- a/src/streamlink/plugins/wwenetwork.py
+++ b/src/streamlink/plugins/wwenetwork.py
@@ -146,7 +146,7 @@ class WWENetwork(Plugin):
                 if info.get("session_key"):
                     self.session_key = info.get("session_key")
                 for url in info["urls"]:
-                    for s in HLSStream.parse_variant_playlist(self.session, url).items():
+                    for s in HLSStream.parse_variant_playlist(self.session, url, name_fmt="{pixels}_{bitrate}").items():
                         yield s
             else:
                 raise PluginError("Could not load streams: {message} ({code})".format(**info["status"]))
diff --git a/src/streamlink/plugins/zdf_mediathek.py b/src/streamlink/plugins/zdf_mediathek.py
index e6798ccd..c71f9100 100644
--- a/src/streamlink/plugins/zdf_mediathek.py
+++ b/src/streamlink/plugins/zdf_mediathek.py
@@ -3,7 +3,6 @@ import re
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http, validate
 from streamlink.stream import HDSStream, HLSStream
-from streamlink.plugin.api.utils import parse_query
 
 API_URL = "https://api.zdf.de"
 
@@ -85,7 +84,7 @@ class zdf_mediathek(Plugin):
             for format_ in priority["formitaeten"]:
                 yield self._extract_from_format(format_)
 
-    def _parse_track(self, track, parser):
+    def _parse_track(self, track, parser, name):
         try:
             return parser(self.session, track["uri"])
         except IOError as err:
@@ -100,25 +99,33 @@ class zdf_mediathek(Plugin):
         name, parser = STREAMING_TYPES[format_["type"]]
         for quality in format_["qualities"]:
             for track in quality["audio"]["tracks"]:
-                option = self._parse_track(track, parser)
-                qualities.update(option)
+                option = self._parse_track(track, parser, name)
+                if option:
+                    qualities.update(option)
 
         return qualities
 
     def _get_streams(self):
-        match = _url_re.match(self.url)
         title = self.url.rsplit('/', 1)[-1]
         if title.endswith(".html"):
             title = title[:-5]
+        if title == "live-tv":
+            self.logger.info("Klicken Sie mit der rechten Maustaste auf dem Player (im Browser) und waehlen Sie 'Beitrags-Url kopieren', um einen gueltigen Link fuer streamlink zu erhalten.")
+            return
+
+        headers = {
+            "Api-Auth": "Bearer d2726b6c8c655e42b68b0db26131b15b22bd1a32",
+            "Referer": self.url
+        }
 
         request_url = "https://api.zdf.de/content/documents/%s.json?profile=player" % title
-        res = http.get(request_url, headers={"Api-Auth": "Bearer d2726b6c8c655e42b68b0db26131b15b22bd1a32"})
+        res = http.get(request_url, headers=headers)
         document = http.json(res, schema=_documents_schema)
 
         stream_request_url = document["mainVideoContent"]["http://zdf.de/rels/target"]["http://zdf.de/rels/streams/ptmd"]
         stream_request_url = API_URL + stream_request_url
 
-        res = http.get(stream_request_url)
+        res = http.get(stream_request_url, headers=headers)
         res = http.json(res, schema=_schema)
 
         streams = {}
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
index 9cfcafa7..05300ac6 100644
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -271,7 +271,7 @@ class HLSStream(HTTPStream):
     @classmethod
     def parse_variant_playlist(cls, session_, url, name_key="name",
                                name_prefix="", check_streams=False,
-                               force_restart=False,
+                               force_restart=False, name_fmt=None,
                                **request_params):
         """Attempts to parse a variant playlist and return its streams.
 
@@ -279,8 +279,10 @@ class HLSStream(HTTPStream):
         :param name_key: Prefer to use this key as stream name, valid keys are:
                          name, pixels, bitrate.
         :param name_prefix: Add this prefix to the stream names.
+        :param check_streams: Only allow streams that are accessible.
         :param force_restart: Start at the first segment even for a live stream
-        :param check_streams: Only allow streams that are accesible.
+        :param name_fmt: A format string for the name, allowed format keys are
+                         name, pixels, bitrate.
         """
         logger = session_.logger.new_module("hls.parse_variant_playlist")
         locale = session_.localization
@@ -338,8 +340,11 @@ class HLSStream(HTTPStream):
                 else:
                     names["bitrate"] = "{0}k".format(bw / 1000.0)
 
-            stream_name = (names.get(name_key) or names.get("name") or
-                           names.get("pixels") or names.get("bitrate"))
+            if name_fmt:
+                stream_name = name_fmt.format(**names)
+            else:
+                stream_name = (names.get(name_key) or names.get("name") or
+                               names.get("pixels") or names.get("bitrate"))
 
             if not stream_name:
                 continue
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index 2f7c4e9f..eef621af 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -131,6 +131,14 @@ def keyvalue(value):
     return match.group("key", "value")
 
 
+def boolean(value):
+    truths = ["yes", "1", "true", "on"]
+    falses = ["no", "0", "false", "off"]
+    if value.lower() not in truths+falses:
+        raise argparse.ArgumentTypeError("{0} was not one of {{{1}}}".format(value, ', '.join(truths+falses)))
+
+    return value.lower() in truths
+
 parser = ArgumentParser(
     fromfile_prefix_chars="@",
     formatter_class=HelpFormatter,
@@ -261,10 +269,14 @@ general.add_argument(
     """
 )
 general.add_argument(
-    "--no-version-check",
-    action="store_true",
+    "--auto-version-check",
+    type=boolean,
+    metavar="{yes,true,1,on,no,false,0,off}",
+    default=False,
     help="""
-    Do not check for new Streamlink releases.
+    Enable or disable the automatic check for a new version of Streamlink.
+
+    Default is no
     """
 )
 general.add_argument(
@@ -1163,6 +1175,27 @@ plugin.add_argument(
     A WWE Network account password to use with --wwenetwork-email.
     """
 )
+plugin.add_argument(
+    "--animelab-email",
+    metavar="EMAIL",
+    help="""
+    The email address used to register with animelab.com.
+    """
+)
+plugin.add_argument(
+    "--animelab-password",
+    metavar="PASSWORD",
+    help="""
+    A TVPlayer account password to use with --animelab-email.
+    """
+)
+plugin.add_argument(
+    "--npo-subtitles",
+    action="store_true",
+    help="""
+    Include subtitles for the deaf or hard of hearing, if available
+    """
+)
 
 # Deprecated options
 stream.add_argument(
@@ -1232,5 +1265,10 @@ plugin.add_argument(
     metavar="LOCALE",
     help=argparse.SUPPRESS
 )
+general.add_argument(
+    "--no-version-check",
+    action="store_true",
+    help=argparse.SUPPRESS
+)
 
 __all__ = ["parser"]
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index af86185b..4e158f74 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -893,6 +893,20 @@ def setup_plugin_options():
     if wwenetwork_password:
         streamlink.set_plugin_option("wwenetwork", "password", wwenetwork_password)
 
+    if args.animelab_email:
+        streamlink.set_plugin_option("animelab", "email", args.animelab_email)
+
+    if args.animelab_email and not args.animelab_password:
+        animelab_password = console.askpass("Enter AnimeLab password: ")
+    else:
+        animelab_password = args.animelab_password
+
+    if animelab_password:
+        streamlink.set_plugin_option("animelab", "password", animelab_password)
+
+    if args.npo_subtitles:
+        streamlink.set_plugin_option("npo", "subtitles", args.npo_subtitles)
+
     # Deprecated options
     if args.jtv_legacy_names:
         console.logger.warning("The option --jtv/twitch-legacy-names is "
@@ -963,7 +977,7 @@ def main():
     setup_http_session()
     check_root()
 
-    if args.version_check or not args.no_version_check:
+    if args.version_check or (not args.no_version_check and args.auto_version_check):
         with ignored(Exception):
             check_version(force=args.version_check)
 


Test patch:
diff --git a/tests/test_plugin_stream.py b/tests/test_plugin_stream.py
index 01a4a97c..a2e37705 100644
--- a/tests/test_plugin_stream.py
+++ b/tests/test_plugin_stream.py
@@ -2,6 +2,7 @@ import unittest
 
 from streamlink import Streamlink
 from streamlink.plugins.stream import StreamURL
+from streamlink.plugin.plugin import stream_weight
 from streamlink.stream import *
 
 
@@ -107,5 +108,31 @@ class TestPluginStream(unittest.TestCase):
             StreamURL._parse_params(""""conn=['B:1', 'S:authMe', 'O:1', 'NN:code:1.23', 'NS:flag:ok', 'O:0']""")
         )
 
+    def test_stream_weight(self):
+        self.assertEqual(
+            (720, "pixels"),
+            stream_weight("720p"))
+        self.assertEqual(
+            (721, "pixels"),
+            stream_weight("720p+"))
+        self.assertEqual(
+            (780, "pixels"),
+            stream_weight("720p60"))
+
+        self.assertTrue(
+            stream_weight("720p+") > stream_weight("720p"))
+        self.assertTrue(
+            stream_weight("720p") == stream_weight("720p"))
+        self.assertTrue(
+            stream_weight("720p_3000k") > stream_weight("720p_2500k"))
+        self.assertTrue(
+            stream_weight("720p60_3000k") > stream_weight("720p_3000k"))
+        self.assertTrue(
+            stream_weight("720p_3000k") < stream_weight("720p+_3000k"))
+
+        self.assertTrue(
+            stream_weight("3000k") > stream_weight("2500k"))
+
+
 if __name__ == "__main__":
     unittest.main()