Repository: streamlink/streamlink
Pull request #2224 (2019-01-01 22:07:58)

Add window title to PotPlayer
After #1576 we can add window title support to other players.

Due to limitations of PotPlayer, titles can only be added to URL streams and there is no separate title argument.
PotPlayer command line documentation in `PotPlayer - About - Command Line`:
- You can specify titles for URLs by separating them with a backslash (\\) at the end of URLs. ("http://...\title of this url")

Because of this, the title needs to be appended at the end of the filename so args need to be formatted after player check.

Here's an example of it working on Windows 10:
![potplayer-title](https://user-images.githubusercontent.com/5807561/50576629-bdf4e480-0e26-11e9-9f21-2d3def2d9536.png)




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/setup.py b/setup.py
index 54007193..2ec570ab 100644
--- a/setup.py
+++ b/setup.py
@@ -13,8 +13,8 @@ deps = [
     'futures;python_version<"3.0"',
     # Require singledispatch on Python <3.4
     'singledispatch;python_version<"3.4"',
-    "requests>=2.2,!=2.12.0,!=2.12.1,!=2.16.0,!=2.16.1,!=2.16.2,!=2.16.3,!=2.16.4,!=2.16.5,!=2.17.1,<3.0",
-    'urllib3[secure]<1.23,>=1.21.1;python_version<"3.0"',
+    "requests>=2.21.0,<3.0",
+    'urllib3[secure]>=1.23;python_version<"3.0"',
     "isodate",
     "websocket-client",
     # Support for SOCKS proxies
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/filmon.py b/src/streamlink/plugins/filmon.py
index ffc0fbe6..1a2e77eb 100644
--- a/src/streamlink/plugins/filmon.py
+++ b/src/streamlink/plugins/filmon.py
@@ -164,7 +164,7 @@ class Filmon(Plugin):
             |
             (?:tv/)channel/(?:export\?)?
             |
-            tv/(?!channel)
+            tv/(?!channel/)
             |
             channel/
             |
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/powerapp.py b/src/streamlink/plugins/powerapp.py
index 0ad0dda1..702505e5 100644
--- a/src/streamlink/plugins/powerapp.py
+++ b/src/streamlink/plugins/powerapp.py
@@ -7,7 +7,7 @@ from streamlink.stream import HLSStream
 
 
 class PowerApp(Plugin):
-    url_re = re.compile(r"https?://(?:www.)?powerapp.com.tr/tv/(\w+)")
+    url_re = re.compile(r"https?://(?:www.)?powerapp.com.tr/tvs?/(\w+)")
     api_url = "http://api.powergroup.com.tr/Channels/{0}/?appRef=iPowerWeb&apiVersion=11"
     api_schema = validate.Schema(validate.all({
         "errorCode": 0,
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 e018ccba..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]
@@ -490,7 +493,7 @@ def build_parser():
         "-f", "--force",
         action="store_true",
         help="""
-        When using -o, always write to file even if it already exists.
+        When using -o or -r, always write to file even if it already exists.
         """
     )
     output.add_argument(
@@ -500,6 +503,24 @@ def build_parser():
         Write stream data to stdout instead of playing it.
         """
     )
+    output.add_argument(
+        "-r", "--record",
+        metavar="FILENAME",
+        help="""
+        Open the stream in the player, while at the same time writing it to FILENAME.
+
+        You will be prompted if the file already exists.
+        """
+    )
+    output.add_argument(
+        "-R", "--record-and-pipe",
+        metavar="FILENAME",
+        help="""
+        Write stream data to stdout, while at the same time writing it to FILENAME.
+
+        You will be prompted if the file already exists.
+        """
+    )
 
     stream = parser.add_argument_group("Stream options")
     stream.add_argument(
@@ -855,7 +876,7 @@ def build_parser():
         """
     )
     transport.add_argument(
-        "--rtmp-rtmpdump", "--rtmpdump", "-r",
+        "--rtmp-rtmpdump", "--rtmpdump",
         metavar="FILENAME",
         help="""
         RTMPDump is used to access RTMP streams. You can specify the
diff --git a/src/streamlink_cli/constants.py b/src/streamlink_cli/constants.py
index 45eb7894..d0407a78 100644
--- a/src/streamlink_cli/constants.py
+++ b/src/streamlink_cli/constants.py
@@ -13,7 +13,8 @@ DEFAULT_STREAM_METADATA = {
 SUPPORTED_PLAYERS = {  # these are the players that streamlink knows how to set the window title for with `--title`. key names are used in help text
     # name: possible binary names (linux/mac and windows)
     "vlc": ["vlc", "vlc.exe"],
-    "mpv": ["mpv", "mpv.exe"]
+    "mpv": ["mpv", "mpv.exe"],
+    "potplayer": ["potplayer", "potplayermini64.exe", "potplayermini.exe"]
 }
 
 if is_win32:
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 4e812b5e..9acb1094 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -81,6 +81,9 @@ def create_output(plugin):
 
     """
 
+    if (args.output or args.stdout) and (args.record or args.record_and_pipe):
+        console.exit("Cannot use record options with other file output options.")
+
     if args.output:
         if args.output == "-":
             out = FileOutput(fd=stdout)
@@ -88,8 +91,11 @@ def create_output(plugin):
             out = check_file_output(args.output, args.force)
     elif args.stdout:
         out = FileOutput(fd=stdout)
+    elif args.record_and_pipe:
+        record = check_file_output(args.record_and_pipe, args.force)
+        out = FileOutput(fd=stdout, record=record)
     else:
-        http = namedpipe = None
+        http = namedpipe = record = None
 
         if not args.player:
             console.exit("The default player (VLC) does not seem to be "
@@ -108,12 +114,17 @@ def create_output(plugin):
             http = create_http_server()
 
         title = create_title(plugin)
+
+        if args.record:
+            record = check_file_output(args.record, args.force)
+
         log.info("Starting player: {0}", args.player)
+
         out = PlayerOutput(args.player, args=args.player_args,
                            quiet=not args.verbose_player,
                            kill=not args.player_no_close,
                            namedpipe=namedpipe, http=http,
-                           title=title)
+                           record=record, title=title)
 
     return out
 
@@ -141,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
@@ -327,6 +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 = hasattr(output, "record") and isinstance(output.record, FileOutput) and output.record.fd is not stdout and sys.stdout.isatty()
 
     stream_iterator = chain(
         [prebuffer],
@@ -335,6 +348,9 @@ def read_stream(stream, output, prebuffer, chunk_size=8192):
     if show_progress:
         stream_iterator = progress(stream_iterator,
                                    prefix=os.path.basename(args.output))
+    elif show_record_progress:
+        stream_iterator = progress(stream_iterator,
+                                   prefix=os.path.basename(args.record))
 
     try:
         for data in stream_iterator:
@@ -965,7 +981,7 @@ def main():
 
     # Console output should be on stderr if we are outputting
     # a stream to stdout.
-    if args.stdout or args.output == "-":
+    if args.stdout or args.output == "-" or args.record_and_pipe:
         console_out = sys.stderr
     else:
         console_out = sys.stdout
diff --git a/src/streamlink_cli/output.py b/src/streamlink_cli/output.py
index 925a802c..e04471be 100644
--- a/src/streamlink_cli/output.py
+++ b/src/streamlink_cli/output.py
@@ -47,31 +47,39 @@ class Output(object):
 
 
 class FileOutput(Output):
-    def __init__(self, filename=None, fd=None):
+    def __init__(self, filename=None, fd=None, record=None):
         super(FileOutput, self).__init__()
         self.filename = filename
         self.fd = fd
+        self.record = record
 
     def _open(self):
         if self.filename:
             self.fd = open(self.filename, "wb")
 
+        if self.record:
+            self.record.open()
+
         if is_win32:
             msvcrt.setmode(self.fd.fileno(), os.O_BINARY)
 
     def _close(self):
         if self.fd is not stdout:
             self.fd.close()
+        if self.record:
+            self.record.close()
 
     def _write(self, data):
         self.fd.write(data)
+        if self.record:
+            self.record.write(data)
 
 
 class PlayerOutput(Output):
     PLAYER_TERMINATE_TIMEOUT = 10.0
 
     def __init__(self, cmd, args=DEFAULT_PLAYER_ARGUMENTS, filename=None, quiet=True, kill=True, call=False, http=None,
-                 namedpipe=None, title=None):
+                 namedpipe=None, record=None, title=None):
         super(PlayerOutput, self).__init__()
         self.cmd = cmd
         self.args = args
@@ -85,6 +93,7 @@ class PlayerOutput(Output):
         self.title = title
         self.player = None
         self.player_name = self.supported_player(self.cmd)
+        self.record = record
 
         if self.namedpipe or self.filename or self.http:
             self.stdin = sys.stdin
@@ -168,8 +177,6 @@ class PlayerOutput(Output):
             filename = self.http.url
         else:
             filename = "-"
-        args = self.args.format(filename=filename)
-        cmd = self.cmd
         extra_args = []
 
         if self.title is not None:
@@ -185,6 +192,17 @@ class PlayerOutput(Output):
                 self.title = self._mpv_title_escape(self.title)
                 extra_args.extend(["--title", self.title])
 
+            # potplayer
+            if self.player_name == "potplayer":
+                if filename != "-":
+                    # PotPlayer - About - Command Line
+                    # You can specify titles for URLs by separating them with a backslash (\) at the end of URLs. ("http://...\title of this url")
+                    self.title = self.title.replace('"', '')
+                    filename = filename[:-1] + '\\' + self.title + filename[-1]
+
+        args = self.args.format(filename=filename)
+        cmd = self.cmd
+        
         # player command
         if is_win32:
             eargs = maybe_decode(subprocess.list2cmdline(extra_args))
@@ -195,6 +213,8 @@ class PlayerOutput(Output):
 
     def _open(self):
         try:
+            if self.record:
+                self.record.open()
             if self.call and self.filename:
                 self._open_call()
             else:
@@ -207,7 +227,11 @@ class PlayerOutput(Output):
 
     def _open_call(self):
         args = self._create_arguments()
-        log.debug(u"Calling: {0}".format(subprocess.list2cmdline(args)))
+        if is_win32:
+            fargs = args
+        else:
+            fargs = subprocess.list2cmdline(args)
+        log.debug(u"Calling: {0}".format(fargs))
         subprocess.call(args,
                         stdout=self.stdout,
                         stderr=self.stderr)
@@ -216,7 +240,11 @@ class PlayerOutput(Output):
         # Force bufsize=0 on all Python versions to avoid writing the
         # unflushed buffer when closing a broken input pipe
         args = self._create_arguments()
-        log.debug(u"Opening subprocess: {0}".format(subprocess.list2cmdline(args)))
+        if is_win32:
+            fargs = args
+        else:
+            fargs = subprocess.list2cmdline(args)
+        log.debug(u"Opening subprocess: {0}".format(fargs))
         self.player = subprocess.Popen(args,
                                        stdin=self.stdin, bufsize=0,
                                        stdout=self.stdout,
@@ -240,6 +268,9 @@ class PlayerOutput(Output):
         elif not self.filename:
             self.player.stdin.close()
 
+        if self.record:
+            self.record.close()
+
         if self.kill:
             with ignored(Exception):
                 self.player.terminate()
@@ -254,6 +285,9 @@ class PlayerOutput(Output):
         self.player.wait()
 
     def _write(self, data):
+        if self.record:
+            self.record.write(data)
+
         if self.namedpipe:
             self.namedpipe.write(data)
         elif self.http:


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_filmon.py b/tests/plugins/test_filmon.py
index a62d407d..ab30fc5d 100644
--- a/tests/plugins/test_filmon.py
+++ b/tests/plugins/test_filmon.py
@@ -10,24 +10,25 @@ class TestPluginFilmon(unittest.TestCase):
             'http://www.filmon.tv/index/popout?channel_id=5510&quality=low',
             'http://www.filmon.tv/tv/channel/export?channel_id=5510&autoPlay=1',
             'http://www.filmon.tv/tv/channel/grandstand-show',
+            'http://www.filmon.tv/tv/channel-4',
             'https://www.filmon.com/tv/bbc-news',
             'https://www.filmon.tv/tv/55',
             'http://www.filmon.tv/vod/view/10250-0-crime-boss',
             'http://www.filmon.tv/group/comedy',
         ]
         for url in should_match:
-            self.assertTrue(Filmon.can_handle_url(url))
+            self.assertTrue(Filmon.can_handle_url(url), url)
 
     def test_can_handle_url_negative(self):
         should_not_match = [
             'https://example.com/index.html',
         ]
         for url in should_not_match:
-            self.assertFalse(Filmon.can_handle_url(url))
+            self.assertFalse(Filmon.can_handle_url(url), url)
 
     def _test_regex(self, url, expected):
         m = Filmon.url_re.match(url)
-        self.assertIsNotNone(m)
+        self.assertIsNotNone(m, url)
         # expected must return [is_group, channel, vod_id]
         self.assertEqual(expected, list(m.groups()))
 
@@ -51,6 +52,10 @@ class TestPluginFilmon(unittest.TestCase):
         self._test_regex('https://www.filmon.com/tv/bbc-news',
                          [None, 'bbc-news', None])
 
+    def test_regex_live_stream_tv_with_channel_in_name(self):
+        self._test_regex('https://www.filmon.com/tv/channel-4',
+                         [None, 'channel-4', None])
+
     def test_regex_live_stream_tv_number(self):
         self._test_regex('https://www.filmon.tv/tv/55',
                          [None, '55', None])
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_powerapp.py b/tests/plugins/test_powerapp.py
index 043e4df9..f000b501 100644
--- a/tests/plugins/test_powerapp.py
+++ b/tests/plugins/test_powerapp.py
@@ -9,9 +9,10 @@ class TestPluginPowerApp(unittest.TestCase):
             'http://powerapp.com.tr/tv/powertv4k',
             'http://powerapp.com.tr/tv/powerturktv4k',
             'http://powerapp.com.tr/tv/powerEarthTV',
+            'http://www.powerapp.com.tr/tvs/powertv'
         ]
         for url in should_match:
-            self.assertTrue(PowerApp.can_handle_url(url))
+            self.assertTrue(PowerApp.can_handle_url(url), url)
 
     def test_can_handle_url_negative(self):
         should_not_match = [
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_cli_main.py b/tests/test_cli_main.py
index 2b58ea5f..a4c0d0d1 100644
--- a/tests/test_cli_main.py
+++ b/tests/test_cli_main.py
@@ -1,15 +1,19 @@
 import os.path
-import os.path
 import tempfile
 
 import streamlink_cli.main
-from streamlink_cli.main import resolve_stream_name, format_valid_streams, check_file_output
-from streamlink_cli.output import FileOutput
+from streamlink_cli.main import resolve_stream_name, format_valid_streams, check_file_output, create_output
+from streamlink_cli.output import FileOutput, PlayerOutput
 from streamlink.plugin.plugin import Plugin
 import unittest
 from tests.mock import Mock, patch
 
 
+class FakePlugin:
+    @classmethod
+    def stream_weight(cls, stream):
+        return Plugin.stream_weight(stream)
+
 class TestCLIMain(unittest.TestCase):
     def test_check_file_output(self):
         streamlink_cli.main.console = Mock()
@@ -89,10 +93,6 @@ class TestCLIMain(unittest.TestCase):
         self.assertEqual(resolve_stream_name(streams, "best-unfiltered"), "1080p")
 
     def test_format_valid_streams(self):
-        class FakePlugin:
-            @classmethod
-            def stream_weight(cls, stream):
-                return Plugin.stream_weight(stream)
         a = Mock()
         b = Mock()
         c = Mock()
@@ -128,3 +128,70 @@ class TestCLIMain(unittest.TestCase):
                 "1080p (best-unfiltered)"
             ])
         )
+
+    def test_create_output_no_file_output_options(self):
+        streamlink_cli.main.console = Mock()
+        streamlink_cli.main.args = args = Mock()
+        args.output = None
+        args.stdout = None
+        args.record = None
+        args.record_and_pipe = None
+        args.title = None
+        args.player = "mpv"
+        self.assertIsInstance(create_output(FakePlugin), PlayerOutput)
+
+    def test_create_output_file_output(self):
+        tmpfile = tempfile.NamedTemporaryFile()
+        try:
+            streamlink_cli.main.args = args = Mock()
+            streamlink_cli.main.console = Mock()
+            args.output = tmpfile.name
+            self.assertTrue(os.path.exists(tmpfile.name))
+            self.assertIsInstance(create_output(FakePlugin), FileOutput)
+        finally:
+            tmpfile.close()
+
+    def test_create_output_stdout(self):
+        streamlink_cli.main.console = Mock()
+        streamlink_cli.main.args = args = Mock()
+        args.output = None
+        args.stdout = True
+        self.assertIsInstance(create_output(FakePlugin), FileOutput)
+
+    def test_create_output_record_and_pipe(self):
+        tmpfile = tempfile.NamedTemporaryFile()
+        try:
+            streamlink_cli.main.console = Mock()
+            streamlink_cli.main.args = args = Mock()
+            args.output = None
+            args.stdout = None
+            args.record_and_pipe = tmpfile.name
+            self.assertIsInstance(create_output(FakePlugin), FileOutput)
+        finally:
+            tmpfile.close()
+
+    def test_create_output_record(self):
+        tmpfile = tempfile.NamedTemporaryFile()
+        try:
+            streamlink_cli.main.console = Mock()
+            streamlink_cli.main.args = args = Mock()
+            args.output = None
+            args.stdout = None
+            args.record = tmpfile.name
+            args.record_and_pipe = None
+            args.title = None
+            args.player = "mpv"
+            args.player_fifo = None
+            self.assertIsInstance(create_output(FakePlugin), PlayerOutput)
+        finally:
+            tmpfile.close()
+
+    def test_create_output_record_and_other_file_output(self):
+        streamlink_cli.main.console = console = Mock()
+        streamlink_cli.main.args = args = Mock()
+        console.exit = Mock()
+        args.output = None
+        args.stdout = True
+        args.record_and_pipe = True
+        create_output(FakePlugin)
+        console.exit.assert_called_with("Cannot use record options with other file output options.")
diff --git a/tests/test_cmdline_title.py b/tests/test_cmdline_title.py
index a05a1293..2c6a9fac 100644
--- a/tests/test_cmdline_title.py
+++ b/tests/test_cmdline_title.py
@@ -52,3 +52,30 @@ class TestCommandLineWithTitleWindows(CommandLineTestCase):
     def test_open_player_with_default_arg_vlc(self):
         self._test_args(["streamlink", "-p", "c:\\Program Files\\VideoLAN\\vlc.exe --argh", "http://test.se", "test"],
                         "c:\\Program Files\\VideoLAN\\vlc.exe --argh --input-title-format http://test.se -")
+
+    # PotPlayer
+    def test_open_player_with_title_pot(self):
+        self._test_args(["streamlink", "-p", "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\"", 
+                        "--title", "{title}", "http://test.se/stream", "hls", "--player-passthrough", "hls"],
+                        "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\" \"http://test.se/playlist.m3u8\\Test Title\"", 
+                        passthrough=True)
+
+    @unittest.skipIf(is_py3, "Encoding is different in Python 2")
+    def test_open_player_with_unicode_author_pot_py2(self):
+        self._test_args(["streamlink", "-p", "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\"", 
+                        "--title", "{author}", "http://test.se/stream", "hls", "--player-passthrough", "hls"],
+                        "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\" \"http://test.se/playlist.m3u8\\" + u"Tѥst Āuƭhǿr".encode(get_filesystem_encoding()) + "\"", 
+                        passthrough=True)
+
+    @unittest.skipIf(not is_py3, "Encoding is different in Python 2")
+    def test_open_player_with_unicode_author_pot_py3(self):
+        self._test_args(["streamlink", "-p", "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\"", 
+                        "--title", "{author}", "http://test.se/stream", "hls", "--player-passthrough", "hls"],
+                        u"\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\" \"http://test.se/playlist.m3u8\\Tѥst Āuƭhǿr\"", 
+                        passthrough=True)
+
+    def test_open_player_with_default_title_pot(self):
+        self._test_args(["streamlink", "-p", "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\"", 
+                        "http://test.se/stream", "hls", "--player-passthrough", "hls"],
+                        "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\" \"http://test.se/playlist.m3u8\\http://test.se/stream\"", 
+                        passthrough=True)
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")
diff --git a/tests/test_output.py b/tests/test_output.py
index e245ef4a..3a0974a9 100644
--- a/tests/test_output.py
+++ b/tests/test_output.py
@@ -14,12 +14,17 @@ class TestPlayerOutput(unittest.TestCase):
         self.assertEqual("mpv",
                          PlayerOutput.supported_player("mpv"))
 
+        self.assertEqual("potplayer",
+                         PlayerOutput.supported_player("potplayermini.exe"))
+
     @patch("streamlink_cli.output.os.path.basename", new=ntpath.basename)
     def test_supported_player_win32(self):
         self.assertEqual("mpv",
                          PlayerOutput.supported_player("C:\\MPV\\mpv.exe"))
         self.assertEqual("vlc",
                          PlayerOutput.supported_player("C:\\VLC\\vlc.exe"))
+        self.assertEqual("potplayer",
+                         PlayerOutput.supported_player("C:\\PotPlayer\\PotPlayerMini64.exe"))
 
     @patch("streamlink_cli.output.os.path.basename", new=posixpath.basename)
     def test_supported_player_posix(self):
@@ -34,6 +39,8 @@ class TestPlayerOutput(unittest.TestCase):
                          PlayerOutput.supported_player("C:\\MPV\\mpv.exe --argh"))
         self.assertEqual("vlc",
                          PlayerOutput.supported_player("C:\\VLC\\vlc.exe --argh"))
+        self.assertEqual("potplayer",
+                         PlayerOutput.supported_player("C:\\PotPlayer\\PotPlayerMini64.exe --argh"))
 
     @patch("streamlink_cli.output.os.path.basename", new=posixpath.basename)
     def test_supported_player_args_posix(self):
@@ -55,6 +62,8 @@ class TestPlayerOutput(unittest.TestCase):
                          PlayerOutput.supported_player("C:\\mpc\\mpc-hd.exe"))
         self.assertEqual(None,
                          PlayerOutput.supported_player("C:\\mplayer\\not-vlc.exe"))
+        self.assertEqual(None,
+                         PlayerOutput.supported_player("C:\\NotPlayer\\NotPlayerMini64.exe"))
 
     def test_open_player_with_title_mpv_escape_1(self):
         self.assertEqual(PlayerOutput._mpv_title_escape("no escape $$ codes $"),