Repository: streamlink/streamlink
Pull request #2285 (2019-02-01 05:32:38)

streams.dash: Support manifest strings in addition to manifest urls
Modifies `parse_manifest()` to accept a `url_or_manifest` parameter.

See also: #2239 

This does not include any of the changes for supporting DASH SegmentBase.


Patch:
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/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/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/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/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..fe5e8e3f 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.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/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..9acb1094 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -152,7 +152,8 @@ def create_title(plugin=None):
             title=lambda: plugin.get_title() or DEFAULT_STREAM_METADATA["title"],
             author=lambda: plugin.get_author() or DEFAULT_STREAM_METADATA["author"],
             category=lambda: plugin.get_category() or DEFAULT_STREAM_METADATA["category"],
-            game=lambda: plugin.get_category() or DEFAULT_STREAM_METADATA["game"]
+            game=lambda: plugin.get_category() or DEFAULT_STREAM_METADATA["game"],
+            url=plugin.url
         )
     else:
         title = args.url
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_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_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 $"),