Repository: streamlink/streamlink
Pull request #2232 (2019-01-04 14:48:35)

Add {url} argument to window --title
For #2225 
Adds `{url}` formatting variable to player window `--title`.

Can there be a situation where `args.url` doesn't exist?
If so, maybe I should add a `DEFAULT_STREAM_METADATA["url"]`?
https://github.com/streamlink/streamlink/blob/07387e22d567994f8fe7eaba79638e22cb2be24b/src/streamlink_cli/constants.py#L7-L12


Patch:
diff --git a/.travis.yml b/.travis.yml
index 188d6eaa..a664f15f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,7 +26,7 @@ matrix:
 
 before_install:
   - pip install --disable-pip-version-check --upgrade pip setuptools
-  - pip install -r dev-requirements.txt
+  - pip install --upgrade -r dev-requirements.txt
   - pip install pycountry
   - if [[ $BUILD_DOCS == 'yes' ]]; then
       pip install -r docs-requirements.txt;
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f25163a..152e00da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,246 @@
 # Changelog
 
+## streamlink 1.0.0 (2019-01-30)
+
+The celebratory release of Streamlink 1.0.0!
+
+*A lot* of hard work has gone into getting Streamlink to where it is. Not only is Streamlink used across multiple applications and platforms, but companies as well. 
+
+Streamlink started from the inaugural [fork of Livestreamer](https://github.com/chrippa/livestreamer/issues/1427) on September 17th, 2016. 
+
+Since then, We've hit multiple milestones:
+
+ - Over 886 PRs
+ - Hit 3,000 commits in Streamlink
+ - Obtaining our first sponsors as well as backers of the project
+ - The creation of our own logo (https://github.com/streamlink/streamlink/issues/1123)
+
+Thanks to everyone who has contributed to Streamlink (and our backers)! Without you, we wouldn't be where we are today.
+
+**Without further ado, here are the changes in release 1.0.0:**
+
+  - We have a new icon / logo for Streamlink! (https://github.com/streamlink/streamlink/pull/2165)
+  - Updated dependencies (https://github.com/streamlink/streamlink/pull/2230)
+  - A *ton* of plugin updates. Have a look at [this search query](https://github.com/streamlink/streamlink/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aclosed+plugins.+) for all the recent updates.
+  - You can now provide a custom key URI to override HLS streams (https://github.com/streamlink/streamlink/pull/2139). For example: `--hls-segment-key-uri <URI>`
+  - User agents for API communication have been updated (https://github.com/streamlink/streamlink/pull/2194)
+  - Special synonyms have been added to sort "best" and "worst" streams (https://github.com/streamlink/streamlink/pull/2127). For example: `streamlink --stream-sorting-excludes '>=480p' URL best,best-unfiltered`
+  - Process output will no longer show if tty is unavailable (https://github.com/streamlink/streamlink/pull/2090)
+  - We've removed BountySource in favour of our OpenCollective page. If you have any features you'd like to request, please open up an issue with the request and possibly consider backing us!
+  - Improved terminal progress display for wide characters (https://github.com/streamlink/streamlink/pull/2032)
+  - Fixed a bug with dynamic playlists on playback (https://github.com/streamlink/streamlink/pull/2096)
+  - Fixed makeinstaller.sh (https://github.com/streamlink/streamlink/pull/2098)
+  - Old Livestreamer deprecations and API references were removed (https://github.com/streamlink/streamlink/pull/1987)
+  - Dependencies have been updated for Python (https://github.com/streamlink/streamlink/pull/1975)
+  - Newer and more common User-Agents are now used (https://github.com/streamlink/streamlink/pull/1974)
+  - DASH stream bitrates now round-up to the nearest 10, 100, 1000, etc. (https://github.com/streamlink/streamlink/pull/1995)
+  - Updated documentation on issue templates (https://github.com/streamlink/streamlink/pull/1996)
+  - URL have been added for better processing of HTML tags (https://github.com/streamlink/streamlink/pull/1675)
+  - Fixed sort and prog issue (https://github.com/streamlink/streamlink/pull/1964)
+  - Reformatted issue templates (https://github.com/streamlink/streamlink/pull/1966)
+  - Fixed crashing bug with player-continuous-http option (https://github.com/streamlink/streamlink/pull/2234)
+  - Make sure all dev dependencies (https://github.com/streamlink/streamlink/pull/2235)
+  - -r parameter has been replaced for --rtmp-rtmpdump (https://github.com/streamlink/streamlink/pull/2152)
+
+**Breaking changes:**
+
+  - A large number of unmaintained or NSFW plugins have been removed. You can find the PR that implemented that change here: https://github.com/streamlink/streamlink/pull/2003 . See our [CONTRIBUTING.md](https://github.com/streamlink/streamlink/blob/130489c6f5ad15488cd4ff7a25c74bf070f163ec/CONTRIBUTING.md) documentation for plugin policy.
+
+
+
+```text
+Billy2011 <kschmidt2007@googlemail.com> (3):
+      streamlink.plugins: replace global http session by self.session.http (#1925)
+      stream.hls_playlist: fix some regex pattern & code optimization (#1918)
+      plugins.filmon: fix NoPluginError, if channel is not an ID (#2005)
+
+David Bell <me@geordish.org> (1):
+      Update ine.py (#2171)
+
+Forrest <forrest.alvarez@protonmail.com> (2):
+      Remove bountysource from donation page, update flattr
+      Add a note about specifying the full player path
+
+Forrest <gravyboat@users.noreply.github.com> (2):
+      Feature/plugin request policy update (#1838)
+      Add icon, modify installer, update docs (#2165)
+
+Hubcapp <walnut.alligator@gmail.com> (1):
+      Window Titles = Stream Titles + Other Attributes (#1576)
+
+Jani Ollikainen <bestis@cd.purkki.org> (1):
+      Added support for viafree.fi
+
+Lukas <keydon@gmail.com> (1):
+      plugins.zdf_mediathek: use ptmd-template api path (#2233)
+
+Maxwell Cody <maxwellcody@riseup.net> (1):
+      Add ability to specify custom key URI override for HLS streams (#2139)
+
+Mohamed El Morabity <melmorabity@users.noreply.github.com> (1):
+      plugins.pluzz: fixes and francetvinfo.fr support (#2119)
+
+Nick Gal <nickgal@me.com> (1):
+      Update steam plugin to work with steam.tv urls
+
+Petar Kukolj <petarkukolj3@yahoo.com> (5):
+      plugins.cubetv: support for live streams on cubetv.sg
+      plugins.ok_live: Changed URL regex to support VoDs
+      plugins.bilibili: Fix plugin after API change
+      plugins.twitch: Add support for {title}, {author} and {category}
+      plugins.skai: Fix plugin after site update
+
+Roman <w.romankornev@gmail.com> (2):
+      [FIX] Debug log arguments string
+      [FIX] Debug log arguments cross-platform
+
+Roman Kornev <w.romankornev@gmail.com> (1):
+      [FIX] Message duplicate
+
+Sebastian Meyer <mail@bastimeyer.de> (1):
+      Reword+reformat issue templates for consistency (#1966)
+
+Stefan de Konink <stefan@konink.de> (1):
+      Update the documentation with comments for playing YouTube Live Streams (#2156)
+
+Søren Fuglede Jørgensen <s@fuglede.dk> (1):
+      Update and reactivate plugin for dr.dk
+
+Twilight0 <twilight@freemail.gr> (1):
+      plugins.ssh101: Fixed plugin (#1916)
+
+Vincent Rozenberg <vincentrozenberg@gmail.com> (1):
+      Update npo.py
+
+Vinny <vincent.aceto@gmail.com> (1):
+      docs: Added documentation for the Funimation plugin (#2091)
+
+Visatouch Deeying <xerodotc@gmail.com> (1):
+      Fix crash on missing output.record when using player-continous-http
+
+back-to <backto@protonmail.ch> (48):
+      plugins.EuropaPlusTV: Fix for "No connection adapters were found"
+      utils.args: Moved streamlink_cli utils.args into streamlink package
+      tests.plugins: Test that each plugin has a test file (#1885)
+      plugins.zattoo: session update and allow muxed hls / dash streams (#1902)
+      plugins.tv4play: Fix for updated website
+      debug: Added Session Language code as a debug message
+      tests: run Python 3.7 tests on AppVeyor and Travis-CI (#1928)
+      plugins.rtve: Fixed AttributeError 'ZTNRClient' has no 'session'
+      plugins.twitch: Fixed AttributeError and Flake8
+      plugins.filmon: Fixed AttributeError
+      plugins.crunchyroll: Fixed AttributeError and Flake8
+      tests.localization: use en_CA instead of en_US for test_equivalent
+      plugins.younow: Fix for session error and plugin cleanup
+      plugins.media_ccc_de: removed plugin
+      plugins.pixiv: use API for the stream URL and cache login cookies
+      plugins.youtube: Added support for {author} and {title} (#1944)
+      docs-CLI: Fix for sort and prog issue
+      plugins.ceskatelevize: Fix for https issues
+      plugins.mjunoon: use a User-Agent
+      api.useragents: use newer and more common User-Agent's
+      script.makeinstaller: use a more recent version of Python and Pycryptodome
+      Removed old Livestreamer deprecations and API references
+      plugins.sportal: Removed old RTMP streams, use HLS
+      plugins.twitch: new video URL regex
+      Removed old or unwanted Streamlink Plugins
+      tests: use unittest instead of pytest for itertags error (#1999)
+      utils.parse_xml: allow a tab for ignore_ns
+      plugins.afreeca: ignore new preloading urls
+      plugins.filmon: use the same cdn for a playlist url (#2074)
+      plugins.teleclubzoom: New plugin for teleclubzoom.ch
+      plugins.oldlivestream: remove plugin, service not available anymore (#2081)
+      travis: increase git clone depth
+      stream.dash_manifest: Fixed bug for dynamic playlists when parent.id is None
+      plugins.ustreamtv: use Desktop instead of Mobile streams (#2082)
+      plugins.youtube: Added support for embedded livestream urls
+      versioneer: always use 7 characters
+      plugins.stv: New Plugin for https://player.stv.tv/live
+      plugins.sbscokr: New Plugin for http://play.sbs.co.kr/onair/pc/index.html
+      plugins.vtvgo: New plugin for https://vtvgo.vn/
+      travis-ci: Fixed Python 3.8 error's
+      plugins.cdnbg: Update for extra channels (#2186)
+      api.useragents: update User-Agent list
+      plugins.afreeca: use Referer for every connection (#2204)
+      plugins.trt: Added support for https url, fixed logger error
+      plugins.turkuvaz: Added support for channel 'apara'
+      plugins.tvrby: Fixed broken plugin.
+      plugins.youtube: replace "gaming" with "www" subdomain
+      plugins.kanal7: Fixed iframe_re/stream_re, added new domain
+
+bastimeyer <mail@bastimeyer.de> (6):
+      Fix bug report template
+      Move/rename issue templates
+      Update generic issue template
+      plugins.euronews: fix live stream API URL scheme
+      Fix installer by moving additional files
+      feature: {best,worst}-unfiltered stream synonyms
+
+beardypig <beardypig@protonmail.com> (23):
+      plugins.live_russia_tv: fix for live streams, and support for VOD
+      plugin args: if args are suppressed, ignore them
+      plugin.tvtoya: refactor, add tests, plugin docs, etc.
+      stream.dash: fix bug where timeline_segments.t can be None
+      stream.hls: only include audio/video streams in MuxedHLSStreams
+      stream.hls: if the primary video stream has audio then include it
+      plugins.facebook: support for videos in posts
+      plugins.steam: api requests now require a session id
+      test for rounding dash stream bitrate
+      stream.dash: make the bitrate name for videos more human friendly
+      plugins.schoolism: add support for assignment feedback videos
+      plugins.senategov: support for hearing streams on senate.gov
+      plugins.stadium: support for live stream on watchstadium.com
+      plugins.funimationnow: workaround for #1899, see #2088
+      cli: make the HH part of HH:MM:SS options optional
+      plugins.bbciplayer: change in the mediator info in the page layout
+      plugins.btsports: fix for change in login flow
+      plugins.filmon: fix for live tv URLs that start with channel
+      plugin.powerapp: support for "tvs" URLs
+      setup: update requests to latest version and set urllib3 to match
+      CI: make sure all the dev dependencies are up to date
+      plugins.tf1: update to support new DASH streams
+      plugins.tf1: re-add support for HLS with the new API
+
+beardypig <git@beardypig.com> (7):
+      Test coverage increase (#1646)
+      Handle unicode log message in Python 2 (#1886)
+      Update method for finding YouTube videoIds (#1888)
+      stream.dash: prefer audio streams based on the user's locale (#1927)
+      plugins.openrectv: update to match site changes and title support (#1968)
+      URL builder utils (#1675)
+      cli: disable progress output for -o when no tty is available (#2090)
+
+fozzy <fozzy@fozzy.co> (1):
+      update regex to support new pattern
+
+fozzy <fozzysec@gmail.com> (1):
+      plugins.egame: new plugin for egame.qq.com (#2070)
+
+jackyzy823 <jackyzy823@gmail.com> (2):
+      Plugin Request: new plugin for Abema.tv (#1949)
+      Improve terminal progress display for  wide characters (#2032)
+
+mp107 <mp107@users.noreply.github.com> (1):
+      plugins.ltvlmslv: new Plugin for Latvian live TV channels on ltv.lsm.lv (#1986)
+
+qkolj <qkolj@users.noreply.github.com> (5):
+      plugins.tamago: support for live streams on player.tamago.live (#2108)
+      plugins.huomao: Fix plugin after website changes (#2134)
+      plugins.metube: Add support for live streams and  VoDs on www.metube.id (#2112)
+      plugins.tvibo: Add support for livestreams on player.tvibo.com (#2130)
+      Fix recording added in #920 (#2152)
+
+remek30 <remek30@users.noreply.github.com> (1):
+      plugins.toya: support for tvtoya.pl
+
+skulblakka <pascal.romahn@mailbox.org> (1):
+      [picarto.tv] Fix regarding changed URL (#1935)
+
+yoya3312 <40212627+yoya3312@users.noreply.github.com> (1):
+      plugins.youtube: use new "hlsManifestUrl" for Livestreams (#2238)
+```
+
+
 ## streamlink 0.14.2 (2018-06-28)
 
 Just a few small fixes in this release. 
diff --git a/LICENSE b/LICENSE
index 49fb9840..b38cbab5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
 Copyright (c) 2011-2016, Christopher Rosell
-Copyright (c) 2016-2018, Streamlink Team
+Copyright (c) 2016-2019, Streamlink Team
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/docs/_themes/sphinx_rtd_theme_violet/LICENSE b/docs/_themes/sphinx_rtd_theme_violet/LICENSE
index 50dc02e6..9b33112e 100644
--- a/docs/_themes/sphinx_rtd_theme_violet/LICENSE
+++ b/docs/_themes/sphinx_rtd_theme_violet/LICENSE
@@ -2,7 +2,7 @@ The MIT License (MIT)
 
 Copyright (c) 2013 Dave Snider
 Copyright (c) 2014 Christopher Rosell
-Copyright (c) 2015-2018 Streamlink
+Copyright (c) 2015-2019 Streamlink
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of
 this software and associated documentation files (the "Software"), to deal in
diff --git a/docs/conf.py b/docs/conf.py
index 9bf738c4..56c68923 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -37,7 +37,7 @@ master_doc = 'index'
 
 # General information about the project.
 project = 'Streamlink'
-copyright = '2018, Streamlink'
+copyright = '2019, Streamlink'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
diff --git a/docs/install.rst b/docs/install.rst
index bae970e4..f870358f 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -49,6 +49,9 @@ Distribution                         Installing
                                         $ nix-env -iA nixos.streamlink
 
                                      `NixOS channel`_
+`OpenBSD`_                           .. code-block:: console
+
+                                        $ doas pkg_add streamlink
 `Solus`_                             .. code-block:: console
 
                                         $ sudo eopkg install streamlink
@@ -70,6 +73,7 @@ Distribution                         Installing
 .. _Gentoo Linux: https://packages.gentoo.org/package/net-misc/streamlink
 .. _NetBSD (pkgsrc): http://pkgsrc.se/multimedia/streamlink
 .. _NixOS: https://github.com/NixOS/nixpkgs/tree/master/pkgs/applications/video/streamlink
+.. _OpenBSD: http://openports.se/multimedia/streamlink
 .. _Solus: https://dev.solus-project.com/source/streamlink/
 .. _Ubuntu: http://ppa.launchpad.net/nilarimogard/webupd8/ubuntu/pool/main/s/streamlink/
 .. _Void: https://github.com/void-linux/void-packages/tree/master/srcpkgs/streamlink
@@ -126,6 +130,7 @@ Fedora                               Mohamed El Morabity <melmorabity at fedorap
 Gentoo                               soredake <fdsfgs at krutt.org>
 NetBSD                               Maya Rashish <maya at netbsd.org>
 NixOS                                Tuomas Tynkkynen <tuomas.tynkkynen at iki.fi>
+OpenBSD                              Brian Callahan <bcallah at openbsd.org>
 Solus                                Bryan T. Meyers <bmeyers at datadrake.com>
 Ubuntu                               Alin Andrei <andrew at webupd8.org>
 Void                                 wkuipers <wietse at kuiprs.nl>
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 6bd754a1..6dd3bdde 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -101,7 +101,8 @@ huya                    huya.com             Yes   No    Temporarily only HLS st
 idf1                    idf1.fr              Yes   Yes
 ine                     ine.com              ---   Yes
 itvplayer               itv.com/itvplayer    Yes   Yes   Streams may be geo-restricted to Great Britain.
-kanal7                  kanal7.com           Yes   No
+kanal7                  - kanal7.com         Yes   No
+                        - tvt.tv.tr
 kingkong                kingkong.com.tw      Yes   --
 live_russia_tv          live.russia.tv       Yes   --
 liveedu                 - liveedu.tv         Yes   --    Some streams require a login.
@@ -211,9 +212,12 @@ trtspor                 trtspor.com          Yes   No    Some streams are geo-re
 turkuvaz                - atv.com.tr         Yes   No
                         - a2tv.com.tr
                         - ahaber.com.tr
+                        - anews.com.tr
                         - aspor.com.tr
-                        - minikago.com.tr
+                        - atvavrupa.tv
                         - minikacocuk.com.tr
+                        - minikago.com.tr
+                        - sabah.com.tr
 tv1channel              tv1channel.org       Yes   Yes
 tv3cat                  tv3.cat              Yes   Yes   Streams may be geo-restricted to Spain.
 tv4play                 - tv4play.se         Yes   Yes   Streams may be geo-restricted to Sweden.
@@ -278,6 +282,7 @@ zattoo                  - zattoo.com         Yes   Yes
                         - www.quantum-tv.com
                         - www.saktv.ch
                         - www.vtxtv.ch
+                        - www.1und1.tv
 zdf_mediathek           zdf.de               Yes   Yes   Streams may be geo-restricted to Germany.
 zengatv                 zengatv.com          Yes   No
 zhanqi                  zhanqi.tv            Yes   No
diff --git a/src/streamlink/__init__.py b/src/streamlink/__init__.py
index 8cb33913..82e8f712 100644
--- a/src/streamlink/__init__.py
+++ b/src/streamlink/__init__.py
@@ -15,7 +15,7 @@ del get_versions
 __title__ = "streamlink"
 __license__ = "Simplified BSD"
 __author__ = "Streamlink"
-__copyright__ = "Copyright 2018 Streamlink"
+__copyright__ = "Copyright 2019 Streamlink"
 __credits__ = [
     "Agustín Carrasco (@asermax)",
     "Andrew Bashore (@bashtech)",
diff --git a/src/streamlink/plugins/bbciplayer.py b/src/streamlink/plugins/bbciplayer.py
index 5e5d0f28..dec5978d 100644
--- a/src/streamlink/plugins/bbciplayer.py
+++ b/src/streamlink/plugins/bbciplayer.py
@@ -187,13 +187,20 @@ class BBCiPlayer(Plugin):
         :return: Whether authentication was successful
         :rtype: bool
         """
+        def auth_check(res):
+            return ptrt_url in ([h.url for h in res.history] + [res.url])
+
+        # make the session request to get the correct cookies
         session_res = self.session.http.get(
             self.session_url,
             params=dict(ptrt=ptrt_url)
         )
 
-        http_nonce = self._extract_nonce(session_res)
+        if auth_check(session_res):
+            log.debug("Already authenticated, skipping authentication")
+            return True
 
+        http_nonce = self._extract_nonce(session_res)
         res = self.session.http.post(
             self.auth_url,
             params=dict(
@@ -208,7 +215,7 @@ class BBCiPlayer(Plugin):
             ),
             headers={"Referer": self.url})
 
-        return len(res.history) != 0
+        return auth_check(res)
 
     def _get_streams(self):
         if not self.get_option("username"):
diff --git a/src/streamlink/plugins/btsports.py b/src/streamlink/plugins/btsports.py
index 30cd594f..29308c53 100644
--- a/src/streamlink/plugins/btsports.py
+++ b/src/streamlink/plugins/btsports.py
@@ -1,12 +1,15 @@
-import re
 import time
+
+import logging
+import re
 from uuid import uuid4
 
 from streamlink.compat import quote
 from streamlink.plugin import Plugin, PluginArguments, PluginArgument
 from streamlink.plugin.api import useragents
 from streamlink.stream import HLSStream
-from streamlink.utils import url_equal
+
+log = logging.getLogger(__name__)
 
 
 class BTSports(Plugin):
@@ -47,25 +50,23 @@ class BTSports(Plugin):
         return cls.url_re.match(url) is not None
 
     def login(self, username, password):
-        self.logger.debug("Logging in as {0}".format(username))
+        log.debug("Logging in as {0}".format(username))
 
-        redirect_to = "https://home.bt.com/ss/Satellite/secure/loginforward?redirectURL={0}".format(quote(self.url))
+        redirect_to = "https://home.bt.com/ss/Satellite/secure/loginforward?view=btsport&redirectURL={0}".format(quote(self.url))
         data = {
             "cookieExpp": "30",
             "Switch": "yes",
             "SMPostLoginUrl": "/appsyouraccount/secure/postlogin",
-            "loginforward": "https://home.bt.com/ss/Satellite/secure/loginforward",
+            "loginforward": "https://home.bt.com/ss/Satellite/secure/loginforward?view=btsport",
             "smauthreason": "0",
             "TARGET": redirect_to,
             "USER": username,
             "PASSWORD": password}
-
         res = self.session.http.post(self.login_url, data=data)
 
-        self.logger.debug("Redirected to: {0}".format(res.url))
-
+        log.debug("Redirected to: {0}".format(res.url))
 
-        if url_equal(res.url, self.url, ignore_scheme=True):
+        if "loginerror" not in res.text:
             self.logger.debug("Login successful, getting SAML token")
             res = self.session.http.get("https://samlfed.bt.com/sportgetfedwebhls?bt.cid={0}".format(self.acid()))
             d = self.saml_re.search(res.text)
@@ -81,7 +82,8 @@ class BTSports(Plugin):
                     self.logger.error("Failed to login: {0} - {1}".format(fed_json['errorDescription'],
                                                                           fed_json['message']))
                 return success
-        return False
+        else:
+            return False
 
     def device_id(self):
         device_id = self.cache.get("device_id") or str(uuid4())
@@ -108,19 +110,23 @@ class BTSports(Plugin):
     def _get_streams(self):
         if self.options.get("email") and self.options.get("password"):
             if self.login(self.options.get("email"), self.options.get("password")):
-                self.logger.debug("Logged in and authenticated with BT Sports.")
+                log.debug("Logged in and authenticated with BT Sports.")
 
                 res = self.session.http.get(self.url)
                 m = self.content_re.findall(res.text)
                 if m:
                     info = dict(m)
                     data = self._get_cdn(info.get("ID"), info.get("TYPE"))
+                    log.debug("CDN respsonse: {0}".format(data))
                     if data['resultCode'] == 'OK':
                         return HLSStream.parse_variant_playlist(self.session, data['resultObj']['src'])
                     else:
-                        self.logger.error("Failed to get stream with error: {0} - {1}".format(data['errorDescription'],
+                        log.error("Failed to get stream with error: {0} - {1}".format(data['errorDescription'],
                                                                                               data['message']))
+            else:
+                log.error("Login failed.")
         else:
-            self.logger.error("A username and password is required to use BT Sports")
+            log.error("A username and password is required to use BT Sports")
+
 
 __plugin__ = BTSports
diff --git a/src/streamlink/plugins/dogus.py b/src/streamlink/plugins/dogus.py
index e5314137..cf446fde 100644
--- a/src/streamlink/plugins/dogus.py
+++ b/src/streamlink/plugins/dogus.py
@@ -1,9 +1,14 @@
 import re
+import logging
 
 from streamlink.plugin import Plugin
+from streamlink.plugin.api.utils import itertags
+from streamlink.plugins.youtube import YouTube
 from streamlink.stream import HLSStream
 from streamlink.utils import update_scheme
 
+log = logging.getLogger(__name__)
+
 
 class Dogus(Plugin):
     """
@@ -14,13 +19,13 @@ class Dogus(Plugin):
         (?:
             ntv.com.tr/canli-yayin/ntv|
             ntvspor.net/canli-yayin|
-            kralmuzik.com.tr/tv/kral-tv|
-            kralmuzik.com.tr/tv/kral-pop-tv|
+            kralmuzik.com.tr/tv/|
             eurostartv.com.tr/canli-izle
         )/?""", re.VERBOSE)
     mobile_url_re = re.compile(r"""(?P<q>["'])(?P<url>(https?:)?//[^'"]*?/live/hls/[^'"]*?\?token=)
                                    (?P<token>[^'"]*?)(?P=q)""", re.VERBOSE)
     token_re = re.compile(r"""token=(?P<q>["'])(?P<token>[^'"]*?)(?P=q)""")
+    kral_token_url = "https://service.kralmuzik.com.tr/version/gettoken"
 
     @classmethod
     def can_handle_url(cls, url):
@@ -28,18 +33,31 @@ class Dogus(Plugin):
 
     def _get_streams(self):
         res = self.session.http.get(self.url)
-        mobile_url_m = self.mobile_url_re.search(res.text)
-
-        mobile_url = mobile_url_m and update_scheme(self.url, mobile_url_m.group("url"))
 
-        token = mobile_url_m and mobile_url_m.group("token")
-        if not token:
-            # if no token is in the url, try to find it else where in the page
-            token_m = self.token_re.search(res.text)
-            token = token_m and token_m.group("token")
+        # Look for Youtube embedded video first
+        for iframe in itertags(res.text, 'iframe'):
+            if YouTube.can_handle_url(iframe.attributes.get("src")):
+                log.debug("Handing off to YouTube plugin")
+                return self.session.streams(iframe.attributes.get("src"))
 
-        return HLSStream.parse_variant_playlist(self.session, mobile_url + token,
-                                                headers={"Referer": self.url})
+        # Next check for HLS URL with token
+        mobile_url_m = self.mobile_url_re.search(res.text)
+        mobile_url = mobile_url_m and update_scheme(self.url, mobile_url_m.group("url"))
+        if mobile_url:
+            log.debug("Found mobile stream: {0}".format(mobile_url_m.group(0)))
+
+            token = mobile_url_m and mobile_url_m.group("token")
+            if not token and "kralmuzik" in self.url:
+                log.debug("Getting Kral Muzik HLS stream token from API")
+                token = self.session.http.get(self.kral_token_url).text
+            elif not token:
+                # if no token is in the url, try to find it else where in the page
+                log.debug("Searching for HLS stream token in URL")
+                token_m = self.token_re.search(res.text)
+                token = token_m and token_m.group("token")
+
+            return HLSStream.parse_variant_playlist(self.session, mobile_url + token,
+                                                    headers={"Referer": self.url})
 
 
 __plugin__ = Dogus
diff --git a/src/streamlink/plugins/kanal7.py b/src/streamlink/plugins/kanal7.py
index 3d1e5103..29d0f463 100644
--- a/src/streamlink/plugins/kanal7.py
+++ b/src/streamlink/plugins/kanal7.py
@@ -1,17 +1,18 @@
-from __future__ import print_function
+import logging
 import re
 
+from streamlink.compat import urljoin, urlparse
 from streamlink.plugin import Plugin
-from streamlink.plugin.api import useragents
-from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
 from streamlink.utils import update_scheme
 
+log = logging.getLogger(__name__)
+
 
 class Kanal7(Plugin):
-    url_re = re.compile(r"https?://(?:www.)?kanal7.com/canli-izle")
-    iframe_re = re.compile(r'iframe .*?src="((?:http:)?//[^"]*?)"')
-    stream_re = re.compile(r'''video-source\s*=\s*['"](http[^"']*?)['"]''')
+    url_re = re.compile(r"https?://(?:www.)?(?:kanal7\.com|tvt\.tv\.tr)/canli-(?:izle|yayin)")
+    iframe_re = re.compile(r'iframe .*?src="([^"]*?iframe\.php)"')
+    stream_re = re.compile(r'''video-source(?:\s*=\s*|["'],)['"](http[^"']*?)['"]''')
 
     @classmethod
     def can_handle_url(cls, url):
@@ -23,24 +24,23 @@ class Kanal7(Plugin):
         iframe = self.iframe_re.search(res.text)
         iframe_url = iframe and iframe.group(1)
         if iframe_url:
+            parsed = urlparse(iframe_url)
+            if not parsed.netloc:
+                iframe_url = urljoin(self.url, iframe_url)
             iframe_url = update_scheme(self.url, iframe_url)
-            self.logger.debug("Found iframe: {}", iframe_url)
+            log.debug("Found iframe: {0}".format(iframe_url))
         return iframe_url
 
     def _get_streams(self):
         iframe1 = self.find_iframe(self.url)
         if iframe1:
-            iframe2 = self.find_iframe(iframe1)
-            if iframe2:
-                ires = self.session.http.get(iframe2)
-                stream_m = self.stream_re.search(ires.text)
-                stream_url = stream_m and stream_m.group(1)
-                if stream_url:
-                    yield "live", HLSStream(self.session, stream_url, headers={"Referer": iframe2})
-            else:
-                self.logger.error("Could not find second iframe, has the page layout changed?")
+            ires = self.session.http.get(iframe1)
+            stream_m = self.stream_re.search(ires.text)
+            stream_url = stream_m and stream_m.group(1)
+            if stream_url:
+                yield "live", HLSStream(self.session, stream_url, headers={"Referer": iframe1})
         else:
-            self.logger.error("Could not find iframe, has the page layout changed?")
+            log.error("Could not find iframe, has the page layout changed?")
 
 
 __plugin__ = Kanal7
diff --git a/src/streamlink/plugins/tf1.py b/src/streamlink/plugins/tf1.py
index 7c81d54f..1e4abb57 100644
--- a/src/streamlink/plugins/tf1.py
+++ b/src/streamlink/plugins/tf1.py
@@ -1,74 +1,58 @@
 from __future__ import print_function
+
+import logging
 import re
 
-from streamlink.compat import urlparse, parse_qsl
+from streamlink import PluginError
+from streamlink.compat import urljoin
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import useragents
-from streamlink.stream import HDSStream
-from streamlink.stream import HLSStream
-from streamlink.utils.url import update_qsd
+from streamlink.stream import DASHStream, HLSStream
+
+log = logging.getLogger(__name__)
 
 
 class TF1(Plugin):
     url_re = re.compile(r"https?://(?:www\.)?(?:tf1\.fr/([\w-]+)/direct|(lci).fr/direct)/?")
-    embed_url = "http://www.wat.tv/embedframe/live{0}"
-    embed_re = re.compile(r"urlLive.*?:.*?\"(http.*?)\"", re.MULTILINE)
-    api_url = "http://www.wat.tv/get/{0}/591997"
-    swf_url = "http://www.wat.tv/images/v70/PlayerLite.swf"
-    hds_channel_remap = {"tf1": "androidliveconnect",
-                         "lci": "androidlivelci",
-                         "tfx": "nt1live",
-                         "hd1": "hd1live",  # renamed to tfx
-                         "tf1-series-films": "hd1live"}
-    hls_channel_remap = {"lci": "LCI",
-                         "tf1": "V4",
-                         "tfx": "nt1",
-                         "tf1-series-films": "hd1"}
+    api_url_base = "https://delivery.tf1.fr/mytf1-wrd/"
+    token = "07e45841-a17a-47cf-af64-a42311bdcc3d"
+
+    def api_call(self, channel, useragent=useragents.CHROME):
+        url = urljoin(self.api_url_base, "L_"+channel.upper())
+        req = self.session.http.get(url,
+                                    params=dict(token=self.token),
+                                    headers={"User-Agent": useragent})
+        return self.session.http.json(req)
+
+    def get_stream_urls(self, channel):
+        for useragent in [useragents.CHROME, useragents.IPHONE_6]:
+            data = self.api_call(channel, useragent)
+
+            if data.get("error"):
+                log.error("Failed to get stream for {0}: {error} ({code})".format(channel, **data))
+            else:
+                log.debug("Got {format} stream {url}".format(**data))
+                yield data['format'], data['url']
 
     @classmethod
     def can_handle_url(cls, url):
         return cls.url_re.match(url) is not None
 
-    def _get_hds_streams(self, channel):
-        channel = self.hds_channel_remap.get(channel, "{0}live".format(channel))
-        self.logger.debug("Using HDS channel name: {0}".format(channel))
-        manifest_url = self.session.http.get(self.api_url.format(channel),
-                                params={"getURL": 1},
-                                headers={"User-Agent": useragents.FIREFOX}).text
-
-        for s in HDSStream.parse_manifest(self.session,
-                                          manifest_url,
-                                          pvswf=self.swf_url,
-                                          headers={"User-Agent": useragents.FIREFOX}).items():
-            yield s
-
-    def _get_hls_streams(self, channel):
-        channel = self.hls_channel_remap.get(channel, channel)
-        embed_url = self.embed_url.format(channel)
-        self.logger.debug("Found embed URL: {0}", embed_url)
-        # page needs to have a mobile user agent
-        embed_page = self.session.http.get(embed_url, headers={"User-Agent": useragents.ANDROID})
-
-        m = self.embed_re.search(embed_page.text)
-        if m:
-            # remove all query string arguments except hdnea
-            hls_stream_url = update_qsd(m.group(1), {"hdnea": None}, remove="*")
-            try:
-                for s in HLSStream.parse_variant_playlist(self.session, hls_stream_url).items():
-                    yield s
-            except Exception:
-                self.logger.error("Failed to load the HLS playlist for {0}", channel)
-
     def _get_streams(self):
         m = self.url_re.match(self.url)
         if m:
             channel = m.group(1) or m.group(2)
             self.logger.debug("Found channel {0}", channel)
-            for s in self._get_hds_streams(channel):
-                yield s
-
-            for s in self._get_hls_streams(channel):
-                yield s
-
+            for sformat, url in self.get_stream_urls(channel):
+                try:
+                    if sformat == "dash":
+                        for s in DASHStream.parse_manifest(self.session, url, headers={"User-Agent": useragents.CHROME}).items():
+                            yield s
+                    if sformat == "hls":
+                        for s in HLSStream.parse_variant_playlist(self.session, url).items():
+                            yield s
+                except PluginError as e:
+                    log.error("Could not open {0} stream".format(sformat))
+                    log.debug("Failed with error: {0}".format(e))
 
 __plugin__ = TF1
diff --git a/src/streamlink/plugins/trt.py b/src/streamlink/plugins/trt.py
index bca9b992..6b274a04 100644
--- a/src/streamlink/plugins/trt.py
+++ b/src/streamlink/plugins/trt.py
@@ -1,19 +1,20 @@
-from __future__ import print_function
+import logging
 import re
+
 from base64 import b64decode
 
+from streamlink.compat import parse_qsl, urlparse
 from streamlink.plugin import Plugin
-from streamlink.plugin.api import validate
-from streamlink.compat import urlparse, parse_qsl
-from streamlink.stream import HDSStream
-from streamlink.stream import HLSStream
+from streamlink.stream import HDSStream, HLSStream
+
+log = logging.getLogger(__name__)
 
 
 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.*", re.I)
+    url_re = re.compile(r"https?://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);''')
@@ -27,7 +28,7 @@ class TRT(Plugin):
     def _get_streams(self):
         args = dict(parse_qsl(urlparse(self.url).query))
         if "k" in args:
-            self.logger.debug("Loading channel: {k}", **args)
+            log.debug("Loading channel: {0}".format(args["k"]))
             res = self.session.http.get(self.url)
             stream_data_m = self.stream_data_re.search(res.text)
             if stream_data_m:
diff --git a/src/streamlink/plugins/turkuvaz.py b/src/streamlink/plugins/turkuvaz.py
index 59c24fed..947f5c79 100644
--- a/src/streamlink/plugins/turkuvaz.py
+++ b/src/streamlink/plugins/turkuvaz.py
@@ -1,5 +1,5 @@
-import random
 import re
+
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import useragents
 from streamlink.plugin.api import validate
@@ -11,13 +11,18 @@ class Turkuvaz(Plugin):
     Plugin to support ATV/A2TV Live streams from www.atv.com.tr and www.a2tv.com.tr
     """
 
-    _url_re = re.compile(r"""https?://(?:www.)?
-                             (?:(atvavrupa).tv|
-                                (atv|a2tv|ahaber|aspor|minikago|minikacocuk|anews).com.tr)
-                                /webtv/(live-broadcast|canli-yayin)""",
-                         re.VERBOSE)
-    _hls_url = "http://trkvz-live.ercdn.net/{channel}/{channel}.m3u8"
-    _token_url = "http://videotoken.tmgrup.com.tr/webtv/secure"
+    _url_re = re.compile(r"""(?x)https?://(?:www\.)?
+    (?:
+        (?:
+            (atvavrupa)\.tv
+            |
+            (atv|a2tv|ahaber|aspor|minikago|minikacocuk|anews)\.com\.tr
+        )/webtv/(?:live-broadcast|canli-yayin)
+    |
+        sabah\.com\.tr/(apara)/canli-yayin
+    )""")
+    _hls_url = "https://trkvz-live.ercdn.net/{channel}/{channel}.m3u8"
+    _token_url = "https://securevideotoken.tmgrup.com.tr/webtv/secure"
     _token_schema = validate.Schema(validate.all(
         {
             "Success": True,
@@ -32,19 +37,20 @@ class Turkuvaz(Plugin):
 
     def _get_streams(self):
         url_m = self._url_re.match(self.url)
-        domain = url_m.group(1) or url_m.group(2)
+        domain = url_m.group(1) or url_m.group(2) or url_m.group(3)
         # remap the domain to channel
         channel = {"atv": "atvhd",
                    "ahaber": "ahaberhd",
+                   "apara": "aparahd",
                    "aspor": "asporhd",
                    "anews": "anewshd",
                    "minikacocuk": "minikagococuk"}.get(domain, domain)
         hls_url = self._hls_url.format(channel=channel)
         # get the secure HLS URL
         res = self.session.http.get(self._token_url,
-                       params="url={0}".format(hls_url),
-                       headers={"Referer": self.url,
-                                "User-Agent": useragents.CHROME})
+                                    params="url={0}".format(hls_url),
+                                    headers={"Referer": self.url,
+                                             "User-Agent": useragents.CHROME})
 
         secure_hls_url = self.session.http.json(res, schema=self._token_schema)
 
diff --git a/src/streamlink/plugins/tvrby.py b/src/streamlink/plugins/tvrby.py
index a6bbc463..ace887f4 100644
--- a/src/streamlink/plugins/tvrby.py
+++ b/src/streamlink/plugins/tvrby.py
@@ -1,21 +1,23 @@
-from __future__ import print_function
 import re
 
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
-from streamlink.stream import RTMPStream
 
 
 class TVRBy(Plugin):
     url_re = re.compile(r"""https?://(?:www\.)?tvr.by/televidenie/belarus""")
-    file_re = re.compile(r"""(?P<q1>["']?)file(?P=q1)\s*:\s*(?P<q2>["'])(?P<url>(?:http.+?m3u8.*?|rtmp://.*?))(?P=q2)""")
+    file_re = re.compile(r"""(?P<url>https://stream\.hoster\.by[^"',]+\.m3u8[^"',]*)""")
+    player_re = re.compile(r"""["'](?P<url>[^"']+tvr\.by/plugines/online-tv-main\.php[^"']+)["']""")
 
     stream_schema = validate.Schema(
         validate.all(
             validate.transform(file_re.finditer),
             validate.transform(list),
-            [validate.get("url")]
+            [validate.get("url")],
+            # remove duplicates
+            validate.transform(set),
+            validate.transform(list),
         ),
     )
 
@@ -29,26 +31,21 @@ class TVRBy(Plugin):
     def can_handle_url(cls, url):
         return cls.url_re.match(url) is not None
 
-    @Plugin.broken()
     def _get_streams(self):
         res = self.session.http.get(self.url)
+        m = self.player_re.search(res.text)
+        if not m:
+            return
+
+        player_url = m.group("url")
+        res = self.session.http.get(player_url)
         stream_urls = self.stream_schema.validate(res.text)
         self.logger.debug("Found {0} stream URL{1}", len(stream_urls),
                           "" if len(stream_urls) == 1 else "s")
 
         for stream_url in stream_urls:
-            if "m3u8" in stream_url:
-                for _, s in HLSStream.parse_variant_playlist(self.session, stream_url).items():
-                    yield "live", s
-            if stream_url.startswith("rtmp://"):
-                a = stream_url.split("///")
-                s = RTMPStream(self.session, {
-                    "rtmp": a[0],
-                    "playpath": "live",
-                    "swfVfy": "http://www.tvr.by/plugines/uppod/uppod.swf",
-                    "pageUrl": self.url
-                })
-                yield "live", s
+            for s in HLSStream.parse_variant_playlist(self.session, stream_url).items():
+                yield s
 
 
 __plugin__ = TVRBy
diff --git a/src/streamlink/plugins/ustreamtv.py b/src/streamlink/plugins/ustreamtv.py
index 31fa7138..d025ef1d 100644
--- a/src/streamlink/plugins/ustreamtv.py
+++ b/src/streamlink/plugins/ustreamtv.py
@@ -83,7 +83,7 @@ class UHSClient(object):
         log.debug("Connecting to {0}".format(self.host))
         self._ws = websocket.create_connection(self.host,
                                                header=["User-Agent: {0}".format(useragents.CHROME)],
-                                               origin="http://www.ustream.tv")
+                                               origin="https://www.ustream.tv")
 
         args = dict(type="viewer",
                     appId=self._app_id,
@@ -269,9 +269,11 @@ class UHSStream(Stream):
             while not self.stopped.wait(0):
                 try:
                     cmd_args = self.api.recv()
-                except SocketError as err:
+                except (SocketError,
+                        websocket._exceptions.WebSocketConnectionClosedException) as err:
                     cmd_args = None
-                    if err.errno in (errno.ECONNRESET, errno.ETIMEDOUT):
+                    if (hasattr(err, "errno") and err.errno in (errno.ECONNRESET, errno.ETIMEDOUT)
+                            or "Connection is already closed." in str(err)):
                         while True:
                             # --stream-timeout will handle the timeout
                             try:
@@ -285,6 +287,8 @@ class UHSStream(Stream):
                                 sleep(reconnect_time_ws)
                     else:
                         raise
+                except PluginError:
+                    continue
 
                 if not cmd_args:
                     continue
@@ -390,7 +394,7 @@ class UStreamTV(Plugin):
                     res["streams"] = []
                     for stream in flv_segmented["streams"]:
                         res["streams"] += [dict(
-                            stream_name=stream["preset"],
+                            stream_name="{0}p".format(stream["videoCodec"]["height"]),
                             path=urljoin(path,
                                          stream["segmentUrl"].replace("%", "%s")),
                             hashes=flv_segmented["hashes"],
@@ -434,7 +438,6 @@ class UStreamTV(Plugin):
             api.connect()
 
             streams_data = {}
-            streams = {}
             for _ in range(5):
                 # do not use to many tries, it might take longer for a timeout
                 # when streamFormats is {} and contentAvailable is True
@@ -450,11 +453,12 @@ class UStreamTV(Plugin):
                         log.debug("Unexpected `{0}` command".format(data["cmd"]))
                         log.trace("{0!r}".format(data))
                 except ModuleInfoNoStreams:
-                    return None
+                    break
 
                 if streams_data.get("streams") and streams_data.get("cdn_url"):
-                    for s in streams_data["streams"]:
-                        streams[s["stream_name"]] = UHSStream(
+                    for s in sorted(streams_data["streams"],
+                                    key=lambda k: (k["stream_name"], k["path"])):
+                        yield s["stream_name"], UHSStream(
                             session=self.session,
                             api=api,
                             first_chunk_data=ChunkData(
@@ -465,7 +469,7 @@ class UStreamTV(Plugin):
                             template_url=urljoin(streams_data["cdn_url"],
                                                  s["path"]),
                         )
-                    return streams
+                    break
 
     def _get_media_app(self):
         umatch = self.url_re.match(self.url)
diff --git a/src/streamlink/plugins/youtube.py b/src/streamlink/plugins/youtube.py
index b1081c13..4032646c 100644
--- a/src/streamlink/plugins/youtube.py
+++ b/src/streamlink/plugins/youtube.py
@@ -4,7 +4,7 @@ import argparse
 import logging
 import re
 
-from streamlink.compat import parse_qsl, is_py2
+from streamlink.compat import is_py2, parse_qsl, urlparse, urlunparse
 from streamlink.plugin import Plugin, PluginError, PluginArguments, PluginArgument
 from streamlink.plugin.api import validate, useragents
 from streamlink.plugin.api.utils import itertags, parse_query
@@ -76,6 +76,15 @@ _config_schema = validate.Schema(
             }]
         ),
         validate.optional("hlsvp"): validate.text,
+        validate.optional("player_response"): validate.all(
+            validate.text,
+            validate.transform(parse_json),
+            {
+                validate.optional("streamingData"): {
+                    validate.optional("hlsManifestUrl"): validate.text,
+                }
+            }
+        ),
         validate.optional("live_playback"): validate.transform(bool),
         validate.optional("reason"): validate.all(validate.text, validate.transform(maybe_decode)),
         validate.optional("livestream"): validate.text,
@@ -156,6 +165,10 @@ class YouTube(Plugin):
 
     def __init__(self, url):
         super(YouTube, self).__init__(url)
+        parsed = urlparse(self.url)
+        if parsed.netloc == 'gaming.youtube.com':
+            self.url = urlunparse(parsed._replace(netloc='www.youtube.com'))
+
         self.author = None
         self.title = None
         self.video_id = None
@@ -349,7 +362,7 @@ class YouTube(Plugin):
         if not is_live:
             streams, protected = self._create_adaptive_streams(info, streams, protected)
 
-        hls_playlist = info.get("hlsvp")
+        hls_playlist = info.get("hlsvp") or info.get("player_response", {}).get("streamingData", {}).get("hlsManifestUrl")
         if hls_playlist:
             try:
                 hls_streams = HLSStream.parse_variant_playlist(
diff --git a/src/streamlink/plugins/zattoo.py b/src/streamlink/plugins/zattoo.py
index 9a8c4a01..c4f726a1 100644
--- a/src/streamlink/plugins/zattoo.py
+++ b/src/streamlink/plugins/zattoo.py
@@ -43,7 +43,7 @@ class Zattoo(Plugin):
                 tvonline\.ewe|nettv\.netcologne|tvplus\.m-net
             )\.de
             )|(?:(?:
-                player\.waly|www\.netplus
+                player\.waly|www\.(?:1und1|netplus)
             )\.tv)
             |www\.bbv-tv\.net
             |www\.meinewelt\.cc
diff --git a/src/streamlink/plugins/zdf_mediathek.py b/src/streamlink/plugins/zdf_mediathek.py
index 4db51ddb..8aaa959b 100644
--- a/src/streamlink/plugins/zdf_mediathek.py
+++ b/src/streamlink/plugins/zdf_mediathek.py
@@ -4,6 +4,7 @@ from streamlink.plugin import Plugin
 from streamlink.plugin.api import validate
 from streamlink.stream import HDSStream, HLSStream
 from streamlink.utils import parse_json
+from streamlink.utils.url import url_concat
 
 API_URL = "https://api.zdf.de"
 
@@ -48,7 +49,7 @@ _documents_schema = validate.Schema(
     {
         "mainVideoContent": {
             "http://zdf.de/rels/target": {
-                "http://zdf.de/rels/streams/ptmd": validate.text
+                "http://zdf.de/rels/streams/ptmd-template": validate.text
             },
         },
     }
@@ -128,6 +129,7 @@ class zdf_mediathek(Plugin):
             return
 
         headers = {
+            "Accept": "application/vnd.de.zdf.v1.0+json",
             "Api-Auth": "Bearer {0}".format(zdf_json['apiToken']),
             "Referer": self.url
         }
@@ -135,8 +137,8 @@ class zdf_mediathek(Plugin):
         res = self.session.http.get(zdf_json['content'], headers=headers)
         document = self.session.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
+        stream_request_url = document["mainVideoContent"]["http://zdf.de/rels/target"]["http://zdf.de/rels/streams/ptmd-template"]
+        stream_request_url = url_concat(API_URL, stream_request_url.format(playerId="ngplayer_2_3").replace(" ", ""))
 
         res = self.session.http.get(stream_request_url, headers=headers)
         res = self.session.http.json(res, schema=_schema)
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index 019a55cb..3958a085 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -465,6 +465,9 @@ def build_parser():
             gaming oriented platforms. "Game being played" is a way to categorize
             the stream, so it doesn't need its own separate handling.
 
+        {{url}}
+            URL of the stream.
+
         Examples:
 
             %(prog)s -p vlc --title "{{title}} -!- {{author}} -!- {{category}} \\$A" <url> [stream]
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index efbe790c..9acb1094 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -152,7 +152,8 @@ def create_title(plugin=None):
             title=lambda: plugin.get_title() or DEFAULT_STREAM_METADATA["title"],
             author=lambda: plugin.get_author() or DEFAULT_STREAM_METADATA["author"],
             category=lambda: plugin.get_category() or DEFAULT_STREAM_METADATA["category"],
-            game=lambda: plugin.get_category() or DEFAULT_STREAM_METADATA["game"]
+            game=lambda: plugin.get_category() or DEFAULT_STREAM_METADATA["game"],
+            url=plugin.url
         )
     else:
         title = args.url
@@ -338,7 +339,7 @@ def read_stream(stream, output, prebuffer, chunk_size=8192):
     is_http = isinstance(output, HTTPServer)
     is_fifo = is_player and output.namedpipe
     show_progress = isinstance(output, FileOutput) and output.fd is not stdout and sys.stdout.isatty()
-    show_record_progress = isinstance(output.record, FileOutput) and output.record.fd is not stdout and sys.stdout.isatty()
+    show_record_progress = hasattr(output, "record") and isinstance(output.record, FileOutput) and output.record.fd is not stdout and sys.stdout.isatty()
 
     stream_iterator = chain(
         [prebuffer],


Test patch:
diff --git a/tests/plugins/test_bbciplayer.py b/tests/plugins/test_bbciplayer.py
index cef746fe..381307de 100644
--- a/tests/plugins/test_bbciplayer.py
+++ b/tests/plugins/test_bbciplayer.py
@@ -1,5 +1,4 @@
 import json
-import logging
 import unittest
 
 from requests import Response, Request
diff --git a/tests/plugins/test_kanal7.py b/tests/plugins/test_kanal7.py
index b54a7ac7..8d539625 100644
--- a/tests/plugins/test_kanal7.py
+++ b/tests/plugins/test_kanal7.py
@@ -7,6 +7,7 @@ class TestPluginKanal7(unittest.TestCase):
     def test_can_handle_url(self):
         should_match = [
             'http://www.kanal7.com/canli-izle',
+            'https://www.tvt.tv.tr/canli-yayin',
         ]
         for url in should_match:
             self.assertTrue(Kanal7.can_handle_url(url))
@@ -17,3 +18,31 @@ class TestPluginKanal7(unittest.TestCase):
         ]
         for url in should_not_match:
             self.assertFalse(Kanal7.can_handle_url(url))
+
+    def test_iframe_re(self):
+        test_list = [
+            # http://www.kanal7.com/canli-izle
+            ('<iframe id="selectFrame" src="https://www.kanal7.com/canli-yayin-iframe.php" scrolling="no"></iframe>',
+             'https://www.kanal7.com/canli-yayin-iframe.php'),
+            # https://www.tvt.tv.tr/canli-yayin
+            ('<iframe id="selectFrame" src="/canli-yayin-iframe.php" height="650"></iframe>',
+             '/canli-yayin-iframe.php'),
+        ]
+        for text, url in test_list:
+            m = Kanal7.iframe_re.search(text)
+            self.assertIsNotNone(m, url)
+            self.assertEqual(m.group(1), url, url)
+
+    def test_stream_re(self):
+        test_list = [
+            # http://www.kanal7.com/canli-izle
+            ('''script.setAttribute('video-source','https://live/kanal7LiveDesktop/index.m3u8');''',
+             'https://live/kanal7LiveDesktop/index.m3u8'),
+            # https://www.tvt.tv.tr/canli-yayin
+            ('''video-source="https://live/tvtLiveStream/index.m3u8"''',
+             'https://live/tvtLiveStream/index.m3u8'),
+        ]
+        for text, url in test_list:
+            m = Kanal7.stream_re.search(text)
+            self.assertIsNotNone(m, url)
+            self.assertEqual(m.group(1), url, url)
diff --git a/tests/plugins/test_trt.py b/tests/plugins/test_trt.py
index c79d2923..ab16c975 100644
--- a/tests/plugins/test_trt.py
+++ b/tests/plugins/test_trt.py
@@ -7,6 +7,7 @@ class TestPluginTRT(unittest.TestCase):
     def test_can_handle_url(self):
         should_match = [
             'http://www.trt.net.tr/anasayfa/canli.aspx?y=tv&k=trt1',
+            'https://www.trt.net.tr/anasayfa/canli.aspx?y=tv&k=trtworld',
         ]
         for url in should_match:
             self.assertTrue(TRT.can_handle_url(url))
@@ -14,6 +15,7 @@ class TestPluginTRT(unittest.TestCase):
     def test_can_handle_url_negative(self):
         should_not_match = [
             'https://example.com/index.html',
+            'https://trt.net.tr/anasayfa/canli.aspx?y=tv&k=trtworld',
         ]
         for url in should_not_match:
             self.assertFalse(TRT.can_handle_url(url))
diff --git a/tests/plugins/test_turkuvaz.py b/tests/plugins/test_turkuvaz.py
index cc4ae0fb..8098cb04 100644
--- a/tests/plugins/test_turkuvaz.py
+++ b/tests/plugins/test_turkuvaz.py
@@ -10,6 +10,11 @@ class TestPluginTurkuvaz(unittest.TestCase):
             'http://www.a2tv.com.tr/webtv/canli-yayin',
             'https://www.ahaber.com.tr/webtv/canli-yayin',
             'https://www.aspor.com.tr/webtv/canli-yayin',
+            'http://www.anews.com.tr/webtv/live-broadcast',
+            'http://www.atvavrupa.tv/webtv/canli-yayin',
+            'http://www.minikacocuk.com.tr/webtv/canli-yayin',
+            'http://www.minikago.com.tr/webtv/canli-yayin',
+            'https://www.sabah.com.tr/apara/canli-yayin',
         ]
         for url in should_match:
             self.assertTrue(Turkuvaz.can_handle_url(url))
diff --git a/tests/plugins/test_zattoo.py b/tests/plugins/test_zattoo.py
index 98fecc58..cc9360e4 100644
--- a/tests/plugins/test_zattoo.py
+++ b/tests/plugins/test_zattoo.py
@@ -21,6 +21,8 @@ class TestPluginZattoo(unittest.TestCase):
         self.assertTrue(Zattoo.can_handle_url('http://tvonline.ewe.de/watch/zdf'))
         self.assertTrue(Zattoo.can_handle_url('https://nettv.netcologne.de/watch/daserste'))
         self.assertTrue(Zattoo.can_handle_url('https://nettv.netcologne.de/watch/zdf'))
+        self.assertTrue(Zattoo.can_handle_url('https://www.1und1.tv/watch/daserste'))
+        self.assertTrue(Zattoo.can_handle_url('https://www.1und1.tv/watch/zdf'))
         # zattoo live
         self.assertTrue(Zattoo.can_handle_url('https://zattoo.com/watch/daserste'))
         self.assertTrue(Zattoo.can_handle_url('https://zattoo.com/watch/zdf'))
diff --git a/tests/test_lazy_formatter.py b/tests/test_lazy_formatter.py
index 259bc2bf..f41c8ab2 100644
--- a/tests/test_lazy_formatter.py
+++ b/tests/test_lazy_formatter.py
@@ -10,6 +10,7 @@ class TestLazyFormat(unittest.TestCase):
         plugin.get_title.return_value = "title"
         plugin.get_author.return_value = "author"
         plugin.get_game.return_value = "game"
+        plugin.url = "url"
 
         return plugin
 
@@ -19,7 +20,8 @@ class TestLazyFormat(unittest.TestCase):
         res = LazyFormatter.format("{title}",
                                    title=plugin.get_title,
                                    author=plugin.get_author,
-                                   game=plugin.get_game)
+                                   game=plugin.get_game,
+                                   url=plugin.url)
 
         self.assertEqual("title", res)
 
@@ -33,7 +35,8 @@ class TestLazyFormat(unittest.TestCase):
         res = LazyFormatter.format("{title} - {author}",
                                    title=plugin.get_title,
                                    author=plugin.get_author,
-                                   game=plugin.get_game)
+                                   game=plugin.get_game,
+                                   url=plugin.url)
 
         self.assertEqual("title - author", res)
 
@@ -48,7 +51,8 @@ class TestLazyFormat(unittest.TestCase):
         res = LazyFormatter.format("{title} - {author} - {game}",
                                    title=plugin.get_title,
                                    author=plugin.get_author,
-                                   game=plugin.get_game)
+                                   game=plugin.get_game,
+                                   url=plugin.url)
 
         self.assertEqual("title - author - game", res)
 
@@ -56,6 +60,21 @@ class TestLazyFormat(unittest.TestCase):
         plugin.get_author.assert_called()
         plugin.get_game.assert_called()
 
+    def test_format_lazy_title_author_game_url(self):
+        plugin = self._get_fake_plugin()
+
+        res = LazyFormatter.format("{title} - {author} - {game} - {url}",
+                                   title=plugin.get_title,
+                                   author=plugin.get_author,
+                                   game=plugin.get_game,
+                                   url=plugin.url)
+
+        self.assertEqual("title - author - game - url", res)
+
+        plugin.get_title.assert_called()
+        plugin.get_author.assert_called()
+        plugin.get_game.assert_called()
+
     def test_format_lazy_title_not_callable(self):
         res = LazyFormatter.format("{title}",
                                    title="title")