codecov[bot]: # [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/2112?src=pr&el=h1) Report
> Merging [#2112](https://codecov.io/gh/streamlink/streamlink/pull/2112?src=pr&el=desc) into [master](https://codecov.io/gh/streamlink/streamlink/commit/1961201e6e4dd74f4c1d0f9fecf8a4ed87a60e66?src=pr&el=desc) will **decrease** coverage by `0.03%`.
> The diff coverage is `44.44%`.


```diff
@@            Coverage Diff             @@
##           master    #2112      +/-   ##
==========================================
- Coverage   35.81%   35.78%   -0.04%     
==========================================
  Files         231      232       +1     
  Lines       20289    20343      +54     
==========================================
+ Hits         7267     7279      +12     
- Misses      13022    13064      +42
```

diff --git a/docs/cli.rst b/docs/cli.rst
index 02c5ed31..0ebcadf0 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -239,6 +239,27 @@ For this, the plugin provides the :option:`--crunchyroll-purge-credentials`
 option, which removes your saved session and credentials and tries to log
 in again using your username and password.
 
+.. _cli-funimationnow:
+
+Authenticating with FunimationNow
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Like Crunchyroll, the FunimationNow plugin requires authenticating with a premium account to access some
+content: :option:`--funimation-email`, :option:`--funimation-password`. In addition, this plugin requires a ``incap_ses`` cookie to be
+sent with each HTTP request (see issue #2088); this unique session cookie can be found in your browser and sent via the :option:`--http-cookie` option.
+
+For example:
+
+.. sourcecode:: console
+
+    $ streamlink --funimation-email='xxx' --funimation-password='xxx' --http-cookie 'incap_ses_xxx=xxxx=' https://funimation.com/shows/show/an-episode-link
+
+.. note::
+
+    There are multiple ways to retrieve the required cookie.  For more
+    information on browser cookies, please consult the following:
+
+    - `What are cookies? <http://www.whatarecookies.com/view.asp>`_
+
 Sideloading plugins
 -------------------
 
diff --git a/docs/donate.rst b/docs/donate.rst
index 94e2ec4c..575852c7 100644
--- a/docs/donate.rst
+++ b/docs/donate.rst
@@ -10,9 +10,7 @@ below. If you would prefer to donate to the team as a whole, please use our
 donations are not tax deductible as Streamlink does not operate as a charitable
 organization. Your donation to the team or a team member may be used in any
 way and does not come with expectations of work to be provided or as payment
-for future work. If you would like a specific feature implemented consider
-creating a bounty on
-`Bountysource <https://www.bountysource.com/teams/streamlink>`_.
+for future work.
 
 ---------------
 Streamlink Team
@@ -38,5 +36,5 @@ Gravyboat
 
 - `Github <https://github.com/gravyboat>`__
 - `Bitcoin <https://blockchain.info/qr?data=1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj>`__ (`1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj`)
-- `Flattr <https://flattr.com/profile/gravyboat>`__
+- `Flattr <https://flattr.com/@gravyboat>`__
 - `Gratipay <https://gratipay.com/~gravyboat/>`__
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 9cb83e2b..0c2a56e0 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -81,7 +81,7 @@ europaplus              europaplus.ru        Yes   --
 facebook                facebook.com         Yes   No    Only 360p HLS streams.
 filmon                  filmon.com           Yes   Yes   Only SD quality streams.
 foxtr                   fox.com.tr           Yes   No
-funimationnow           - funimation.com     --    Yes
+funimationnow           - funimation.com     --    Yes   :ref:`Requires session cookies <cli-funimationnow>`
                         - funimationnow.uk
 gardenersworld          gardenersworld.com   --    Yes
 garena                  garena.live          Yes   --
@@ -108,6 +108,7 @@ livestream              new.livestream.com   Yes   --
 lrt                     lrt.lt               Yes   No
 ltv_lsm_lv              ltv.lsm.lv           Yes   No    Streams may be geo-restricted to Latvia.
 mediaklikk              mediaklikk.hu        Yes   No    Streams may be geo-restricted to Hungary.
+metube                  metube.id            Yes   Yes
 mitele                  mitele.es            Yes   No    Streams may be geo-restricted to Spain.
 mixer                   mixer.com            Yes   Yes
 mjunoon                 mjunoon.tv           Yes   Yes
@@ -141,7 +142,7 @@ playtv                  - playtv.fr          Yes   --    Streams may be geo-rest
 pluzz                   - france.tv          Yes   Yes   Streams may be geo-restricted to France, Andorra and Monaco.
                         - ludo.fr
                         - zouzous.fr
-                        - france3-reg.. [8]_
+                        - francetvinfo.fr
 powerapp                powerapp.com.tr      Yes   No
 qq                      live.qq.com          Yes   No
 radionet                - radio.net          Yes   --
@@ -188,6 +189,7 @@ svtplay                 - svtplay.se         Yes   Yes   Streams may be geo-rest
                         - oppetarkiv.se
 swisstxt                - srf.ch             Yes   No    Streams are geo-restricted to Switzerland.
                         - rsi.ch
+tamago                  player.tamago.live   Yes   --
 teamliquid              teamliquid.net       Yes   Yes
 teleclubzoom            teleclubzoom.ch      Yes   No    Streams are geo-restricted to Switzerland.
 telefe                  telefe.com           No    Yes   Streams are geo-restricted to Argentina.
@@ -214,7 +216,7 @@ tv4play                 - tv4play.se         Yes   Yes   Streams may be geo-rest
                         - fotbollskanalen.se
 tv5monde                - tv5monde.com       Yes   Yes   Streams may be geo-restricted to France, Belgium and Switzerland.
                         - tv5mondeplus.com
-                        - tv5mondepl... [9]_
+                        - tv5mondepl... [8]_
 tv8                     tv8.com.tr           Yes   No
 tv360                   tv360.com.tr         Yes   No
 tvcatchup               tvcatchup.com        Yes   No    Streams may be geo-restricted to Great Britain.
@@ -256,10 +258,10 @@ youtube                 - youtube.com        Yes   Yes   Protected videos are no
                         - youtu.be
 yupptv                  yupptv.com           Yes   Yes   Some streams require an account and subscription.
 zattoo                  - zattoo.com         Yes   Yes
-                        - nettv.net... [10]_
+                        - nettv.net... [9]_
                         - tvonline.ewe.de
-                        - iptv.glat... [11]_
-                        - mobiltv.q... [12]_
+                        - iptv.glat... [10]_
+                        - mobiltv.q... [11]_
                         - player.waly.tv
                         - tvplus.m-net.de
                         - www.bbv-tv.net
@@ -279,8 +281,7 @@ zhanqi                  zhanqi.tv            Yes   No
 .. [5] mediathek.daserste.de
 .. [6] players.brightcove.net
 .. [7] player.theplatform.com
-.. [8] france3-regions.francetvinfo.fr
-.. [9] tv5mondeplusafrique.com
-.. [10] nettv.netcologne.de
-.. [11] iptv.glattvision.ch
-.. [12] mobiltv.quickline.com
+.. [8] tv5mondeplusafrique.com
+.. [9] nettv.netcologne.de
+.. [10] iptv.glattvision.ch
+.. [11] mobiltv.quickline.com
diff --git a/src/streamlink/_version.py b/src/streamlink/_version.py
index 5e81ded0..48366bc0 100644
--- a/src/streamlink/_version.py
+++ b/src/streamlink/_version.py
@@ -235,7 +235,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
     # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
     # if there isn't one, this yields HEX[-dirty] (no NUM)
     describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
-                                          "--always", "--long",
+                                          "--always", "--long", "--abbrev=7",
                                           "--match", "%s*" % tag_prefix],
                                    cwd=root)
     # --long was added in git-1.5.5
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
index 1f5513d7..ef38ac47 100644
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -384,6 +384,7 @@ class Plugin(object):
 
         stream_names = filter(stream_weight_only, streams.keys())
         sorted_streams = sorted(stream_names, key=stream_weight_only)
