Repository: streamlink/streamlink
Pull request #2286 (2019-02-01 05:34:27)

plugins.facebook: Support manifest strings and tahoe player urls
Addresses #2168, #2133.
Immediate playback for DASH streams with separate audio and video
streams will not be supported until proper SegmentBase handling is
implemented.

Depends on the `url_or_manifest` changes from #2285.

See also: #2239 

Immediate playback for DASH streams with separate audio and video streams is still not supported, this will require implementing support for DASH SegmentBase.


Patch:
diff --git a/.travis.yml b/.travis.yml
index a664f15f..0a83c747 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,6 +32,9 @@ before_install:
       pip install -r docs-requirements.txt;
       pip install doctr;
     fi
+  - if [[ $BUILD_INSTALLER == 'yes' ]]; then
+      pip install git+https://github.com/takluyver/pynsist.git@88f56f9e86af9c55522147a67f8b7806ac2ca55b;
+    fi
 
 install:
   - pip install -e .
@@ -63,7 +66,6 @@ deploy:
   - provider: releases
     api_key: "${RELEASES_API_KEY}"
     file: "${STREAMLINK_INSTALLER_DIST_DIR}/streamlink-${TRAVIS_TAG}.exe"
-    file_glob: true
     skip_cleanup: true
     on:
       tags: true
@@ -94,7 +96,7 @@ deploy:
       repo: streamlink/streamlink
   - provider: releases
     api_key: "${RELEASES_API_KEY}"
-    file: "${STREAMLINK_DIST_DIR}/streamlink-${TRAVIS_TAG}*"
+    file: "${STREAMLINK_DIST_DIR}/streamlink-${TRAVIS_TAG}.tar.gz{,.asc}"
     file_glob: true
     skip_cleanup: true
     on:
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 0b88f176..ae5d6c7e 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -5,5 +5,4 @@ codecov
 coverage
 mock
 requests-mock
-pynsist
 freezegun
diff --git a/docs/install.rst b/docs/install.rst
index bae970e4..a2a28a3a 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -7,59 +7,120 @@
 Installation
 ============
 
-Linux and BSD packages
-----------------------
+Windows
+-------
+
+.. rst-class:: table-custom-layout
+
+==================================== ===========================================
+Method                               Installing
+==================================== ===========================================
+Installers                           See the `Windows binaries`_ section below
+
+Portable                             See the `Windows portable version`_ section below
+
+`Chocolatey`_                        .. code-block:: bat
+
+                                        choco install streamlink
+
+                                     `Installing Chocolatey packages`_
+==================================== ===========================================
+
+.. _Chocolatey: https://chocolatey.org/packages/streamlink
+.. _Installing Chocolatey packages: https://chocolatey.org
+
+
+macOS
+-----
+
+.. rst-class:: table-custom-layout
+
+==================================== ===========================================
+Method                               Installing
+==================================== ===========================================
+Easy install                         .. code-block:: bash
+
+                                        sudo easy_install -U streamlink
+
+`Homebrew`_                          .. code-block:: bash
+
+                                        brew install streamlink
+
+                                     `Installing Homebrew packages`_
+==================================== ===========================================
+
+.. _Homebrew: https://github.com/Homebrew/homebrew-core/blob/master/Formula/streamlink.rb
+.. _Installing Homebrew packages: https://brew.sh
+
+
+Linux and BSD
+-------------
 
 .. rst-class:: table-custom-layout
 
 ==================================== ===========================================
 Distribution                         Installing
 ==================================== ===========================================
-`Arch Linux`_                        .. code-block:: console
+`Arch Linux`_                        .. code-block:: bash
 
-                                        # pacman -S streamlink
+                                        sudo pacman -S streamlink
 
-`Arch Linux (aur, git)`_             .. code-block:: console
+`Arch Linux (aur, git)`_             .. code-block:: bash
 
-                                        # pacaur -S streamlink-git
+                                        git clone https://aur.archlinux.org/streamlink-git.git
+                                        cd streamlink-git
+                                        makepkg -si
 
                                      `Installing AUR packages`_
-`Debian (sid, testing)`_             .. code-block:: console
 
-                                        # apt update
-                                        # apt install streamlink
-`Debian (stable)`_                   .. code-block:: console
+`Debian (sid, testing)`_             .. code-block:: bash
+
+                                        sudo apt update
+                                        sudo apt install streamlink
 
-                                        # wget -qO- "https://bintray.com/user/downloadSubjectPublicKey?username=amurzeau" | apt-key add -
-                                        # echo "deb https://dl.bintray.com/amurzeau/streamlink-debian stretch-backports main" | tee "/etc/apt/sources.list.d/streamlink.list"
-                                        # apt update
-                                        # apt install streamlink
-`Fedora`_                            .. code-block:: console
+`Debian (stable)`_                   .. code-block:: bash
 
-                                        # dnf install streamlink
-`Gentoo Linux`_                      .. code-block:: console
+                                        wget -qO- "https://bintray.com/user/downloadSubjectPublicKey?username=amurzeau" | sudo apt-key add -
+                                        echo "deb https://dl.bintray.com/amurzeau/streamlink-debian stretch-backports main" | sudo tee "/etc/apt/sources.list.d/streamlink.list"
+                                        sudo apt update
+                                        sudo apt install streamlink
 
-                                        # emerge net-misc/streamlink
-`NetBSD (pkgsrc)`_                   .. code-block:: console
+`Fedora`_                            .. code-block:: bash
 
-                                        $ cd /usr/pkgsrc/multimedia/streamlink
-                                        # make install clean
-`NixOS`_                             .. code-block:: console
+                                        sudo dnf install streamlink
 
-                                        $ nix-env -iA nixos.streamlink
+`Gentoo Linux`_                      .. code-block:: bash
+
+                                        sudo emerge net-misc/streamlink
+
+`NetBSD (pkgsrc)`_                   .. code-block:: bash
+
+                                        cd /usr/pkgsrc/multimedia/streamlink
+                                        sudo make install clean
+
+`NixOS`_                             .. code-block:: bash
+
+                                        nix-env -iA nixos.streamlink
 
                                      `NixOS channel`_
-`Solus`_                             .. code-block:: console
 
-                                        $ sudo eopkg install streamlink
-`Ubuntu`_                            .. code-block:: console
+`OpenBSD`_                           .. code-block:: bash
+
+                                        doas pkg_add streamlink
 
-                                        $ sudo add-apt-repository ppa:nilarimogard/webupd8
-                                        $ sudo apt update
-                                        $ sudo apt install streamlink
-`Void`_                              .. code-block:: console
+`Solus`_                             .. code-block:: bash
 
-                                        # xbps-install streamlink
+                                        sudo eopkg install streamlink
+
+`Ubuntu`_                            .. code-block:: bash
+
+                                        sudo add-apt-repository ppa:nilarimogard/webupd8
+                                        sudo apt update
+                                        sudo apt install streamlink
+
+`Void`_                              .. code-block:: bash
+
+                                        sudo xbps-install streamlink
 ==================================== ===========================================
 
 .. _Arch Linux: https://www.archlinux.org/packages/community/any/streamlink/
@@ -70,6 +131,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
@@ -78,38 +140,6 @@ Distribution                         Installing
 .. _NixOS channel: https://nixos.org/nixos/packages.html#streamlink
 
 
-Other platforms
----------------
-
-.. rst-class:: table-custom-layout
-
-==================================== ===========================================
-Platform                             Installing
-==================================== ===========================================
-Mac OS X                             .. code-block:: console
-
-                                        # easy_install -U streamlink
-`Homebrew`_                          .. code-block:: console
-
-                                        # brew install streamlink
-
-                                     `Installing Homebrew packages`_
-Microsoft Windows                    See `Windows binaries`_ and `Windows portable version`_.
-
-`Chocolatey`_                        .. code-block:: console
-
-                                        C:\> choco install streamlink
-
-                                     `Installing Chocolatey packages`_
-==================================== ===========================================
-
-.. _Homebrew: https://github.com/Homebrew/homebrew-core/blob/master/Formula/streamlink.rb
-.. _Chocolatey: https://chocolatey.org/packages/streamlink
-
-.. _Installing Homebrew packages: https://brew.sh
-.. _Installing Chocolatey packages: https://chocolatey.org
-
-
 Package maintainers
 -------------------
 
