Repository: streamlink/streamlink

Issue:
Azerbaycan stations 
## Feature Request

- [x ] This is a feature request and I have read the contribution guidelines.

### Description

I want see the LiveStream over Streamlink Plugin 

### Expected / Actual behavior

The livestreams can be played on the page http://www.idmantv.az via the Firefox browser.
The site includes the broadcasters AZTV, Idman TV and Medeniyet TV
About the VLC player, it is not possible because the transmitter permanently tokens changes, the Streamlinkplugin should be able to handle it.


### Additional comments, screenshots, etc.

 
![idman tv](https://user-images.githubusercontent.com/36420635/47272680-7bdb3700-d589-11e8-99ec-39b7a683bedc.JPG)

 AZ TV 
http://185.102.219.82/5929820/tracks-v1a1/index.m3u8?token=
http://player.tvibo.com/aztv/5929820

Medeniyet
http://185.102.219.82/6858270/tracks-v1a1/index.m3u8?token=
http://player.tvibo.com/aztv/6858270/

Idman TV 
http://185.102.219.82/3977238/tracks-v1a1/index.m3u8?token=
http://player.tvibo.com/aztv/3977238/


Discussion:

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 664e80f6..81d3ec1d 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -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
@@ -219,6 +220,7 @@ tv5monde                - tv5monde.com       Yes   Yes   Streams may be geo-rest
 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.
+tvibo                   player.tvibo.com     Yes   --
 tvnbg                   - tvn.bg             Yes   -
                         - live.tvn.bg
 tvp                     tvpstream.vod.tvp.pl Yes   No    Streams may be geo-restricted to Poland.
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/tvibo.py b/src/streamlink/plugins/tvibo.py
new file mode 100644
index 00000000..71ec03a7
--- /dev/null
+++ b/src/streamlink/plugins/tvibo.py
@@ -0,0 +1,34 @@
+import logging
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+class Tvibo(Plugin):
+
+    _url_re = re.compile(r"https?://player\.tvibo\.com/\w+/(?P<id>\d+)")
+    _api_url = "http://panel.tvibo.com/api/player/streamurl/{id}"
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def _get_streams(self):
+        channel_id = self._url_re.match(self.url).group("id")
+
+        api_response = self.session.http.get(
+            self._api_url.format(id=channel_id),
+            acceptable_status=(200, 404))
+
+        data = self.session.http.json(api_response)
+        log.trace("{0!r}".format(data))
+        if data.get("st"):
+            yield "source", HLSStream(self.session, data["st"])
+        elif data.get("error"):
+            log.error(data["error"]["message"])
+
+
+__plugin__ = Tvibo
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],


Test patch:
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_tvibo.py b/tests/plugins/test_tvibo.py
new file mode 100644
index 00000000..401bc70b
--- /dev/null
+++ b/tests/plugins/test_tvibo.py
@@ -0,0 +1,22 @@
+import unittest
+
+from streamlink.plugins.tvibo import Tvibo
+
+
+class TestPluginTvibo(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'http://player.tvibo.com/aztv/5929820',
+            'http://player.tvibo.com/aztv/6858270/',
+            'http://player.tvibo.com/aztv/3977238/',
+        ]
+        for url in should_match:
+            self.assertTrue(Tvibo.can_handle_url(url))
+
+    def test_can_handle_url_negative(self):
+        should_not_match = [
+            'http://www.idmantv.az/',
+            'https://www.twitch.tv/twitch'
+        ]
+        for url in should_not_match:
+            self.assertFalse(Tvibo.can_handle_url(url))
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()