+        unfiltered_sorted_streams = sorted_streams
 
         if isinstance(sorting_excludes, list):
             for expr in sorting_excludes:
@@ -402,6 +403,11 @@ class Plugin(object):
             worst = sorted_streams[0]
             final_sorted_streams["worst"] = streams[worst]
             final_sorted_streams["best"] = streams[best]
+        elif len(unfiltered_sorted_streams) > 0:
+            best = unfiltered_sorted_streams[-1]
+            worst = unfiltered_sorted_streams[0]
+            final_sorted_streams["worst-unfiltered"] = streams[worst]
+            final_sorted_streams["best-unfiltered"] = streams[best]
 
         return final_sorted_streams
 
diff --git a/src/streamlink/plugins/bilibili.py b/src/streamlink/plugins/bilibili.py
index 4e3c068e..8ddb95cd 100644
--- a/src/streamlink/plugins/bilibili.py
+++ b/src/streamlink/plugins/bilibili.py
@@ -1,15 +1,12 @@
-import hashlib
 import re
-import time
 
 from requests.adapters import HTTPAdapter
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import validate, useragents
 from streamlink.stream import HTTPStream
 
-API_URL = "http://live.bilibili.com/api/playurl?cid={0}&player=1&quality=0&sign={1}&otype=json"
+API_URL = "https://api.live.bilibili.com/room/v1/Room/playUrl?cid={0}&quality=0&platform=web"
 ROOM_API = "https://api.live.bilibili.com/room/v1/Room/room_init?id={}"
-API_SECRET = "95acd7f6cc3392f3"
 SHOW_STATUS_OFFLINE = 0
 SHOW_STATUS_ONLINE = 1
 SHOW_STATUS_ROUND = 2
@@ -32,6 +29,15 @@ _room_id_schema = validate.Schema(
     validate.get("data")
 )
 
+_room_stream_list_schema = validate.Schema(
+    {
+        "data": validate.any(None, {
+            "durl": [{"url": validate.url()}]
+        })
+    },
+    validate.get("data")
+)
+
 
 class Bilibili(Plugin):
     @classmethod
@@ -56,11 +62,8 @@ class Bilibili(Plugin):
         if room_id_json['live_status'] != SHOW_STATUS_ONLINE:
             return
 
-        ts = int(time.time() / 60)
-        sign = hashlib.md5(("{0}{1}".format(channel, API_SECRET, ts)).encode("utf-8")).hexdigest()
-
-        res = self.session.http.get(API_URL.format(room_id, sign))
-        room = self.session.http.json(res)
+        res = self.session.http.get(API_URL.format(room_id))
+        room = self.session.http.json(res, schema=_room_stream_list_schema)
         if not room:
             return
 
diff --git a/src/streamlink/plugins/huomao.py b/src/streamlink/plugins/huomao.py
index aee762b3..e0d5770f 100644
--- a/src/streamlink/plugins/huomao.py
+++ b/src/streamlink/plugins/huomao.py
@@ -4,8 +4,8 @@ simply extracts the videos stream_id, stream_url and stream_quality by
 scraping the HTML and JS of one of Huomaos mobile webpages.
 
 When viewing a stream on huomao.com, the base URL references a room_id. This
-room_id is mapped one-to-one to a stream_id which references the actual .flv
-video. Both stream_id, stream_url and stream_quality can be found in the
+room_id is mapped one-to-one to a stream_id which references the actual .m3u8
+file. Both stream_id, stream_url and stream_quality can be found in the
 HTML and JS source of the mobile_page. Since one stream can occur in many
 different qualities, we scrape all stream_url and stream_quality occurrences
 and return each option to the user.
@@ -14,7 +14,7 @@ and return each option to the user.
 import re
 
 from streamlink.plugin import Plugin