@@ -126,6 +156,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>
@@ -138,51 +169,106 @@ Windows port. version                RosadinTV <RosadinTV at outlook.com> |br|
 Source code
 -----------
 
-If a package is not available for your platform (or it's out of date) you
-can install Streamlink via source.
+If a package is not available on your platform (or if it's out of date), Streamlink can be installed from source.
 
-There are a few different methods to do this,
-`pip <http://pip.readthedocs.org/en/latest/installing.html>`_ the Python package
-manager, or by checking out the latest code with
-`Git <http://git-scm.com/downloads>`_. Using :command:`easy_install` is no longer recommended.
+This can be done in a couple of different ways, for example by using `pip`_, the Python package manager,
+or by checking out the source code with `git`_ and installing it via setuptools. |br|
+Using :command:`easy_install` is no longer recommended.
 
 .. note::
 
-    For some Linux distributions the Python headers package needs to be installed before installing streamlink
-    (``python-devel`` in RedHat, Fedora, etc.).
+    On some Linux distributions, the Python headers package needs to be installed before installing Streamlink
+    (``python-devel`` on RedHat, Fedora, etc.).
 
-    Ensure that you are using an up-to-date version of :command:`pip`, at least version **6** is recommended.
+    Ensure that you are using an up-to-date version of `pip`_. At least version **6** is required.
 
+.. note::
+
+    On Linux, when not using a virtual environment, it is recommended to install custom python packages like this
+    only for the current user (see the ``--user`` parameter below), since system-wide packages can cause conflicts with
+    the system's regular package manager.
 
-The commands listed here will also upgrade any existing version of Streamlink.
+    Those user-packages will be installed into ``~/.local`` instead of ``/usr`` and entry-scripts for
+    running the programs can be found in ``~/.local/bin``, eg. ``~/.local/bin/streamlink``.
+
+    In order for the command line shell to be able to find these executables, the user's ``PATH`` environment variable
+    needs to be extended. This can be done by adding ``export PATH="${HOME}/.local/bin:${PATH}"``
+    to ``~/.profile`` or ``~/.bashrc``.
 
 .. rst-class:: table-custom-layout
 
 ==================================== ===========================================
 Version                              Installing
 ==================================== ===========================================
-`Latest release (pip)`_              .. code-block:: console
+`Latest release (pip)`_              .. code-block:: bash
+
+                                        # Current user
+                                        pip install --upgrade --user streamlink
 
-                                        # pip install -U streamlink
-`Latest release (easy_install)`_     .. code-block:: console
+                                        # System wide
+                                        sudo pip install --upgrade streamlink
 
-                                        # easy_install -U streamlink
-`Development version (pip)`_         .. code-block:: console
+`Development version (pip)`_         .. code-block:: bash
 
-                                        # pip install -U git+https://github.com/streamlink/streamlink.git
+                                        # Current user
+                                        pip install --upgrade --user git+https://github.com/streamlink/streamlink.git
 
-`Development version (git)`_         .. code-block:: console
+                                        # System wide
+                                        sudo pip install --upgrade git+https://github.com/streamlink/streamlink.git
 
-                                        $ git clone git://github.com/streamlink/streamlink.git
-                                        $ cd streamlink
-                                        # python setup.py install
+`Development version (git)`_         .. code-block:: bash
+
+                                        # Current user
+                                        git clone https://github.com/streamlink/streamlink.git
+                                        cd streamlink
+                                        python setup.py install --user
+
+                                        # System wide
+                                        git clone https://github.com/streamlink/streamlink.git
+                                        cd streamlink
+                                        sudo python setup.py install
 ==================================== ===========================================
 
+.. _pip: https://pip.readthedocs.org/en/latest/installing.html
+.. _git: https://git-scm.com/
 .. _Latest release (pip): https://pypi.python.org/pypi/streamlink
 .. _Latest release (easy_install): https://pypi.python.org/pypi/streamlink
 .. _Development version (pip): https://github.com/streamlink/streamlink
 .. _Development version (git): https://github.com/streamlink/streamlink
 
+Virtual environment
+^^^^^^^^^^^^^^^^^^^
+
+Another method of installing Streamlink in a non-system-wide way is
+using `virtualenv`_, which creates a user owned Python environment instead.
+
+.. code-block:: bash
+
+    # Create a new environment
+    virtualenv ~/myenv
+
+    # Activate the environment
+    source ~/myenv/bin/activate
+
+    # Install Streamlink in the environment
+    pip install --upgrade streamlink
+
+    # Use Streamlink in the environment
+    streamlink ...
+
+    # Deactivate the environment
+    deactivate
+
+    # Use Streamlink without activating the environment
+    ~/myenv/bin/streamlink ...
+
+.. note::
+
+    This may also be required on some macOS versions that seem to have weird
+    permission issues.
+
+.. _virtualenv: https://virtualenv.readthedocs.io/en/latest/
+
 Dependencies
 ^^^^^^^^^^^^
 
@@ -249,42 +335,6 @@ With these two environment variables it is possible to use `pycrypto`_ instead o
 .. _shutil_which: https://pypi.org/project/backports.shutil_which/
 
 
-Installing without root permissions
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-If you do not wish to install Streamlink globally on your system it's
-recommended to use `virtualenv`_ to create a user owned Python environment
-instead.
-
-.. code-block:: console
-
-    Creating an environment
-    $ virtualenv ~/myenv
-
-    Activating the environment
-    $ source ~/myenv/bin/activate
-
-    Installing streamlink into the environment
-    (myenv)$ pip install streamlink
-
-    Using streamlink in the environment
-    (myenv)$ streamlink ...
-
-    Deactivating the environment
-    (myenv)$ deactivate
-
-    Using streamlink without activating the environment
-    $ ~/myenv/bin/streamlink ...
-
-.. note::
-
-    This may also be required on some OS X versions that seems to have weird
-    permission issues.
-
-
-.. _virtualenv: https://virtualenv.readthedocs.io/en/latest/
-
-
 Windows binaries
 ----------------
 
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 6dd3bdde..538eeb86 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -40,6 +40,7 @@ cdnbg                   - tv.bnt.bg          Yes   No    Streams may be geo-rest
                         - bgonair.bg
                         - bitelevision.com
                         - bloombergtv.bg
+                        - bstv.bg
                         - inlife.bg
                         - kanal3.bg
                         - mmtvmusic.com
@@ -81,7 +82,7 @@ eltrecetv               eltrecetv.com.ar     Yes   Yes   Streams may be geo-rest
 eurocom                 eurocom.bg           Yes   No
 euronews                euronews.com         Yes   No
 europaplus              europaplus.ru        Yes   --
-facebook                facebook.com         Yes   No    Only 360p HLS streams.
+facebook                facebook.com         Yes   Yes
 filmon                  filmon.com           Yes   Yes   Only SD quality streams.
 foxtr                   fox.com.tr           Yes   No
 funimationnow           - funimation.com     --    Yes   :ref:`Requires session cookies <cli-funimationnow>`
diff --git a/script/github_releases.py b/script/github_releases.py
index 09c83246..edf1601d 100755
--- a/script/github_releases.py
+++ b/script/github_releases.py
@@ -17,20 +17,7 @@ TEMPLATE = """
 
 ## Installation
 
-### Installing with Pip:
-
-```sh
-sudo pip install streamlink
-```
-
-### Manual installation:
-
-```sh
-curl -L https://github.com/streamlink/streamlink/releases/download/{version}/streamlink-{version}.tar.gz -O streamlink.tar.gz
-tar xvf streamlink.tar.gz
-cd streamlink
-sudo python setup.py install
-```
+See the detailed [installation instructions](https://streamlink.github.io/install.html) on Streamlink's website.
 
 ## Supporting Streamlink
 
diff --git a/script/makeinstaller.sh b/script/makeinstaller.sh
index 1e84dc88..e4739581 100755
--- a/script/makeinstaller.sh
+++ b/script/makeinstaller.sh
@@ -106,6 +106,10 @@ files=../win32/LICENSE.txt > \$INSTDIR
 [Command streamlink]
 entry_point=streamlink_cli.main:main
 
+[Command streamlinkw]
+entry_point=streamlink_cli.main:main
+console=false
+
 [Build]
 directory=nsis
 nsi_template=installer_tmpl.nsi
diff --git a/script/sdistsign.sh b/script/sdistsign.sh
index 95c9997e..6bfba2ee 100755
--- a/script/sdistsign.sh
+++ b/script/sdistsign.sh
@@ -1,4 +1,5 @@
 #!/usr/bin/env bash
+shopt -s nullglob
 set -e
 
 KEY_ID=2E390FA0
@@ -8,6 +9,8 @@ version=$(python setup.py --version)
 dist_dir=${STREAMLINK_DIST_DIR:-dist}
 temp_keyring=$(mktemp -d) && trap "rm -rf ${temp_keyring}" EXIT || exit 255
 
+wheel_platforms_windows=("win32" "win-amd64" "cygwin")
+
 if [[ -n "${TRAVIS}" ]]; then
       openssl aes-256-cbc -K ${encrypted_eeb8b970d3a3_key} -iv ${encrypted_eeb8b970d3a3_iv} -in signing.key.enc -out "${SDIST_KEY_FILE}" -d
 fi
@@ -15,14 +18,23 @@ fi
 echo "build: Installing twine and wheel" >&2
 pip -q install -U setuptools twine wheel
 
-echo "build: Building streamlink sdist and wheel" >&2
-python setup.py -q sdist bdist_wheel --dist-dir "${dist_dir}"
+echo "build: Building Streamlink sdist" >&2
+python setup.py -q sdist --dist-dir "${dist_dir}"
+
+echo "build: Building Streamlink wheel (universal)" >&2
+python setup.py -q bdist_wheel --dist-dir "${dist_dir}"
+
+for platform in "${wheel_platforms_windows[@]}"; do
+    echo "build: Building Streamlink wheel (${platform})" >&2
+    python setup.py -q bdist_wheel --plat-name "${platform}" --dist-dir "${dist_dir}"
+done
 
 if [ -f "${KEY_FILE}" ]; then
     echo "build: Signing sdist and wheel files" >&2
     gpg --homedir "${temp_keyring}" --import "${KEY_FILE}" 2>&1 > /dev/null
-    gpg --homedir "${temp_keyring}" --trust-model always --default-key "${KEY_ID}" --detach-sign --armor "${dist_dir}/streamlink-${version}.tar.gz"
-    gpg --homedir "${temp_keyring}" --trust-model always --default-key "${KEY_ID}" --detach-sign --armor "${dist_dir}/streamlink-${version}-py2.py3-none-any.whl"
+    for file in "${dist_dir}"/streamlink-"${version}"{.tar.gz,-*.whl}; do
+        gpg --homedir "${temp_keyring}" --trust-model always --default-key "${KEY_ID}" --detach-sign --armor "${file}"
+    done
 else
     echo "warning: no signing key, files not signed" >&2
 fi
@@ -30,9 +42,5 @@ fi
 if [[ "${DEPLOY_PYPI}" == "yes" ]]; then
     echo "build: Uploading sdist and wheel to PyPI" >&2
     twine upload --username "${PYPI_USER}" --password "${PYPI_PASSWORD}" \
-        "${dist_dir}/streamlink-${version}.tar.gz" \
-        "${dist_dir}/streamlink-${version}.tar.gz.asc"
-    twine upload --username "${PYPI_USER}" --password "${PYPI_PASSWORD}" \
-        "${dist_dir}/streamlink-${version}-py2.py3-none-any.whl" \
-        "${dist_dir}/streamlink-${version}-py2.py3-none-any.whl.asc"
-fi
\ No newline at end of file
+        "${dist_dir}"/streamlink-"${version}"{.tar.gz,-*.whl}{,.asc}
+fi
diff --git a/setup.py b/setup.py
index 2ec570ab..cde8848c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,7 @@
 #!/usr/bin/env python
 import codecs
-from os import environ
-from os import path
-from sys import path as sys_path
+from os import environ, path
+from sys import argv, path as sys_path
 
 from setuptools import setup, find_packages
 
@@ -53,6 +52,27 @@ sys_path.insert(0, srcdir)
 with codecs.open(path.join(this_directory, "README.md"), 'r', "utf8") as f:
     long_description = f.read()
 
+
+def is_wheel_for_windows():
+    if "bdist_wheel" in argv:
+        names = ["win32", "win-amd64", "cygwin"]
+        length = len(argv)
+        for pos in range(argv.index("bdist_wheel") + 1, length):
+            if argv[pos] == "--plat-name" and pos + 1 < length:
+                return argv[pos + 1] in names
+            elif argv[pos][:12] == "--plat-name=":
+                return argv[pos][12:] in names
+    return False
+
+
+entry_points = {
+    "console_scripts": ["streamlink=streamlink_cli.main:main"]
+}
+
+if is_wheel_for_windows():
+    entry_points["gui_scripts"] = ["streamlinkw=streamlink_cli.main:main"]
+
+
 setup(name="streamlink",
       version=versioneer.get_version(),
       cmdclass=versioneer.get_cmdclass(),
@@ -74,9 +94,7 @@ setup(name="streamlink",
       license="Simplified BSD",
       packages=find_packages("src"),
       package_dir={"": "src"},
-      entry_points={
-          "console_scripts": ["streamlink=streamlink_cli.main:main"]
-      },
+      entry_points=entry_points,
       install_requires=deps,
       test_suite="tests",
       python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4",
diff --git a/src/streamlink/compat.py b/src/streamlink/compat.py
index 0eedef5d..cb19ab77 100644
--- a/src/streamlink/compat.py
+++ b/src/streamlink/compat.py
@@ -34,12 +34,12 @@ elif is_py3:
 
 try:
     from urllib.parse import (
-        urlparse, urlunparse, urljoin, quote, unquote, parse_qsl, urlencode, urlsplit, urlunsplit
+        urlparse, urlunparse, urljoin, quote, unquote, unquote_plus, parse_qsl, urlencode, urlsplit, urlunsplit
     )
     import queue
 except ImportError:
     from urlparse import urlparse, urlunparse, urljoin, parse_qsl, urlsplit, urlunsplit
-    from urllib import quote, unquote, urlencode
+    from urllib import quote, unquote, unquote_plus, urlencode
     import Queue as queue
 
 try:
@@ -60,5 +60,5 @@ getargspec = getattr(inspect, "getfullargspec", inspect.getargspec)
 
 __all__ = ["is_py2", "is_py3", "is_py33", "is_win32", "str", "bytes",
            "urlparse", "urlunparse", "urljoin", "parse_qsl", "quote",
-           "unquote", "queue", "range", "urlencode", "devnull", "which",
+           "unquote", "unquote_plus", "queue", "range", "urlencode", "devnull", "which",
            "izip", "urlsplit", "urlunsplit", "getargspec", "html_unescape"]
diff --git a/src/streamlink/plugins/atresplayer.py b/src/streamlink/plugins/atresplayer.py
index 27961a8b..ee43e863 100644
--- a/src/streamlink/plugins/atresplayer.py
+++ b/src/streamlink/plugins/atresplayer.py
@@ -5,7 +5,7 @@ import re
 from functools import partial
 
 from streamlink.plugin import Plugin
-from streamlink.plugin.api import validate, StreamMapper
+from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream, DASHStream
 from streamlink.utils import parse_json, update_scheme, search_dict
 
@@ -22,7 +22,16 @@ class AtresPlayer(Plugin):
             validate.all(
                 validate.get(1),
                 validate.transform(parse_json),
-                validate.transform(partial(search_dict, key="urlVideo"))
+                validate.transform(partial(search_dict, key="href")),
+            )
+        )
+    )
+    player_api_schema = validate.Schema(
+        validate.any(
+            None,
+            validate.all(
+                validate.transform(parse_json),
+                validate.transform(partial(search_dict, key="urlVideo")),
             )
         )
     )
@@ -35,7 +44,6 @@ class AtresPlayer(Plugin):
             })
         ]}, validate.get("sources"))
 
-
     @classmethod
     def can_handle_url(cls, url):
         return cls.url_re.match(url) is not None
@@ -46,14 +54,21 @@ class AtresPlayer(Plugin):
 
     def _get_streams(self):
         api_urls = self.session.http.get(self.url, schema=self.channel_id_schema)
-        for api_url in api_urls:
-            log.debug("API URL: {0}".format(api_url))
+        _api_url = list(api_urls)[0]
+        log.debug("API URL: {0}".format(_api_url))
+        player_api_url = self.session.http.get(_api_url, schema=self.player_api_schema)
+        for api_url in player_api_url:
+            log.debug("Player API URL: {0}".format(api_url))
             for source in self.session.http.get(api_url, schema=self.stream_schema):
                 log.debug("Stream source: {0} ({1})".format(source['src'], source.get("type", "n/a")))
 
                 if "type" not in source or source["type"] == "application/vnd.apple.mpegurl":
-                    for s in HLSStream.parse_variant_playlist(self.session, source["src"]).items():
-                        yield s
+                    streams = HLSStream.parse_variant_playlist(self.session, source["src"])
+                    if not streams:
+                        yield "live", HLSStream(self.session, source["src"])
+                    else:
+                        for s in streams.items():
+                            yield s
                 elif source["type"] == "application/dash+xml":
                     for s in DASHStream.parse_manifest(self.session, source["src"]).items():
                         yield s
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/cdnbg.py b/src/streamlink/plugins/cdnbg.py
index dfb5591d..79b0172b 100644
--- a/src/streamlink/plugins/cdnbg.py
+++ b/src/streamlink/plugins/cdnbg.py
@@ -22,6 +22,7 @@ class CDNBG(Plugin):
             inlife\.bg|
             mmtvmusic\.com/live|
             mu-vi\.tv/LiveStreams/pages/Live\.aspx|
+            videochanel\.bstv\.bg|
             bloombergtv.bg/video
         )/?
     """, re.VERBOSE)
diff --git a/src/streamlink/plugins/crunchyroll.py b/src/streamlink/plugins/crunchyroll.py
index b3498c27..a1d649da 100644
--- a/src/streamlink/plugins/crunchyroll.py
+++ b/src/streamlink/plugins/crunchyroll.py
@@ -38,6 +38,9 @@ _url_re = re.compile(r"""
     (?:
         com|de|es|fr|co.jp
     )
+    (?:
+        /(en-gb|es|es-es|pt-pt|pt-br|fr|de|ar|it|ru)
+    )?
     (?:/[^/&?]+)?
     /[^/&?]+-(?P<media_id>\d+)
 """, re.VERBOSE)
diff --git a/src/streamlink/plugins/facebook.py b/src/streamlink/plugins/facebook.py
index 537b4a5a..426649aa 100644
--- a/src/streamlink/plugins/facebook.py
+++ b/src/streamlink/plugins/facebook.py
@@ -1,53 +1,110 @@
+import logging
 import re
 
+from streamlink.compat import bytes, is_py3, unquote_plus, urlencode
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import useragents
 from streamlink.stream import DASHStream, HTTPStream
 from streamlink.utils import parse_json
 
+log = logging.getLogger(__name__)
+
 
 class Facebook(Plugin):
-    _url_re = re.compile(r"https?://(?:www\.)?facebook\.com/[^/]+/(posts|videos)")
+    _url_re = re.compile(r'''(?x)https?://(?:www\.)?facebook(?:\.com|corewwwi.onion)
+        /[^/]+/(?:posts|videos)/(?P<video_id>[0-9]+)''')
     _src_re = re.compile(r'''(sd|hd)_src["']?\s*:\s*(?P<quote>["'])(?P<url>.+?)(?P=quote)''')
+    _dash_manifest_re = re.compile(r'''dash_manifest["']?\s*:\s*["'](?P<manifest>.+?)["'],''')
     _playlist_re = re.compile(r'''video:\[({url:".+?}\])''')
     _plurl_re = re.compile(r'''url:"(.*?)"''')
+    _pc_re = re.compile(r'''pkg_cohort["']\s*:\s*["'](.+?)["']''')
+    _rev_re = re.compile(r'''client_revision["']\s*:\s*(\d+),''')
+    _dtsg_re = re.compile(r'''DTSGInitialData["'],\s*\[\],\s*{\s*["']token["']\s*:\s*["'](.+?)["']''')
+    _DEFAULT_PC = "PHASED:DEFAULT"
+    _DEFAULT_REV = 4681796
+    _TAHOE_URL = "https://www.facebook.com/video/tahoe/async/{0}/?chain=true&isvideo=true&payloadtype=primary"
 
     @classmethod
     def can_handle_url(cls, url):
-        return cls._url_re.match(url)
-
-    def _get_streams(self):
-        res = self.session.http.get(self.url, headers={"User-Agent": useragents.CHROME})
-
-        streams = {}
-        vod_urls = set([])
+        return cls._url_re.match(url) is not None
 
+    def _parse_streams(self, res):
         for match in self._src_re.finditer(res.text):
             stream_url = match.group("url")
             if "\\/" in stream_url:
                 # if the URL is json encoded, decode it
                 stream_url = parse_json("\"{}\"".format(stream_url))
             if ".mpd" in stream_url:
-                streams.update(DASHStream.parse_manifest(self.session, stream_url))
+                for s in DASHStream.parse_manifest(self.session, stream_url).items():
+                    yield s
             elif ".mp4" in stream_url:
-                streams[match.group(1)] = HTTPStream(self.session, stream_url)
-                vod_urls.add(stream_url)
+                yield match.group(1), HTTPStream(self.session, stream_url)
             else:
-                self.logger.debug("Non-dash/mp4 stream: {0}".format(stream_url))
+                log.debug("Non-dash/mp4 stream: {0}".format(stream_url))
 
-        if streams:
-            return streams
+        match = self._dash_manifest_re.search(res.text)
+        if match:
+            # facebook replaces "<" characters with the substring "\\x3C"
+            manifest = match.group("manifest").replace("\\/", "/")
+            if is_py3:
+                manifest = bytes(unquote_plus(manifest), "utf-8").decode("unicode_escape")
+            else:
+                manifest = unquote_plus(manifest).decode("string_escape")
+            # Ignore unsupported manifests until DASH SegmentBase support is implemented
+            if "SegmentBase" in manifest:
+                log.error("Skipped DASH manifest with SegmentBase streams")
+            else:
+                for s in DASHStream.parse_manifest(self.session, manifest).items():
+                    yield s
+
+    def _get_streams(self):
+        self.session.http.headers.update({'User-Agent': useragents.CHROME})
+        done = False
+        res = self.session.http.get(self.url)
+        for s in self._parse_streams(res):
+            done = True
+            yield s
+        if done:
+            return
 
         # fallback on to playlist
-        self.logger.debug("Falling back to playlist regex")
+        log.debug("Falling back to playlist regex")
         match = self._playlist_re.search(res.text)
         playlist = match and match.group(1)
         if playlist:
-            for url in dict(url.group(1) for url in self._plurl_re.finditer(playlist)):
-                if url not in vod_urls:
-                    streams["sd"] = HTTPStream(self.session, url)
+            match = self._plurl_re.search(playlist)
+            if match:
+                url = match.group(1)
+                yield "sd", HTTPStream(self.session, url)
+                return
+
+        # fallback to tahoe player url
+        log.debug("Falling back to tahoe player")
+        video_id = self._url_re.match(self.url).group("video_id")
+        url = self._TAHOE_URL.format(video_id)
+        data = {
+            "__a": 1,
+            "__pc": self._DEFAULT_PC,
+            "__rev": self._DEFAULT_REV,
+            "fb_dtsg": "",
+        }
+        match = self._pc_re.search(res.text)
+        if match:
+            data["__pc"] = match.group(1)
+        match = self._rev_re.search(res.text)
+        if match:
+            data["__rev"] = match.group(1)
+        match = self._dtsg_re.search(res.text)
+        if match:
+            data["fb_dtsg"] = match.group(1)
+        res = self.session.http.post(
+            url,
+            headers={"Content-Type": "application/x-www-form-urlencoded"},
+            data=urlencode(data).encode("ascii")
+        )
 
-        return streams
+        for s in self._parse_streams(res):
+            yield s
 
 
 __plugin__ = Facebook
diff --git a/src/streamlink/plugins/mitele.py b/src/streamlink/plugins/mitele.py
index 5be5fa10..8e0d62f1 100644
--- a/src/streamlink/plugins/mitele.py
+++ b/src/streamlink/plugins/mitele.py
@@ -1,39 +1,26 @@
-from __future__ import print_function
-
+import logging
 import re
 
-from streamlink import NoStreamsError
-from streamlink.compat import urljoin
 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 parse_json
 
+log = logging.getLogger(__name__)
 
-class Mitele(Plugin):
-    _url_re = re.compile(r"https?://(?:www\.)?mitele\.es/directo/(?P<channel>\w+)")
-    _suffix_re = re.compile(r"""acl=(?P<path>[^"'*]+)""")
-
-    # For more informations see:
-    # https://github.com/streamlink/streamlink/issues/1337
-    livehlsdai = "https://livehlsdai-i.akamaihd.net"
-    mdslivehls = "https://mdslivehls-i.akamaihd.net"
 
-    _channel_domains = {
-        "bemad": mdslivehls,
-        "boing": mdslivehls,
-        "cuatro": livehlsdai,
-        "divinity": mdslivehls,
-        "energy": mdslivehls,
-        "fdf": mdslivehls,
-        "telecinco": livehlsdai,
-    }
+class Mitele(Plugin):
+    _url_re = re.compile(r"https?://(?:www\.)?mitele\.es/directo/(?P<channel>[\w-]+)")
 
     pdata_url = "https://indalo.mediaset.es/mmc-player/api/mmc/v1/{channel}/live/html5.json"
     gate_url = "https://gatekeeper.mediaset.es"
 
-    pdata_schema = validate.Schema(
+    error_schema = validate.Schema({
+        "code": validate.any(validate.text, int),
+        "message": validate.text,
+    })
+    pdata_schema = validate.Schema(validate.transform(parse_json), validate.any(
         validate.all(
             {
                 "locations": [{
@@ -43,81 +30,56 @@ class Mitele(Plugin):
             },
             validate.get("locations"),
             validate.get(0),
+        ),
+        error_schema,
+    ))
+    gate_schema = validate.Schema(
+        validate.transform(parse_json),
+        validate.any(
+            {
+                "mimeType": validate.text,
+                "stream": validate.url(),
+            },
+            error_schema,
         )
     )
 
     def __init__(self, url):
         super(Mitele, self).__init__(url)
-        self.headers = {
+        self.session.http.headers.update({
             "User-Agent": useragents.FIREFOX,
             "Referer": self.url
-        }
+        })
 
     @classmethod
     def can_handle_url(cls, url):
-        m = cls._url_re.match(url)
-        return m and m.group("channel") in cls._channel_domains.keys()
-
-    def get_pdata(self, channel):
-        """
-        Get the params for the post request
-        :param channel: channel name
-        :return: "gcp" and "ogn"
-        """
-        res = self.session.http.get(self.pdata_url.format(channel=channel))
-        return parse_json(res.text, schema=self.pdata_schema)
-
-    def create_hls_url(self, suffix):
-        """
-        creates a valid hls_url
-        :param suffix: url session params
-        :return: hls_url
-        """
-        m = self._suffix_re.search(suffix)
-        if not m:
-            return
-        self.logger.debug("create hls_url from suffix")
-
-        channel = self._url_re.match(self.url).group("channel")
-        domain = self._channel_domains[channel]
-        path = m.group("path")
-        path_suffix = "master.m3u8?{suffix}".format(suffix=suffix)
-
-        hls_url = urljoin(domain, path)
-        hls_url = urljoin(hls_url, path_suffix)
-        return hls_url
-
-    def get_stream_url(self, data):
-        """
-        Get the hls_url from the post request
-        :param data: dict with "gcp" and "ogn"
-        :return: hls_url
-        """
-        try:
-            res = self.session.http.post(self.gate_url, headers=self.headers, data=data)
-        except Exception as e:
-            if "403" in str(e):
-                self.logger.error("This Video is Not Available in Your Country.")
-            raise NoStreamsError(self.url)
-
-        r_data = parse_json(res.text)
-        hls_url = r_data.get("stream")
-        suffix = r_data.get("suffix")
-
-        if hls_url is None and suffix:
-            hls_url = self.create_hls_url(suffix)
-        return hls_url
+        return cls._url_re.match(url) is not None
 
     def _get_streams(self):
         channel = self._url_re.match(self.url).group("channel")
 
-        pdata = self.get_pdata(channel)
-        hls_url = self.get_stream_url(pdata)
+        pdata = self.session.http.get(self.pdata_url.format(channel=channel),
+                                      acceptable_status=(200, 403, 404),
+                                      schema=self.pdata_schema)
+        log.trace("{0!r}".format(pdata))
+        if pdata.get("code"):
+            log.error("{0} - {1}".format(pdata["code"], pdata["message"]))
+            return
+
+        gdata = self.session.http.post(self.gate_url,
+                                       acceptable_status=(200, 403, 404),
+                                       data=pdata,
+                                       schema=self.gate_schema)
+        log.trace("{0!r}".format(gdata))
+        if gdata.get("code"):
+            log.error("{0} - {1}".format(gdata["code"], gdata["message"]))
+            return
 
-        if hls_url:
-            self.logger.debug("HLS URL: {0}".format(hls_url))
-            for s in HLSStream.parse_variant_playlist(self.session, hls_url, headers=self.headers, name_fmt="{pixels}_{bitrate}").items():
-                yield s
+        log.debug("Stream: {0} ({1})".format(gdata["stream"], gdata.get("mimeType", "n/a")))
+        for s in HLSStream.parse_variant_playlist(self.session,
+                                                  gdata["stream"],
+                                                  name_fmt="{pixels}_{bitrate}").items():
+            yield s
 
 
 __plugin__ = Mitele
diff --git a/src/streamlink/plugins/pluzz.py b/src/streamlink/plugins/pluzz.py
index 7ba24028..63edf383 100644
--- a/src/streamlink/plugins/pluzz.py
+++ b/src/streamlink/plugins/pluzz.py
@@ -1,3 +1,4 @@
+import logging
 import re
 import sys
 import time
@@ -7,6 +8,8 @@ from streamlink.plugin.api import validate
 from streamlink.stream import DASHStream, HDSStream, HLSStream, HTTPStream
 from streamlink.stream.ffmpegmux import MuxedStream
 
+log = logging.getLogger(__name__)
+
 
 class Pluzz(Plugin):
     GEO_URL = 'http://geo.francetv.fr/ws/edgescape.json'
@@ -20,7 +23,7 @@ class Pluzz(Plugin):
             www\.(ludo|zouzous)\.fr/heros/[\w-]+ |
             (.+\.)?francetvinfo\.fr)
     ''', re.VERBOSE)
-    _pluzz_video_id_re = re.compile(r'data-main-video="(?P<video_id>.+?)"')
+    _pluzz_video_id_re = re.compile(r'''(?P<q>["']*)videoId(?P=q):\s*["'](?P<video_id>[^"']+)["']''')
     _jeunesse_video_id_re = re.compile(r'playlist: \[{.*?,"identity":"(?P<video_id>.+?)@(?P<catalogue>Ludo|Zouzous)"')
     _sport_video_id_re = re.compile(r'data-video="(?P<video_id>.+?)"')
     _embed_video_id_re = re.compile(r'href="http://videos\.francetv\.fr/video/(?P<video_id>.+?)(?:@.+?)?"')
@@ -92,13 +95,14 @@ class Pluzz(Plugin):
 
     @classmethod
     def can_handle_url(cls, url):
-        return Pluzz._url_re.match(url)
+        return cls._url_re.match(url) is not None
 
     def _get_streams(self):
         # Retrieve geolocation data
         res = self.session.http.get(self.GEO_URL)
         geo = self.session.http.json(res, schema=self._geo_schema)
         country_code = geo['reponse']['geo_info']['country_code']
+        log.debug('Country: {0}'.format(country_code))
 
         # Retrieve URL page and search for video ID
         res = self.session.http.get(self.url)
@@ -113,6 +117,7 @@ class Pluzz(Plugin):
         if match is None:
             return
         video_id = match.group('video_id')
+        log.debug('Video ID: {0}'.format(video_id))
 
         res = self.session.http.get(self.API_URL.format(video_id))
         videos = self.session.http.json(res, schema=self._api_schema)
@@ -125,6 +130,7 @@ class Pluzz(Plugin):
 
         streams = []
         for video in videos['videos']:
+            log.trace('{0!r}'.format(video))
             video_url = video['url']
 
             # Check whether video format is available
@@ -204,13 +210,13 @@ class Pluzz(Plugin):
                 yield stream
 
         if offline:
-            self.logger.error('Failed to access stream, may be due to offline content')
+            log.error('Failed to access stream, may be due to offline content')
         if geolocked:
-            self.logger.error('Failed to access stream, may be due to geo-restricted content')
+            log.error('Failed to access stream, may be due to geo-restricted content')
         if drm:
-            self.logger.error('Failed to access stream, may be due to DRM-protected content')
+            log.error('Failed to access stream, may be due to DRM-protected content')
         if expired:
-            self.logger.error('Failed to access stream, may be due to expired content')
+            log.error('Failed to access stream, may be due to expired content')
 
 
 __plugin__ = Pluzz
diff --git a/src/streamlink/plugins/rtve.py b/src/streamlink/plugins/rtve.py
index d4fcb6ac..1c26ca3d 100644
--- a/src/streamlink/plugins/rtve.py
+++ b/src/streamlink/plugins/rtve.py
@@ -1,4 +1,5 @@
 import base64
+import logging
 import re
 from functools import partial
 
@@ -8,11 +9,14 @@ from streamlink.compat import bytes, is_py3
 from streamlink.plugin import Plugin, PluginArguments, PluginArgument
 from streamlink.plugin.api import useragents
 from streamlink.plugin.api import validate
+from streamlink.plugin.api.utils import itertags
 from streamlink.stream import HLSStream
 from streamlink.stream import HTTPStream
 from streamlink.stream.ffmpegmux import MuxedStream
 from streamlink.utils import parse_xml
 
+log = logging.getLogger(__name__)
+
 
 class ZTNRClient(object):
     base_url = "http://ztnr.rtve.es/ztnr/res/"
@@ -54,7 +58,6 @@ class ZTNRClient(object):
 
 class Rtve(Plugin):
     secret_key = base64.b64decode("eWVMJmRhRDM=")
-    content_id_re = re.compile(r'data-id\s*=\s*"(\d+)"')
     url_re = re.compile(r"""
         https?://(?:www\.)?rtve\.es/(?:directo|noticias|television|deportes|alacarta|drmn)/.*?/?
     """, re.VERBOSE)
@@ -118,8 +121,11 @@ class Rtve(Plugin):
 
     def _get_content_id(self):
         res = self.session.http.get(self.url)
-        m = self.content_id_re.search(res.text)
-        return m and int(m.group(1))
+        for div in itertags(res.text, "div"):
+            if div.attributes.get("data-id"):
+                return int(div.attributes.get("data-id"))
+        else:
+            log.error("Failed to get content_id")
 
     def _get_subtitles(self, content_id):
         res = self.session.http.get(self.subtitles_api.format(id=content_id))
@@ -138,7 +144,7 @@ class Rtve(Plugin):
         streams = []
         content_id = self._get_content_id()
         if content_id:
-            self.logger.debug("Found content with id: {0}", content_id)
+            log.debug("Found content with id: {0}", content_id)
             stream_data = self.zclient.get_cdn_list(content_id, schema=self.cdn_schema)
             quality_map = None
 
@@ -147,8 +153,8 @@ class Rtve(Plugin):
                     if ".m3u8" in url:
                         try:
                             streams.extend(HLSStream.parse_variant_playlist(self.session, url).items())
-                        except (IOError, OSError):
-                            self.logger.debug("Failed to load m3u8 url: {0}", url)
+                        except (IOError, OSError) as err:
+                            log.error(str(err))
                     elif ((url.endswith("mp4") or url.endswith("mov") or url.endswith("avi"))
                           and self.session.http.head(url, raise_for_status=False).status_code == 200):
                         if quality_map is None:  # only make the request when it is necessary
diff --git a/src/streamlink/plugins/streamme.py b/src/streamlink/plugins/streamme.py
index 3e78c371..28217ad8 100644
--- a/src/streamlink/plugins/streamme.py
+++ b/src/streamlink/plugins/streamme.py
@@ -1,26 +1,124 @@
+import logging
 import re
 
+from streamlink.exceptions import PluginError
 from streamlink.plugin import Plugin
+from streamlink.plugin.api import useragents, validate
+from streamlink.plugin.api.utils import parse_json
 from streamlink.stream import HLSStream
 
-_RE_URL = re.compile(r'^https?://(?:www.)stream.me/(\w+).*$')
+log = logging.getLogger(__name__)
 
 
 class StreamMe(Plugin):
+    _url_re = re.compile(r'^https?://(?:www\.)?stream\.me/(\w+).*$')
+
+    _streams_schema = validate.Schema(validate.all(
+        validate.transform(parse_json),
+        validate.any(
+            validate.all({
+                '_embedded': {
+                    'streams': [{
+                        'manifest': validate.url(),
+                        'active': bool,
+                        validate.optional('title'): validate.text,
+                        validate.optional('privateLocked'): bool,
+                        validate.optional('userSlug'): validate.text,
+                    }]
+                }},
+                validate.get('_embedded'),
+                validate.get('streams'),
+                validate.get(0),
+            ),
+            {
+                'reasons': [
+                    {
+                        'message': validate.text,
+                        validate.optional('code'): validate.text,
+                    }
+                ]
+            }
+        )
+    ))
+    _formats_schema = validate.Schema(validate.all(
+        validate.transform(parse_json),
+        {
+            'formats': {
+                'mp4-hls': {
+                    'encodings': [{
+                        'videoHeight': int,
+                        'location': validate.url()
+                    }],
+                    validate.optional('origin'): validate.any(None, {
+                        validate.optional('location'): validate.url(),
+                    })
+                }
+            }
+        },
+        validate.get('formats'),
+        validate.get('mp4-hls'),
+    ))
+
+    API_CHANNEL = 'https://www.stream.me/api-user/v1/{0}/channel'
+    STREAM_WEIGHTS = {
+        'source': 65535,
+    }
+
+    def __init__(self, url):
+        super(StreamMe, self).__init__(url)
+        self.author = None
+        self.title = None
+        self.session.http.headers.update({'User-Agent': useragents.FIREFOX})
 
     @classmethod
     def can_handle_url(cls, url):
-        return bool(_RE_URL.match(url))
+        return cls._url_re.match(url) is not None
+
+    @classmethod
+    def stream_weight(cls, stream):
+        if stream in cls.STREAM_WEIGHTS:
+            return cls.STREAM_WEIGHTS[stream], 'streamme'
+        return Plugin.stream_weight(stream)
+
+    def get_author(self):
+        if self.author is None:
+            self._get_channel_data()
+        return self.author
+
+    def get_title(self):
+        if self.title is None:
+            self._get_channel_data()
+        return self.title
+
+    def _get_channel_data(self):
+        username = self._url_re.match(self.url).group(1)
+        data = self.session.http.get(self.API_CHANNEL.format(username),
+                                     acceptable_status=(200, 403, 404),
+                                     schema=self._streams_schema)
+        if data.get('reasons'):
+            raise PluginError(data['reasons'][0]['message'])
+        self.author = data.get('userSlug')
+        self.title = data.get('title')
+        return data
 
     def _get_streams(self):
-        username = _RE_URL.match(self.url).group(1)
-        url = 'https://www.stream.me/api-user/v1/{0}/channel'.format(username)
-        data = self.session.http.get(url).json()
-        try:
-            m3u8 = data['_embedded']['streams'][0]['_links']['hlsmp4']['href']
-            return HLSStream.parse_variant_playlist(self.session, m3u8)
-        except KeyError:
-            return {}
+        data = self._get_channel_data()
+        log.trace('{0!r}'.format(data))
+        if not data['active']:
+            log.error('Stream is not active')
+            return
+
+        hls_streams = self.session.http.get(data['manifest'],
+                                            schema=self._formats_schema)
+        for s in hls_streams['encodings']:
+            log.trace('{0!r}'.format(s))
+            yield ('{0}p'.format(s['videoHeight']),
+                   HLSStream(self.session, s['location']))
+
+        source = hls_streams.get('origin')
+        if source:
+            log.trace('{0!r}'.format(source))
+            yield 'source', HLSStream(self.session, source['location'])
 
 
 __plugin__ = StreamMe
diff --git a/src/streamlink/plugins/tf1.py b/src/streamlink/plugins/tf1.py
index 1e4abb57..b5900bb7 100644
--- a/src/streamlink/plugins/tf1.py
+++ b/src/streamlink/plugins/tf1.py
@@ -1,4 +1,4 @@
-from __future__ import print_function
+from __future__ import unicode_literals
 
 import logging
 import re
@@ -18,7 +18,7 @@ class TF1(Plugin):
     token = "07e45841-a17a-47cf-af64-a42311bdcc3d"
 
     def api_call(self, channel, useragent=useragents.CHROME):
-        url = urljoin(self.api_url_base, "L_"+channel.upper())
+        url = urljoin(self.api_url_base, "L_" + channel.upper())
         req = self.session.http.get(url,
                                     params=dict(token=self.token),
                                     headers={"User-Agent": useragent})
@@ -29,7 +29,7 @@ class TF1(Plugin):
             data = self.api_call(channel, useragent)
 
             if data.get("error"):
-                log.error("Failed to get stream for {0}: {error} ({code})".format(channel, **data))
+                log.error("Failed to get {format} stream for {0}: {error} ({code})".format(channel, **data))
             else:
                 log.debug("Got {format} stream {url}".format(**data))
                 yield data['format'], data['url']
@@ -42,7 +42,7 @@ class TF1(Plugin):
         m = self.url_re.match(self.url)
         if m:
             channel = m.group(1) or m.group(2)
-            self.logger.debug("Found channel {0}", channel)
+            log.debug("Found channel {0}".format(channel))
             for sformat, url in self.get_stream_urls(channel):
                 try:
                     if sformat == "dash":
@@ -55,4 +55,5 @@ class TF1(Plugin):
                     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/twitch.py b/src/streamlink/plugins/twitch.py
index 410a22b6..17ce4e0b 100644
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -222,7 +222,7 @@ class TwitchAPI(object):
     # Private API calls
 
     def access_token(self, endpoint, asset, **params):
-        return self.call("/api/{0}/{1}/access_token".format(endpoint, asset), **params)
+        return self.call("/api/{0}/{1}/access_token".format(endpoint, asset), **dict(platform="_", **params))
 
     def token(self, **params):
         return self.call("/api/viewer/token", **params)
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/stream/dash.py b/src/streamlink/stream/dash.py
index 7339735c..df8d45f4 100644
--- a/src/streamlink/stream/dash.py
+++ b/src/streamlink/stream/dash.py
@@ -11,6 +11,7 @@ from streamlink.stream.stream import Stream
 from streamlink.stream.dash_manifest import MPD, sleeper, sleep_until, utc, freeze_timeline
 from streamlink.stream.ffmpegmux import FFMPEGMuxer
 from streamlink.stream.segmented import SegmentedStreamReader, SegmentedStreamWorker, SegmentedStreamWriter
+from streamlink.utils import parse_xml
 from streamlink.utils.l10n import Language
 
 log = logging.getLogger(__name__)
@@ -162,22 +163,26 @@ class DASHStream(Stream):
         return dict(type=type(self).shortname(), url=req.url, headers=headers)
 
     @classmethod
-    def parse_manifest(cls, session, url, **args):
+    def parse_manifest(cls, session, url_or_manifest, **args):
         """
         Attempt to parse a DASH manifest file and return its streams
 
         :param session: Streamlink session instance
-        :param url: URL of the manifest file
+        :param url_or_manifest: URL of the manifest file or an XML manifest string
         :return: a dict of name -> DASHStream instances
         """
         ret = {}
-        res = session.http.get(url, **args)
-        url = res.url
 
-        urlp = list(urlparse(url))
-        urlp[2], _ = urlp[2].rsplit("/", 1)
+        if url_or_manifest.startswith('<?xml'):
+            mpd = MPD(parse_xml(url_or_manifest, ignore_ns=True))
+        else:
+            res = session.http.get(url_or_manifest, **args)
+            url = res.url
 
-        mpd = MPD(session.http.xml(res, ignore_ns=True), base_url=urlunparse(urlp), url=url)
+            urlp = list(urlparse(url))
+            urlp[2], _ = urlp[2].rsplit("/", 1)
+
+            mpd = MPD(session.http.xml(res, ignore_ns=True), base_url=urlunparse(urlp), url=url)
 
         video, audio = [], []
 
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/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 992c2119..b1a83a40 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
@@ -629,8 +630,11 @@ def authenticate_twitch_oauth():
     client_id = TWITCH_CLIENT_ID
     redirect_uri = "https://streamlink.github.io/twitch_oauth.html"
     url = ("https://api.twitch.tv/kraken/oauth2/authorize"
-           "?response_type=token&client_id={0}&redirect_uri="
-           "{1}&scope=user_read+user_subscriptions").format(client_id, redirect_uri)
+           "?response_type=token"
+           "&client_id={0}"
+           "&redirect_uri={1}"
+           "&scope=user_read+user_subscriptions"
+           "&force_verify=true").format(client_id, redirect_uri)
 
     console.msg("Attempting to open a browser to let you authenticate "
                 "Streamlink with Twitch")
diff --git a/src/streamlink_cli/output.py b/src/streamlink_cli/output.py
index e8fd698d..e04471be 100644
--- a/src/streamlink_cli/output.py
+++ b/src/streamlink_cli/output.py
@@ -177,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:
@@ -194,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))


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_cdnbg.py b/tests/plugins/test_cdnbg.py
index 7b6026a6..b98b16ee 100644
--- a/tests/plugins/test_cdnbg.py
+++ b/tests/plugins/test_cdnbg.py
@@ -22,6 +22,7 @@ class TestPluginCDNBG(unittest.TestCase):
         self.assertTrue(CDNBG.can_handle_url("http://inlife.bg/"))
         self.assertTrue(CDNBG.can_handle_url("https://mmtvmusic.com/live/"))
         self.assertTrue(CDNBG.can_handle_url("http://mu-vi.tv/LiveStreams/pages/Live.aspx"))
+        self.assertTrue(CDNBG.can_handle_url("http://videochanel.bstv.bg/"))
         self.assertTrue(CDNBG.can_handle_url("https://www.bloombergtv.bg/video"))
 
         # shouldn't match
diff --git a/tests/plugins/test_crunchyroll.py b/tests/plugins/test_crunchyroll.py
index edcc4736..1a4e0659 100644
--- a/tests/plugins/test_crunchyroll.py
+++ b/tests/plugins/test_crunchyroll.py
@@ -7,9 +7,13 @@ class TestPluginCrunchyroll(unittest.TestCase):
     def test_can_handle_url(self):
         # should match
         self.assertTrue(Crunchyroll.can_handle_url("http://www.crunchyroll.com/idol-incidents/episode-1-why-become-a-dietwoman-728233"))
+        self.assertTrue(Crunchyroll.can_handle_url("http://www.crunchyroll.com/ru/idol-incidents/episode-1-why-become-a-dietwoman-728233"))
         self.assertTrue(Crunchyroll.can_handle_url("http://www.crunchyroll.com/idol-incidents/media-728233"))
+        self.assertTrue(Crunchyroll.can_handle_url("http://www.crunchyroll.com/fr/idol-incidents/media-728233"))
         self.assertTrue(Crunchyroll.can_handle_url("http://www.crunchyroll.com/media-728233"))
+        self.assertTrue(Crunchyroll.can_handle_url("http://www.crunchyroll.com/de/media-728233"))
         self.assertTrue(Crunchyroll.can_handle_url("http://www.crunchyroll.fr/media-728233"))
+        self.assertTrue(Crunchyroll.can_handle_url("http://www.crunchyroll.fr/es/media-728233"))
 
         # shouldn't match
         self.assertFalse(Crunchyroll.can_handle_url("http://www.crunchyroll.com/gintama"))
diff --git a/tests/plugins/test_facebook.py b/tests/plugins/test_facebook.py
index 76e72dff..33e2bcf6 100644
--- a/tests/plugins/test_facebook.py
+++ b/tests/plugins/test_facebook.py
@@ -10,6 +10,7 @@ class TestPluginFacebook(unittest.TestCase):
         self.assertTrue(Facebook.can_handle_url("https://www.facebook.com/nytfood/videos/1485091228202006/"))
         self.assertTrue(Facebook.can_handle_url("https://www.facebook.com/SporTurkTR/videos/798553173631138/"))
         self.assertTrue(Facebook.can_handle_url("https://www.facebook.com/119555411802156/posts/500665313691162/"))
+        self.assertTrue(Facebook.can_handle_url("https://www.facebookcorewwwi.onion/SporTurkTR/videos/798553173631138/"))
 
         # shouldn't match
         self.assertFalse(Facebook.can_handle_url("https://www.facebook.com"))
diff --git a/tests/plugins/test_mitele.py b/tests/plugins/test_mitele.py
index 502c75a4..7388df89 100644
--- a/tests/plugins/test_mitele.py
+++ b/tests/plugins/test_mitele.py
@@ -13,13 +13,14 @@ class TestPluginMitele(unittest.TestCase):
             "http://www.mitele.es/directo/energy",
             "http://www.mitele.es/directo/fdf",
             "http://www.mitele.es/directo/telecinco",
+            "https://www.mitele.es/directo/gh-duo-24h-senal-1",
+            "https://www.mitele.es/directo/gh-duo-24h-senal-2",
         ]
         for url in should_match:
             self.assertTrue(Mitele.can_handle_url(url))
 
         should_not_match = [
             "http://www.mitele.es",
-            "http://www.mitele.es/directo/random_channel",
         ]
         for url in should_not_match:
             self.assertFalse(Mitele.can_handle_url(url))
