Repository: streamlink/streamlink
Pull request #675 (2017-03-07 13:44:20)

stream.hls: format string name input for parse_variant_playlist
This allows developers to use a format string to name the streams, which allows more descriptive names. eg. {pixels}_{bitrate} can be used to produce 720p_3500k, 720p_4500k, which is useful if the streams are available in multiple resolutions at the same bitrate.

This feature can also be used when using the builtin stream plugins, eg.
```shell
streamlink "hlsvariant://test.se/master.m3u8 name_fmt=\"{pixels}_{bitrate}\""
```

The limitation being that the name string has to be enclosed by `"` quotes.

This should go someway to fixing issues like #540.

codecov[bot]: # [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/675?src=pr&el=h1) Report
> Merging [#675](https://codecov.io/gh/streamlink/streamlink/pull/675?src=pr&el=desc) into [master](https://codecov.io/gh/streamlink/streamlink/commit/0a680946397e4ca2a038322c1a627c24d43a0f3e?src=pr&el=desc) will **decrease** coverage by `-0.01%`.
> The diff coverage is `63.63%`.


```diff
@@            Coverage Diff             @@
##           master     #675      +/-   ##
==========================================
- Coverage   42.38%   42.38%   -0.01%     
==========================================
  Files          69       69              
  Lines        6670     6675       +5     
==========================================
+ Hits         2827     2829       +2     
- Misses       3843     3846       +3
```

------

[Continue to review full report at Codecov](https://codecov.io/gh/streamlink/streamlink/pull/675?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/675?src=pr&el=footer). Last update [0a68094...db861a0](https://codecov.io/gh/streamlink/streamlink/compare/0a680946397e4ca2a038322c1a627c24d43a0f3e...db861a09f15d79809a183ed0cedb256830aa04d3?el=footer&src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).
beardypig: I'll add some tests :)

Patch:
diff --git a/docs/donate.rst b/docs/donate.rst
index e0acc40e..e36f921c 100644
--- a/docs/donate.rst
+++ b/docs/donate.rst
@@ -15,7 +15,7 @@ Streamlink Team
 ---------------
 
 Bastimeyer 
-^^^^^^^^^^
+----------
 
 - `Github <https://github.com/bastimeyer>`__
 - `Bitcoin <https://blockchain.info/qr?data=1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8>`__ (`1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8`)
@@ -24,13 +24,13 @@ Bastimeyer
 - `Paypal <https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YUCGRLVALHS8C&item_name=Streamlink%20Twitch%20GUI>`__
 
 Beardypig 
-^^^^^^^^^
+---------
 
 - `Github <https://github.com/beardypig>`__
 - `Bitcoin <https://blockchain.info/qr?data=1Ey3KZ9SwTj6QyASE6vgJVebUiJsW1HuRh>`__ (`1Ey3KZ9SwTj6QyASE6vgJVebUiJsW1HuRh`)
 
 Gravyboat
-^^^^^^^^^
+---------
 
 - `Github <https://github.com/gravyboat>`__
 - `Bitcoin <https://blockchain.info/qr?data=1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj>`__ (`1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj`)
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
index 3fe7ac0e..067b6257 100644
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -7,6 +7,10 @@ from ..cache import Cache
 from ..exceptions import PluginError, NoStreamsError
 from ..options import Options
 
+# FIXME: This is a crude attempt at making a bitrate's
+# weight end up similar to the weight of a resolution.
+# Someone who knows math, please fix.
+BIT_RATE_WEIGHT_RATIO = 2.8
 
 QUALITY_WEIGTHS_EXTRA = {
     "other": {
@@ -37,28 +41,28 @@ def stream_weight(stream):
         if stream in weights:
             return weights[stream], group
 
-    match = re.match("^(\d+)([k]|[p])?(\d*)([\+])?$", stream)
+    match = re.match(r"^(\d+)(k|p)?(\d+)?(\+)?(?:_(\d+)k)?$", stream)
 
     if match:
-        if match.group(2) == "k":
+        name_type = match.group(2)
+        if name_type == "k":  # bit rate
             bitrate = int(match.group(1))
-
-            # FIXME: This is a crude attempt at making a bitrate's
-            # weight end up similar to the weight of a resolution.
-            # Someone who knows math, please fix.
-            weight = bitrate / 2.8
+            weight = bitrate / BIT_RATE_WEIGHT_RATIO
 
             return weight, "bitrate"
 
-        elif match.group(2) == "p":
+        elif name_type == "p":  # resolution
             weight = int(match.group(1))
 
-            if match.group(3):
+            if match.group(3):  # fps eg. 60p or 50p
                 weight += int(match.group(3))
 
             if match.group(4) == "+":
                 weight += 1
 
+            if match.group(5):  # bit rate classifier for resolution
+                weight += int(match.group(5)) / BIT_RATE_WEIGHT_RATIO
+
             return weight, "pixels"
 
     return 0, "none"
@@ -85,7 +89,7 @@ def stream_type_priority(stream_types, stream):
 
 
 def stream_sorting_filter(expr, stream_weight):
-    match = re.match(r"(?P<op><=|>=|<|>)?(?P<value>[\w\+]+)", expr)
+    match = re.match(r"(?P<op><=|>=|<|>)?(?P<value>[\w+]+)", expr)
 
     if not match:
         raise PluginError("Invalid filter expression: {0}".format(expr))
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
index 9cfcafa7..05300ac6 100644
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -271,7 +271,7 @@ class HLSStream(HTTPStream):
     @classmethod
     def parse_variant_playlist(cls, session_, url, name_key="name",
                                name_prefix="", check_streams=False,
-                               force_restart=False,
+                               force_restart=False, name_fmt=None,
                                **request_params):
         """Attempts to parse a variant playlist and return its streams.
 
@@ -279,8 +279,10 @@ class HLSStream(HTTPStream):
         :param name_key: Prefer to use this key as stream name, valid keys are:
                          name, pixels, bitrate.
         :param name_prefix: Add this prefix to the stream names.
+        :param check_streams: Only allow streams that are accessible.
         :param force_restart: Start at the first segment even for a live stream
-        :param check_streams: Only allow streams that are accesible.
+        :param name_fmt: A format string for the name, allowed format keys are
+                         name, pixels, bitrate.
         """
         logger = session_.logger.new_module("hls.parse_variant_playlist")
         locale = session_.localization
@@ -338,8 +340,11 @@ class HLSStream(HTTPStream):
                 else:
                     names["bitrate"] = "{0}k".format(bw / 1000.0)
 
-            stream_name = (names.get(name_key) or names.get("name") or
-                           names.get("pixels") or names.get("bitrate"))
+            if name_fmt:
+                stream_name = name_fmt.format(**names)
+            else:
+                stream_name = (names.get(name_key) or names.get("name") or
+                               names.get("pixels") or names.get("bitrate"))
 
             if not stream_name:
                 continue


Test patch:
diff --git a/tests/test_plugin_stream.py b/tests/test_plugin_stream.py
index 01a4a97c..a2e37705 100644
--- a/tests/test_plugin_stream.py
+++ b/tests/test_plugin_stream.py
@@ -2,6 +2,7 @@ import unittest
 
 from streamlink import Streamlink
 from streamlink.plugins.stream import StreamURL
+from streamlink.plugin.plugin import stream_weight
 from streamlink.stream import *
 
 
@@ -107,5 +108,31 @@ class TestPluginStream(unittest.TestCase):
             StreamURL._parse_params(""""conn=['B:1', 'S:authMe', 'O:1', 'NN:code:1.23', 'NS:flag:ok', 'O:0']""")
         )
 
+    def test_stream_weight(self):
+        self.assertEqual(
+            (720, "pixels"),
+            stream_weight("720p"))
+        self.assertEqual(
+            (721, "pixels"),
+            stream_weight("720p+"))
+        self.assertEqual(
+            (780, "pixels"),
+            stream_weight("720p60"))
+
+        self.assertTrue(
+            stream_weight("720p+") > stream_weight("720p"))
+        self.assertTrue(
+            stream_weight("720p") == stream_weight("720p"))
+        self.assertTrue(
+            stream_weight("720p_3000k") > stream_weight("720p_2500k"))
+        self.assertTrue(
+            stream_weight("720p60_3000k") > stream_weight("720p_3000k"))
+        self.assertTrue(
+            stream_weight("720p_3000k") < stream_weight("720p+_3000k"))
+
+        self.assertTrue(
+            stream_weight("3000k") > stream_weight("2500k"))
+
+
 if __name__ == "__main__":
     unittest.main()