-from streamlink.stream import HTTPStream
+from streamlink.stream import HLSStream
 
 # URL pattern for recognizing inputed Huomao.tv / Huomao.com URL.
 url_re = re.compile(r"""
@@ -35,18 +35,15 @@ mobile_url = "http://www.huomao.com/mobile/mob_live/{0}"
 #   <input id="html_stream" value="efmrCH" type="hidden">
 stream_id_pattern = re.compile(r'id=\"html_stream\" value=\"(?P<stream_id>\w+)\"')
 
-# Pattern for extracting each stream_url, stream_quality_url and a prettified
+# Pattern for extracting each stream_url and
 # stream_quality_name used for quality naming.
 #
 # Example from HTML:
-#   "2: 'http://live-ws.huomaotv.cn/live/'+stream+'_720/playlist.m3u8'"
+#   src="http://live-ws-hls.huomaotv.cn/live/<stream_id>_720/playlist.m3u8"
 stream_info_pattern = re.compile(r"""
-    [1-9]:
-    \s+
-    '(?P<stream_url>(?:\w|\.|:|-|/)+)
-    '\+stream\+'
-    (?P<stream_quality_url>_?(?P<stream_quality_name>\d*))
-    /playlist.m3u8'
+    (?P<stream_url>(?:[\w\/\.\-:]+)
+    \/[^_\"]+(?:_(?P<stream_quality_name>\d+))
+    ?/playlist.m3u8)
 """, re.VERBOSE)
 
 
@@ -65,11 +62,11 @@ class Huomao(Plugin):
         return stream_id.group("stream_id")
 
     def get_stream_info(self, html):
-        """Returns a nested list of different stream options.
+        """
+        Returns a nested list of different stream options.
 
-        Each entry in the list will contain a stream_url, stream_quality_url
-        and stream_quality_name for each stream occurrence that was found in
-        the JS.
+        Each entry in the list will contain a stream_url and stream_quality_name
+        for each stream occurrence that was found in the JS.
         """
         stream_info = stream_info_pattern.findall(html)
 
@@ -80,8 +77,8 @@ class Huomao(Plugin):
         # list and reassigning.
         stream_info_list = []
         for info in stream_info:
-            if not info[2]:
-                stream_info_list.append([info[0], info[1], "source"])
+            if not info[1]:
+                stream_info_list.append([info[0], "source"])
             else:
                 stream_info_list.append(list(info))
 
@@ -95,8 +92,8 @@ class Huomao(Plugin):
 
         streams = {}
         for info in stream_info:
-            streams[info[2]] = HTTPStream(self.session,
-                                          info[0] + stream_id + info[1] + ".flv")
+            if stream_id in info[0]:
+                streams[info[1]] = HLSStream(self.session, info[0])
 
         return streams
 
diff --git a/src/streamlink/plugins/metube.py b/src/streamlink/plugins/metube.py
new file mode 100644
index 00000000..8e78e929
--- /dev/null
+++ b/src/streamlink/plugins/metube.py
@@ -0,0 +1,58 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import useragents
+from streamlink.stream import HLSStream
+
+
+class MeTube(Plugin):
+
+    _url_re = re.compile(r"""https?://(?:www\.)?metube\.id/
+                         (?P<type>live|videos)/\w+(?:/.*)?""", re.VERBOSE)
+
+    # extracted from webpage source
+    _VOD_STREAM_NAMES = {
+        "3000k": "1080p",
+        "1800k": "720p",
+        "800k": "480p",
+        "300k": "240p"
+    }
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def _get_streams(self):
+        stream_type = self._url_re.match(self.url).group("type")
+        hls_re = re.compile(r"""["'](?P<url>[^"']+\.m3u8[^"']*?)["']""")
+
+        headers = {
+            "Origin": "https://www.metube.id",
+            "User-Agent": useragents.FIREFOX
+        }
+
+        res = self.session.http.get(self.url)
+        match = hls_re.search(res.text)
+
+        if not match:
+            return
+
+        stream_url = match.group("url")
+
+        if stream_type == "live":
+            return HLSStream.parse_variant_playlist(self.session, stream_url,
+                                                    headers=headers)
+        else:
+            streams = {}
+
+            for quality, stream in HLSStream.parse_variant_playlist(
+                                   self.session,
+                                   stream_url,
+                                   headers=headers).items():
+                    name = self._VOD_STREAM_NAMES.get(quality, quality)
+                    streams[name] = stream
+
+            return streams
+
+
+__plugin__ = MeTube
diff --git a/src/streamlink/plugins/pluzz.py b/src/streamlink/plugins/pluzz.py
index 3e00b4eb..7ba24028 100644
--- a/src/streamlink/plugins/pluzz.py
+++ b/src/streamlink/plugins/pluzz.py
@@ -6,25 +6,24 @@ from streamlink.plugin import Plugin, PluginArguments, PluginArgument
 from streamlink.plugin.api import validate
 from streamlink.stream import DASHStream, HDSStream, HLSStream, HTTPStream
 from streamlink.stream.ffmpegmux import MuxedStream
-from streamlink.utils import update_scheme
 
 
 class Pluzz(Plugin):
     GEO_URL = 'http://geo.francetv.fr/ws/edgescape.json'
     API_URL = 'http://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/?idDiffusion={0}'
-    PLAYER_GENERATOR_URL = 'https://sivideo.webservices.francetelevisions.fr/assets/staticmd5/getUrl?id=jquery.player.7.js'
     TOKEN_URL = 'http://hdfauthftv-a.akamaihd.net/esi/TA?url={0}'
-
-    _url_re = re.compile(
-        r'https?://((?:www)\.france\.tv/.+\.html|www\.(ludo|zouzous)\.fr/heros/[\w-]+|(sport|france3-regions)\.francetvinfo\.fr/.+?/(tv/direct)?)')
+    SWF_PLAYER_URL = 'https://staticftv-a.akamaihd.net/player/bower_components/player_flash/dist/FranceTVNVPVFlashPlayer.akamai-7301b6035a43c4e29b7935c9c36771d2.swf'
+
+    _url_re = re.compile(r'''
+        https?://(
+            (?:www\.)france\.tv/.+\.html |
+            www\.(ludo|zouzous)\.fr/heros/[\w-]+ |
+            (.+\.)?francetvinfo\.fr)
+    ''', re.VERBOSE)
     _pluzz_video_id_re = re.compile(r'data-main-video="(?P<video_id>.+?)"')
     _jeunesse_video_id_re = re.compile(r'playlist: \[{.*?,"identity":"(?P<video_id>.+?)@(?P<catalogue>Ludo|Zouzous)"')
-    _f3_regions_video_id_re = re.compile(r'"http://videos\.francetv\.fr/video/(?P<video_id>.+?)@Regions"')
     _sport_video_id_re = re.compile(r'data-video="(?P<video_id>.+?)"')
-    _player_re = re.compile(
-        r'src="(?P<player>//staticftv-a\.akamaihd\.net/player/jquery\.player.+?-[0-9a-f]+?\.js)"></script>')
-    _swf_re = re.compile(
-        r'"(bower_components/player_flash/dist/FranceTVNVPVFlashPlayer\.akamai-[0-9a-f]+\.swf)"')
+    _embed_video_id_re = re.compile(r'href="http://videos\.francetv\.fr/video/(?P<video_id>.+?)(?:@.+?)?"')
     _hds_pv_data_re = re.compile(r"~data=.+?!")
     _mp4_bitrate_re = re.compile(r'.*-(?P<bitrate>[0-9]+k)\.mp4')
 
@@ -107,23 +106,14 @@ class Pluzz(Plugin):
             match = self._pluzz_video_id_re.search(res.text)
         elif 'ludo.fr' in self.url or 'zouzous.fr' in self.url:
             match = self._jeunesse_video_id_re.search(res.text)
-        elif 'france3-regions.francetvinfo.fr' in self.url:
-            match = self._f3_regions_video_id_re.search(res.text)
         elif 'sport.francetvinfo.fr' in self.url:
             match = self._sport_video_id_re.search(res.text)
+        else:
+            match = self._embed_video_id_re.search(res.text)
         if match is None:
             return
         video_id = match.group('video_id')
 
-        # Retrieve SWF player URL
-        swf_url = None
-        res = self.session.http.get(self.PLAYER_GENERATOR_URL)
-        player_url = update_scheme(self.url, self.session.http.json(res, schema=self._player_schema)['result'])
-        res = self.session.http.get(player_url)
-        match = self._swf_re.search(res.text)
-        if match is not None:
-            swf_url = 'https://staticftv-a.akamaihd.net/player/' + match.group(1)
-
         res = self.session.http.get(self.API_URL.format(video_id))
         videos = self.session.http.json(res, schema=self._api_schema)
         now = time.time()
@@ -162,12 +152,8 @@ class Pluzz(Plugin):
                 expired = expired or True
                 continue
 
-            if ('.f4m' in video_url or
-                '.mpd' in video_url or
-                'france.tv' in self.url or
-                'sport.francetvinfo.fr' in self.url):
-                res = self.session.http.get(self.TOKEN_URL.format(video_url))
-                video_url = res.text
+            res = self.session.http.get(self.TOKEN_URL.format(video_url))
+            video_url = res.text
 
             if '.mpd' in video_url:
                 # Get redirect video URL
@@ -176,11 +162,11 @@ class Pluzz(Plugin):
                 for bitrate, stream in DASHStream.parse_manifest(self.session,
                                                                  video_url).items():
                     streams.append((bitrate, stream))
-            elif '.f4m' in video_url and swf_url is not None:
+            elif '.f4m' in video_url:
                 for bitrate, stream in HDSStream.parse_manifest(self.session,
                                                                 video_url,
                                                                 is_akamai=True,
-                                                                pvswf=swf_url).items():
+                                                                pvswf=self.SWF_PLAYER_URL).items():
                     # HDS videos with data in their manifest fragment token
                     # doesn't seem to be supported by HDSStream. Ignore such
                     # stream (but HDS stream having only the hdntl parameter in
diff --git a/src/streamlink/plugins/tamago.py b/src/streamlink/plugins/tamago.py
new file mode 100644
index 00000000..0b6dc719
--- /dev/null
+++ b/src/streamlink/plugins/tamago.py
@@ -0,0 +1,52 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import validate
+from streamlink.stream import HTTPStream
+from streamlink import NoStreamsError
+
+
+class Tamago(Plugin):
+
+    _url_re = re.compile(r"https?://(?:player\.)?tamago\.live/w/(?P<id>\d+)")
+
+    _api_url_base = "https://player.tamago.live/api/rooms/{id}"
+
+    _api_response_schema = validate.Schema({
+        u"status": 200,
+        u"message": u"Success",
+        u"data": {
+            u"room_number": validate.text,
+            u"stream": {validate.text: validate.url()}
+        }
+    })
+
+    _stream_qualities = {
+        u"150": "144p",
+        u"350": "360p",
+        u"550": "540p",
+        u"900": "720p",
+    }
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def _get_streams(self):
+        user_id = self._url_re.match(self.url).group('id')
+
+        try:
+            api_response = self.session.http.get(self._api_url_base.format(id=user_id))
+            streams = self.session.http.json(api_response, schema=self._api_response_schema)['data']['stream']
+        except Exception:
+            raise NoStreamsError(self.url)
+
+        unique_stream_urls = []
+        for stream in streams.keys():
+            if streams[stream] not in unique_stream_urls:
+                unique_stream_urls.append(streams[stream])
+                quality = self._stream_qualities[stream] if stream in self._stream_qualities.keys() else "720p+"
+                yield quality, HTTPStream(self.session, streams[stream])
+
+
+__plugin__ = Tamago
diff --git a/src/streamlink/plugins/youtube.py b/src/streamlink/plugins/youtube.py
index 38e07368..b1081c13 100644
--- a/src/streamlink/plugins/youtube.py
+++ b/src/streamlink/plugins/youtube.py
@@ -7,7 +7,7 @@ import re
 from streamlink.compat import parse_qsl, is_py2
 from streamlink.plugin import Plugin, PluginError, PluginArguments, PluginArgument
 from streamlink.plugin.api import validate, useragents
-from streamlink.plugin.api.utils import parse_query
+from streamlink.plugin.api.utils import itertags, parse_query
 from streamlink.stream import HTTPStream, HLSStream
 from streamlink.stream.ffmpegmux import MuxedStream
 from streamlink.utils import parse_json, search_dict
@@ -89,23 +89,26 @@ _config_schema = validate.Schema(
 )
 
 _ytdata_re = re.compile(r'window\["ytInitialData"\]\s*=\s*({.*?});', re.DOTALL)
-_url_re = re.compile(r"""
-    http(s)?://(\w+\.)?youtube.com
+_url_re = re.compile(r"""(?x)https?://(?:\w+\.)?youtube\.com
     (?:
         (?:
-            /(watch.+v=|embed/|v/)
+            /(?:watch.+v=|embed/(?!live_stream)|v/)
             (?P<video_id>[0-9A-z_-]{11})
         )
         |
         (?:
-            /(user|channel)/(?P<user>[^/?]+)
+            /(?:
+                (?:user|channel)/
+                |
+                embed/live_stream\?channel=
+            )(?P<user>[^/?&]+)
         )
         |
         (?:
-            /(c/)?(?P<liveChannel>[^/?]+)/live
+            /(?:c/)?(?P<liveChannel>[^/?]+)/live/?$
         )
     )
-""", re.VERBOSE)
+""")
 
 
 class YouTube(Plugin):
@@ -270,6 +273,14 @@ class YouTube(Plugin):
                             log.debug("Video ID from videoRenderer (live)")
                             return x["videoId"]
 
+        if "/embed/live_stream" in url:
+            for link in itertags(res.text, "link"):
+                if link.attributes.get("rel") == "canonical":
+                    canon_link = link.attributes.get("href")
+                    if canon_link != url:
+                        log.debug("Re-directing to canonical URL: {0}".format(canon_link))
+                        return self._find_video_id(canon_link)
+
         raise PluginError("Could not find a video on this page")
 
     def _get_stream_info(self, video_id):
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index 0c77adb3..6af91976 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -107,7 +107,7 @@ def build_parser():
         help="""
         Stream to play.
 
-        Use "best" or "worst" for selecting the highest or lowest available
+        Use ``best`` or ``worst`` for selecting the highest or lowest available
         quality.
 
         Fallback streams can be specified by using a comma-separated list:
@@ -523,7 +523,7 @@ def build_parser():
         help="""
         Stream to play.
 
-        Use "best" or "worst" for selecting the highest or lowest available
+        Use ``best`` or ``worst`` for selecting the highest or lowest available
         quality.
 
         Fallback streams can be specified by using a comma-separated list:
@@ -590,13 +590,17 @@ def build_parser():
         metavar="STREAMS",
         type=comma_list,
         help="""
-        Fine tune best/worst synonyms by excluding unwanted streams.
+        Fine tune the ``best`` and ``worst`` stream name synonyms by excluding unwanted streams.
+
+        If all of the available streams get excluded, ``best`` and ``worst`` will become
+        inaccessible and new special stream synonyms ``best-unfiltered`` and ``worst-unfiltered``
+        can be used as a fallback selection method.
 
         Uses a filter expression in the format:
 
           [operator]<value>
 
-        Valid operators are >, >=, < and <=. If no operator is specified then
+        Valid operators are ``>``, ``>=``, ``<`` and ``<=``. If no operator is specified then
         equality is tested.
 
         For example this will exclude streams ranked higher than "480p":
diff --git a/src/streamlink_cli/constants.py b/src/streamlink_cli/constants.py
index 05ec5dba..45eb7894 100644
--- a/src/streamlink_cli/constants.py
+++ b/src/streamlink_cli/constants.py
@@ -28,7 +28,7 @@ else:
     ]
     PLUGINS_DIR = os.path.expanduser(XDG_CONFIG_HOME + "/streamlink/plugins")
 
-STREAM_SYNONYMS = ["best", "worst"]
+STREAM_SYNONYMS = ["best", "worst", "best-unfiltered", "worst-unfiltered"]
 STREAM_PASSTHROUGH = ["hls", "http", "rtmp"]
 
 __all__ = [
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 3fe315ce..b43175a6 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -57,10 +57,14 @@ def check_file_output(filename, force):
     log.debug("Checking file output")
 
     if os.path.isfile(filename) and not force:
-        answer = console.ask("File {0} already exists! Overwrite it? [y/N] ",
-                             filename)
+        if sys.stdin.isatty():
+            answer = console.ask("File {0} already exists! Overwrite it? [y/N] ",
+                                 filename)
 
-        if answer.lower() != "y":
+            if answer.lower() != "y":
+                sys.exit()
+        else:
+            log.error("File {0} already exists, use --force to overwrite it.".format(filename))
             sys.exit()
 
     return FileOutput(filename)
@@ -322,7 +326,7 @@ def read_stream(stream, output, prebuffer, chunk_size=8192):
     is_player = isinstance(output, PlayerOutput)
     is_http = isinstance(output, HTTPServer)
     is_fifo = is_player and output.namedpipe
-    show_progress = isinstance(output, FileOutput) and output.fd is not stdout
+    show_progress = isinstance(output, FileOutput) and output.fd is not stdout and sys.stdout.isatty()
 
     stream_iterator = chain(
         [prebuffer],
diff --git a/versioneer.py b/versioneer.py
index 64fea1c8..831dac8b 100644
--- a/versioneer.py
+++ b/versioneer.py
@@ -1047,7 +1047,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
     # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
     # if there isn't one, this yields HEX[-dirty] (no NUM)
     describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
-                                          "--always", "--long",
+                                          "--always", "--long", "--abbrev=7",
                                           "--match", "%s*" % tag_prefix],
                                    cwd=root)
     # --long was added in git-1.5.5

metube.id plugin request
## Plugin Request
- [x] This is a plugin request and I have read the contribution guidelines and plugin request requirements.

### Description
www.metube.id
- serveral Indonesia TV channels / legal streaming portal, see TERMS & CONDITIONS
link to live channel selection:
- https://www.metube.id/live

### Example stream URLs
1.  https://www.metube.id/live/METROTV
2.  https://www.metube.id/live/GTV
3.  https://www.metube.id/live/RCTI
4.  https://www.metube.id/live/TRANS7
5. ...

### Additional comments, screenshots, etc.
sample embeded stream: m3u8 / accessing: HTTP 502 bad Gateway

1. https://cdn-livetv1.metube.id/hls/metrotv.m3u8  / 
    https://cdn-livetv1.metube.id/hls/metrotv_480/index.m3u8
2. https://cdn-livetv2.metube.id/hls/globaltv.m3u8 / 
    https://cdn-livetv2.metube.id/hls/globaltv_480/index.m3u8
3. https://cdn-livetv2.metube.id/hls/rcti.m3u8
    https://cdn-livetv2.metube.id/hls/rcti_480/index.m3u8
4. https://cdn-livetv1.metube.id/hls/trans7.m3u8
    https://cdn-livetv1.metube.id/hls/trans7_480/index.m3u8
5. .....

######


diff --git a/tests/plugins/test_huomao.py b/tests/plugins/test_huomao.py
index a27efdcb..1261680f 100644
--- a/tests/plugins/test_huomao.py
+++ b/tests/plugins/test_huomao.py
@@ -15,15 +15,12 @@ class TestPluginHuomao(unittest.TestCase):
         #   room_id             = 123456
         #   stream_id           = 9qsvyF24659
         #   stream_url          = http://live-ws.huomaotv.cn/live/
-        #   stream_quality      = source, _720 and _480
         #   stream_quality_name = source, 720 and 480
         self.mock_html = """
             <input id="html_stream" value="9qsvyF24659" type="hidden">
-            <!--            urls:{-->
-            <!--                1: 'http://live-ws.huomaotv.cn/live/'+stream+'/playlist.m3u8',-->
-            <!--                2: 'http://live-ws.huomaotv.cn/live/'+stream+'_720/playlist.m3u8',-->
-            <!--                3: 'http://live-ws.huomaotv.cn/live/'+stream+'_480/playlist.m3u8'-->
-            <!--            },-->
+            <source  src="http://live-ws-hls.huomaotv.cn/live/9qsvyF24659/playlist.m3u8">
+            <source  src="http://live-ws-hls.huomaotv.cn/live/9qsvyF24659_720/playlist.m3u8">
+            <source  src="http://live-ws-hls.huomaotv.cn/live/9qsvyF24659_480/playlist.m3u8">
         """
 
         # Create a mock Huomao object.
@@ -43,9 +40,9 @@ class TestPluginHuomao(unittest.TestCase):
         # Assert that the stream_url, stream_quality and stream_quality_name
         # is correctly extracted from the mock HTML.
         self.assertEqual(self.mock_huomao.get_stream_info(self.mock_html), [
-            ["http://live-ws.huomaotv.cn/live/", "", "source"],
-            ["http://live-ws.huomaotv.cn/live/", "_720", "720"],
-            ["http://live-ws.huomaotv.cn/live/", "_480", "480"]
+            ["http://live-ws-hls.huomaotv.cn/live/9qsvyF24659/playlist.m3u8", "source"],
+            ["http://live-ws-hls.huomaotv.cn/live/9qsvyF24659_720/playlist.m3u8", "720"],
+            ["http://live-ws-hls.huomaotv.cn/live/9qsvyF24659_480/playlist.m3u8", "480"]
         ])
 
     def test_can_handle_url(self):
diff --git a/tests/plugins/test_metube.py b/tests/plugins/test_metube.py
new file mode 100644
index 00000000..0848dac8
--- /dev/null
+++ b/tests/plugins/test_metube.py
@@ -0,0 +1,24 @@
+import unittest
+
+from streamlink.plugins.metube import MeTube
+
+
+class TestPluginMeTube(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'https://www.metube.id/live/METROTV',
+            'https://www.metube.id/live/GTV',
+            'https://www.metube.id/videos/16881738/yudi_28_bogor_-amazingakak',
+            'https://www.metube.id/videos/16873428/liverpool-vs-psg-3-2-goals-and-highlights-2018',
+        ]
+        for url in should_match:
+            self.assertTrue(MeTube.can_handle_url(url))
+
+    def test_can_handle_url_negative(self):
+        should_not_match = [
+            'https://www.metube.id/me/IMAA2018',
+            'https://www.metube.id/auditions',
+            'https://www.twitch.tv/twitch'
+        ]
+        for url in should_not_match:
+            self.assertFalse(MeTube.can_handle_url(url))
diff --git a/tests/plugins/test_pluzz.py b/tests/plugins/test_pluzz.py
index 5a709c31..f49018eb 100644
--- a/tests/plugins/test_pluzz.py
+++ b/tests/plugins/test_pluzz.py
@@ -23,6 +23,7 @@ class TestPluginPluzz(unittest.TestCase):
         self.assertTrue(Pluzz.can_handle_url("http://sport.francetvinfo.fr/roland-garros/direct"))
         self.assertTrue(Pluzz.can_handle_url("http://sport.francetvinfo.fr/roland-garros/live-court-3"))
         self.assertTrue(Pluzz.can_handle_url("http://sport.francetvinfo.fr/roland-garros/andy-murray-gbr-1-andrey-kuznetsov-rus-1er-tour-court-philippe-chatrier"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.francetvinfo.fr/en-direct/tv.html"))
 
         # shouldn't match
         self.assertFalse(Pluzz.can_handle_url("http://www.france.tv/"))
@@ -30,6 +31,5 @@ class TestPluginPluzz(unittest.TestCase):
         self.assertFalse(Pluzz.can_handle_url("http://www.ludo.fr/"))
         self.assertFalse(Pluzz.can_handle_url("http://www.ludo.fr/jeux"))
         self.assertFalse(Pluzz.can_handle_url("http://www.zouzous.fr/"))
-        self.assertFalse(Pluzz.can_handle_url("http://france3-regions.francetvinfo.fr/"))
         self.assertFalse(Pluzz.can_handle_url("http://www.tvcatchup.com/"))
         self.assertFalse(Pluzz.can_handle_url("http://www.youtube.com/"))
diff --git a/tests/plugins/test_tamago.py b/tests/plugins/test_tamago.py
new file mode 100644
index 00000000..06afc7db
--- /dev/null
+++ b/tests/plugins/test_tamago.py
@@ -0,0 +1,24 @@
+import unittest
+
+from streamlink.plugins.tamago import Tamago
+
+
+class TestPluginTamago(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'https://player.tamago.live/w/2009642',
+            'https://player.tamago.live/w/1882066',
+            'https://player.tamago.live/w/1870142',
+            'https://player.tamago.live/w/1729968',
+        ]
+        for url in should_match:
+            self.assertTrue(Tamago.can_handle_url(url))
+
+    def test_can_handle_url_negative(self):
+        should_not_match = [
+            'https://download.tamago.live/faq',
+            'https://player.tamago.live/gaming/pubg',
+            'https://www.twitch.tv/twitch'
+        ]
+        for url in should_not_match:
+            self.assertFalse(Tamago.can_handle_url(url))
diff --git a/tests/plugins/test_youtube.py b/tests/plugins/test_youtube.py
index 4b1934d0..d6e1ab60 100644
--- a/tests/plugins/test_youtube.py
+++ b/tests/plugins/test_youtube.py
@@ -1,17 +1,19 @@
 import unittest
 
-from streamlink.plugins.youtube import YouTube
+from streamlink.plugins.youtube import YouTube, _url_re
 
 
 class TestPluginYouTube(unittest.TestCase):
     def test_can_handle_url(self):
         should_match = [
             "https://www.youtube.com/c/EXAMPLE/live",
+            "https://www.youtube.com/c/EXAMPLE/live/",
             "https://www.youtube.com/channel/EXAMPLE",
             "https://www.youtube.com/v/aqz-KE-bpKQ",
             "https://www.youtube.com/embed/aqz-KE-bpKQ",
             "https://www.youtube.com/user/EXAMPLE/",
             "https://www.youtube.com/watch?v=aqz-KE-bpKQ",
+            "https://www.youtube.com/embed/live_stream?channel=UCNye-wNBqNL5ZzHSJj3l8Bg",
         ]
         for url in should_match:
             self.assertTrue(YouTube.can_handle_url(url))
@@ -21,3 +23,44 @@ class TestPluginYouTube(unittest.TestCase):
         ]
         for url in should_not_match:
             self.assertFalse(YouTube.can_handle_url(url))
+
+    def _test_regex(self, url, expected_string, expected_group):
+        m = _url_re.match(url)
+        self.assertIsNotNone(m)
+        self.assertEqual(expected_string, m.group(expected_group))
+
+    def test_regex_liveChannel_c(self):
+        self._test_regex("https://www.youtube.com/c/EXAMPLE/live",
+                         "EXAMPLE", "liveChannel")
+
+    def test_regex_liveChannel_no_c(self):
+        self._test_regex("https://www.youtube.com/EXAMPLE1/live",
+                         "EXAMPLE1", "liveChannel")
+
+    def test_regex_user_channel(self):
+        self._test_regex("https://www.youtube.com/channel/EXAMPLE2",
+                         "EXAMPLE2", "user")
+
+    def test_regex_user_user(self):
+        self._test_regex("https://www.youtube.com/channel/EXAMPLE3",
+                         "EXAMPLE3", "user")
+
+    def test_regex_user_embed_list_stream(self):
+        self._test_regex("https://www.youtube.com/embed/live_stream?channel=UCNye-wNBqNL5ZzHSJj3l8Bg",
+                         "UCNye-wNBqNL5ZzHSJj3l8Bg", "user")
+
+    def test_regex_user_embed_list_stream_2(self):
+        self._test_regex("https://www.youtube.com/embed/live_stream?channel=UCNye-wNBqNL5ZzHSJj3l8Bg&autoplay=1&modestbranding=1&rel=0&showinfo=0&color=white&fs=1",
+                         "UCNye-wNBqNL5ZzHSJj3l8Bg", "user")
+
+    def test_regex_video_id_v(self):
+        self._test_regex("https://www.youtube.com/v/aqz-KE-bpKQ",
+                         "aqz-KE-bpKQ", "video_id")
+
+    def test_regex_video_id_embed(self):
+        self._test_regex("https://www.youtube.com/embed/aqz-KE-bpKQ",
+                         "aqz-KE-bpKQ", "video_id")
+
+    def test_regex_video_id_watch(self):
+        self._test_regex("https://www.youtube.com/watch?v=aqz-KE-bpKQ",
+                         "aqz-KE-bpKQ", "video_id")
diff --git a/tests/plugins/testplugin.py b/tests/plugins/testplugin.py
index 06b693c4..66822ef4 100644
--- a/tests/plugins/testplugin.py
+++ b/tests/plugins/testplugin.py
@@ -37,6 +37,14 @@ class TestPlugin(Plugin):
     def _get_streams(self):
         if "empty" in self.url:
             return
+
+        if "UnsortableStreamNames" in self.url:
+            def gen():
+                for i in range(3):
+                    yield "vod", HTTPStream(self.session, "http://test.se/stream")
+
+            return gen()
+
         if "NoStreamsError" in self.url:
             raise NoStreamsError(self.url)
 
diff --git a/tests/test_cli_main.py b/tests/test_cli_main.py
index 9d817da7..2b58ea5f 100644
--- a/tests/test_cli_main.py
+++ b/tests/test_cli_main.py
@@ -3,8 +3,9 @@ import os.path
 import tempfile
 
 import streamlink_cli.main
-from streamlink_cli.main import resolve_stream_name, check_file_output
+from streamlink_cli.main import resolve_stream_name, format_valid_streams, check_file_output
 from streamlink_cli.output import FileOutput
+from streamlink.plugin.plugin import Plugin
 import unittest
 from tests.mock import Mock, patch
 
@@ -18,12 +19,25 @@ class TestCLIMain(unittest.TestCase):
         tmpfile = tempfile.NamedTemporaryFile()
         try:
             streamlink_cli.main.console = console = Mock()
+            streamlink_cli.main.sys.stdin = stdin = Mock()
+            stdin.isatty.return_value = True
             console.ask.return_value = "y"
             self.assertTrue(os.path.exists(tmpfile.name))
             self.assertIsInstance(check_file_output(tmpfile.name, False), FileOutput)
         finally:
             tmpfile.close()
 
+    def test_check_file_output_exists_notty(self):
+        tmpfile = tempfile.NamedTemporaryFile()
+        try:
+            streamlink_cli.main.console = console = Mock()
+            streamlink_cli.main.sys.stdin = stdin = Mock()
+            stdin.isatty.return_value = False
+            self.assertTrue(os.path.exists(tmpfile.name))
+            self.assertRaises(SystemExit, check_file_output, tmpfile.name, False)
+        finally:
+            tmpfile.close()
+
     def test_check_file_output_exists_force(self):
         tmpfile = tempfile.NamedTemporaryFile()
         try:
@@ -46,19 +60,71 @@ class TestCLIMain(unittest.TestCase):
             tmpfile.close()
 
     def test_resolve_stream_name(self):
-        high = Mock()
-        medium = Mock()
-        low = Mock()
+        a = Mock()
+        b = Mock()
+        c = Mock()
+        d = Mock()
+        e = Mock()
         streams = {
-            "low": low,
-            "medium": medium,
-            "high": high,
-            "worst": low,
-            "best": high
+            "160p": a,
+            "360p": b,
+            "480p": c,
+            "720p": d,
+            "1080p": e,
+            "worst": b,
+            "best": d,
+            "worst-unfiltered": a,
+            "best-unfiltered": e
         }
-        self.assertEqual("high", resolve_stream_name(streams, "best"))
-        self.assertEqual("low", resolve_stream_name(streams, "worst"))
-        self.assertEqual("medium", resolve_stream_name(streams, "medium"))
-        self.assertEqual("high", resolve_stream_name(streams, "high"))
-        self.assertEqual("low", resolve_stream_name(streams, "low"))
 
+        self.assertEqual(resolve_stream_name(streams, "unknown"), "unknown")
+        self.assertEqual(resolve_stream_name(streams, "160p"), "160p")
+        self.assertEqual(resolve_stream_name(streams, "360p"), "360p")
+        self.assertEqual(resolve_stream_name(streams, "480p"), "480p")
+        self.assertEqual(resolve_stream_name(streams, "720p"), "720p")
+        self.assertEqual(resolve_stream_name(streams, "1080p"), "1080p")
+        self.assertEqual(resolve_stream_name(streams, "worst"), "360p")
+        self.assertEqual(resolve_stream_name(streams, "best"), "720p")
+        self.assertEqual(resolve_stream_name(streams, "worst-unfiltered"), "160p")
+        self.assertEqual(resolve_stream_name(streams, "best-unfiltered"), "1080p")
+
+    def test_format_valid_streams(self):
+        class FakePlugin:
+            @classmethod
+            def stream_weight(cls, stream):
+                return Plugin.stream_weight(stream)
+        a = Mock()
+        b = Mock()
+        c = Mock()
+
+        streams = {
+            "audio": a,
+            "720p": b,
+            "1080p": c,
+            "worst": b,
+            "best": c
+        }
+        self.assertEqual(
+            format_valid_streams(FakePlugin, streams),
+            ", ".join([
+                "audio",
+                "720p (worst)",
+                "1080p (best)"
+            ])
+        )
+
+        streams = {
+            "audio": a,
+            "720p": b,
+            "1080p": c,
+            "worst-unfiltered": b,
+            "best-unfiltered": c
+        }
+        self.assertEqual(
+            format_valid_streams(FakePlugin, streams),
+            ", ".join([
+                "audio",
+                "720p (worst-unfiltered)",
+                "1080p (best-unfiltered)"
+            ])
+        )
diff --git a/tests/test_session.py b/tests/test_session.py
index 573aae37..e923ab53 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -99,12 +99,23 @@ class TestSession(unittest.TestCase):
         self.assertTrue(isinstance(streams["480p"], RTMPStream))
         self.assertTrue(isinstance(streams["480p_http"], HTTPStream))
 
-    def test_plugin_stream_sorted_excludes(self):
+    def test_plugin_stream_sorting_excludes(self):
         channel = self.session.resolve_url("http://test.se/channel")
-        streams = channel.streams(sorting_excludes=["1080p", "3000k"])
 
+        streams = channel.streams(sorting_excludes=[])
         self.assertTrue("best" in streams)
         self.assertTrue("worst" in streams)
+        self.assertFalse("best-unfiltered" in streams)
+        self.assertFalse("worst-unfiltered" in streams)
+        self.assertTrue(streams["worst"] is streams["350k"])
+        self.assertTrue(streams["best"] is streams["1080p"])
+
+        streams = channel.streams(sorting_excludes=["1080p", "3000k"])
+        self.assertTrue("best" in streams)
+        self.assertTrue("worst" in streams)
+        self.assertFalse("best-unfiltered" in streams)
+        self.assertFalse("worst-unfiltered" in streams)
+        self.assertTrue(streams["worst"] is streams["350k"])
         self.assertTrue(streams["best"] is streams["1500k"])
 
         streams = channel.streams(sorting_excludes=[">=1080p", ">1500k"])
@@ -113,6 +124,24 @@ class TestSession(unittest.TestCase):
         streams = channel.streams(sorting_excludes=lambda q: not q.endswith("p"))
         self.assertTrue(streams["best"] is streams["3000k"])
 
+        streams = channel.streams(sorting_excludes=lambda q: False)
+        self.assertFalse("best" in streams)
+        self.assertFalse("worst" in streams)
+        self.assertTrue("best-unfiltered" in streams)
+        self.assertTrue("worst-unfiltered" in streams)
+        self.assertTrue(streams["worst-unfiltered"] is streams["350k"])
+        self.assertTrue(streams["best-unfiltered"] is streams["1080p"])
+
+        channel = self.session.resolve_url("http://test.se/UnsortableStreamNames")
+        streams = channel.streams()
+        self.assertFalse("best" in streams)
+        self.assertFalse("worst" in streams)
+        self.assertFalse("best-unfiltered" in streams)
+        self.assertFalse("worst-unfiltered" in streams)
+        self.assertTrue("vod" in streams)
+        self.assertTrue("vod_alt" in streams)
+        self.assertTrue("vod_alt2" in streams)
+
     def test_plugin_support(self):
         channel = self.session.resolve_url("http://test.se/channel")
         streams = channel.streams()

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov codecov coverage mock requests-mock pynsist freezegun sphinx recommonmark
apt-get update apt-get install -y gcc
3.7
dev-requirements.txt docs-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
Babel==2.14.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
codecov==2.1.13
commonmark==0.9.1
coverage==7.2.7
distlib==0.3.9
docutils==0.20.1
exceptiongroup==1.2.2
freezegun==1.5.1
idna==3.10
imagesize==1.4.1
importlib-metadata==6.7.0
iniconfig==2.0.0
iso-639==0.4.5
iso3166==2.1.1
isodate==0.7.2
Jinja2==3.1.6
MarkupSafe==2.1.5
mock==5.2.0
packaging==24.0
pluggy==1.2.0
pycryptodome==3.22.0
Pygments==2.17.2
pynsist==2.8
PySocks==1.7.1
pytest==7.4.4
pytest-cov==4.1.0
python-dateutil==2.9.0.post0
pytz==2025.2
recommonmark==0.7.1
requests==2.31.0
requests-mock==1.12.1
requests_download==0.1.2
six==1.17.0
snowballstemmer==2.2.0
Sphinx==1.6.7
sphinxcontrib-serializinghtml==1.1.5
sphinxcontrib-websupport==1.2.4
-e git+https://github.com/streamlink/streamlink.git@1961201e6e4dd74f4c1d0f9fecf8a4ed87a60e66#egg=streamlink
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
websocket-client==1.6.1
yarg==0.1.10
zipp==3.15.0

name: streamlink
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - babel==2.14.0
      - charset-normalizer==3.4.1
      - codecov==2.1.13
      - commonmark==0.9.1
      - coverage==7.2.7
      - distlib==0.3.9
      - docutils==0.20.1
      - exceptiongroup==1.2.2
      - freezegun==1.5.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - iso-639==0.4.5
      - iso3166==2.1.1
      - isodate==0.7.2
      - jinja2==3.1.6
      - markupsafe==2.1.5
      - mock==5.2.0
      - packaging==24.0
      - pluggy==1.2.0
      - pycryptodome==3.22.0
      - pygments==2.17.2
      - pynsist==2.8
      - pysocks==1.7.1
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - recommonmark==0.7.1
      - requests==2.31.0
      - requests-download==0.1.2
      - requests-mock==1.12.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==1.6.7
      - sphinxcontrib-serializinghtml==1.1.5
      - sphinxcontrib-websupport==1.2.4
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - websocket-client==1.6.1
      - yarg==0.1.10
      - zipp==3.15.0
prefix: /opt/conda/envs/streamlink

tests/plugins/test_huomao.py::TestPluginHuomao::test_can_handle_url tests/plugins/test_huomao.py::TestPluginHuomao::test_get_stream_id tests/plugins/test_huomao.py::TestPluginHuomao::test_get_stream_quality tests/plugins/test_metube.py::TestPluginMeTube::test_can_handle_url tests/plugins/test_metube.py::TestPluginMeTube::test_can_handle_url_negative tests/plugins/test_pluzz.py::TestPluginPluzz::test_can_handle_url tests/plugins/test_tamago.py::TestPluginTamago::test_can_handle_url tests/plugins/test_tamago.py::TestPluginTamago::test_can_handle_url_negative tests/plugins/test_youtube.py::TestPluginYouTube::test_can_handle_url tests/plugins/test_youtube.py::TestPluginYouTube::test_regex_liveChannel_c tests/plugins/test_youtube.py::TestPluginYouTube::test_regex_liveChannel_no_c tests/plugins/test_youtube.py::TestPluginYouTube::test_regex_user_channel tests/plugins/test_youtube.py::TestPluginYouTube::test_regex_user_embed_list_stream tests/plugins/test_youtube.py::TestPluginYouTube::test_regex_user_embed_list_stream_2 tests/plugins/test_youtube.py::TestPluginYouTube::test_regex_user_user tests/plugins/test_youtube.py::TestPluginYouTube::test_regex_video_id_embed tests/plugins/test_youtube.py::TestPluginYouTube::test_regex_video_id_v tests/plugins/test_youtube.py::TestPluginYouTube::test_regex_video_id_watch tests/test_cli_main.py::TestCLIMain::test_check_file_output tests/test_cli_main.py::TestCLIMain::test_check_file_output_exists tests/test_cli_main.py::TestCLIMain::test_check_file_output_exists_force tests/test_cli_main.py::TestCLIMain::test_check_file_output_exists_no tests/test_cli_main.py::TestCLIMain::test_check_file_output_exists_notty tests/test_cli_main.py::TestCLIMain::test_format_valid_streams tests/test_cli_main.py::TestCLIMain::test_resolve_stream_name tests/test_session.py::TestSession::test_builtin_plugins tests/test_session.py::TestSession::test_exceptions tests/test_session.py::TestSession::test_load_plugins tests/test_session.py::TestSession::test_options tests/test_session.py::TestSession::test_plugin tests/test_session.py::TestSession::test_plugin_stream_sorting_excludes tests/test_session.py::TestSession::test_plugin_stream_types tests/test_session.py::TestSession::test_plugin_support tests/test_session.py::TestSession::test_resolve_url tests/test_session.py::TestSession::test_resolve_url_no_redirect tests/test_session.py::TestSession::test_resolve_url_priority tests/test_session.py::TestSession::test_set_and_get_locale tests/test_session.py::TestSession::test_short_exception
versioneer.py src/streamlink/_version.py src/streamlink/plugins/pluzz.py src/streamlink/plugins/youtube.py src/streamlink_cli/constants.py src/streamlink_cli/main.py src/streamlink_cli/argparser.py docs/plugin_matrix.rst docs/donate.rst src/streamlink/plugins/tamago.py src/streamlink/plugins/metube.py src/streamlink/plugins/huomao.py docs/cli.rst src/streamlink/plugins/bilibili.py src/streamlink/plugin/plugin.py
versioneer.py src/streamlink/_version.py src/streamlink/plugins/pluzz.py src/streamlink/plugins/youtube.py src/streamlink_cli/constants.py src/streamlink_cli/main.py src/streamlink_cli/argparser.py docs/plugin_matrix.rst docs/donate.rst src/streamlink/plugins/tamago.py src/streamlink/plugins/metube.py src/streamlink/plugins/huomao.py docs/cli.rst src/streamlink/plugins/bilibili.py src/streamlink/plugin/plugin.py