diff --git a/tests/plugins/test_streamme.py b/tests/plugins/test_streamme.py
index fb06503e..4ba25455 100644
--- a/tests/plugins/test_streamme.py
+++ b/tests/plugins/test_streamme.py
@@ -7,6 +7,7 @@ class TestPluginStreamMe(unittest.TestCase):
     def test_can_handle_url(self):
         # should match
         self.assertTrue(StreamMe.can_handle_url("http://www.stream.me/nameofstream"))
+        self.assertTrue(StreamMe.can_handle_url("https://stream.me/nameofstream"))
 
         # shouldn't match
         self.assertFalse(StreamMe.can_handle_url("http://www.livestream.me/nameofstream"))
diff --git a/tests/streams/test_dash.py b/tests/streams/test_dash.py
index 9d9bd0df..05a8fef0 100644
--- a/tests/streams/test_dash.py
+++ b/tests/streams/test_dash.py
@@ -5,7 +5,7 @@ from streamlink.stream import *
 from streamlink.stream.dash import DASHStreamWorker
 from streamlink.stream.dash_manifest import MPD
 from tests.mock import MagicMock, patch, ANY, Mock, call
-from tests.resources import xml
+from tests.resources import text, xml
 
 
 class TestDASHStream(unittest.TestCase):
@@ -208,6 +208,13 @@ class TestDASHStream(unittest.TestCase):
                           self.session, self.test_url)
         mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd")
 
+    def test_parse_manifest_string(self):
+        with text("dash/test_9.mpd") as mpd_txt:
+            test_manifest = mpd_txt.read()
+
+        streams = DASHStream.parse_manifest(self.session, test_manifest)
+        self.assertSequenceEqual(list(streams.keys()), ['2500k'])
+
     @patch('streamlink.stream.dash.DASHStreamReader')
     @patch('streamlink.stream.dash.FFMPEGMuxer')
     def test_stream_open_video_only(self, muxer, reader):
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 $"),
diff --git a/tests/test_plugin_utils.py b/tests/test_plugin_utils.py
index 1f2ec968..d4a6f32d 100644
--- a/tests/test_plugin_utils.py
+++ b/tests/test_plugin_utils.py
@@ -5,6 +5,24 @@ import unittest
 from streamlink.plugin.api.utils import itertags
 
 
+def unsupported_versions_1979():
+    """Unsupported python versions for itertags
+       3.7.0 - 3.7.2 and 3.8.0a1
+       - https://github.com/streamlink/streamlink/issues/1979
+       - https://bugs.python.org/issue34294
+    """
+    v = sys.version_info
+    if (v.major == 3) and (
+        # 3.7.0 - 3.7.2
+        (v.minor == 7 and v.micro <= 2)
+        # 3.8.0a1
+        or (v.minor == 8 and v.micro == 0 and v.releaselevel == 'alpha' and v.serial <= 1)
+    ):
+        return True
+    else:
+        return False
+
+
 class TestPluginUtil(unittest.TestCase):
     test_html = """
 <!doctype html>
@@ -42,7 +60,7 @@ href="http://test.se/foo">bar</a>
         self.assertEqual(script[1].text.strip(), """Tester.ready(function () {\nalert("Hello, world!"); });""")
         self.assertEqual(script[1].attributes, {})
 
-    @unittest.skipIf(sys.version_info >= (3, 7),
+    @unittest.skipIf(unsupported_versions_1979(),
                      "python3.7 issue, see bpo-34294")
     def test_itertags_multi_attrs(self):
         metas = list(itertags(self.test_html, "meta"))
@@ -64,7 +82,7 @@ href="http://test.se/foo">bar</a>
         self.assertEqual(anchor[0].text, "bar")
         self.assertEqual(anchor[0].attributes, {"href": "http://test.se/foo"})
 
-    @unittest.skipIf(sys.version_info >= (3, 7),
+    @unittest.skipIf(unsupported_versions_1979(),
                      "python3.7 issue, see bpo-34294")
     def test_no_end_tag(self):
         links = list(itertags(self.test_html, "link"))