summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMax Goltzsche <max.goltzsche@gmail.com>2024-02-23 03:52:31 +0100
committerMax Goltzsche <max.goltzsche@gmail.com>2024-04-13 04:55:37 +0200
commitc0afd3eb3c897b3651c52c04b37ce597dfe9d2ce (patch)
treef26580d8cffb6aa8267607cf4985aff5b116a8ac
parentcc941df025d288d4bdf65967ef658103d15086a1 (diff)
smartplaylist: allow exporting item fields
Allow generating extm3u playlists so that they contain additional item fields such as the `id`. The feature is required by the mgoltzsche/beets-webm3u plugin (M3U server) to transform playlists using a request based item URI template which may require additional fields such as the `id`, e.g. `beets:library:track;$id`.
-rw-r--r--beetsplug/smartplaylist.py25
-rw-r--r--docs/changelog.rst1
-rw-r--r--docs/plugins/index.rst4
-rw-r--r--docs/plugins/smartplaylist.rst6
-rw-r--r--test/plugins/test_smartplaylist.py52
5 files changed, 83 insertions, 5 deletions
diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py
index 12a1c9218..7e16d3390 100644
--- a/beetsplug/smartplaylist.py
+++ b/beetsplug/smartplaylist.py
@@ -16,6 +16,7 @@
"""
+import json
import os
from urllib.request import pathname2url
@@ -46,6 +47,7 @@ class SmartPlaylistPlugin(BeetsPlugin):
"auto": True,
"playlists": [],
"uri_format": None,
+ "fields": [],
"forward_slash": False,
"prefix": "",
"urlencode": False,
@@ -297,7 +299,7 @@ class SmartPlaylistPlugin(BeetsPlugin):
item_uri = prefix + item_uri
if item_uri not in m3us[m3u_name]:
- m3us[m3u_name].append({"item": item, "uri": item_uri})
+ m3us[m3u_name].append(PlaylistItem(item, item_uri))
if pretend and self.config["pretend_paths"]:
print(displayable_path(item_uri))
elif pretend:
@@ -317,16 +319,23 @@ class SmartPlaylistPlugin(BeetsPlugin):
raise Exception(msg.format(pl_format))
m3u8 = pl_format == "m3u8"
with open(syspath(m3u_path), "wb") as f:
+ keys = []
if m3u8:
+ keys = self.config["fields"].get(list)
f.write(b"#EXTM3U\n")
for entry in m3us[m3u]:
- item = entry["item"]
+ item = entry.item
comment = ""
if m3u8:
- comment = "#EXTINF:{},{} - {}\n".format(
- int(item.length), item.artist, item.title
+ attr = [(k, entry.item[k]) for k in keys]
+ al = [
+ f" {a[0]}={json.dumps(str(a[1]))}" for a in attr
+ ]
+ attrs = "".join(al)
+ comment = "#EXTINF:{}{},{} - {}\n".format(
+ int(item.length), attrs, item.artist, item.title
)
- f.write(comment.encode("utf-8") + entry["uri"] + b"\n")
+ f.write(comment.encode("utf-8") + entry.uri + b"\n")
# Send an event when playlists were updated.
send_event("smartplaylist_update")
@@ -339,3 +348,9 @@ class SmartPlaylistPlugin(BeetsPlugin):
self._log.info(
"{0} playlists updated", len(self._matched_playlists)
)
+
+
+class PlaylistItem:
+ def __init__(self, item, uri):
+ self.item = item
+ self.uri = uri
diff --git a/docs/changelog.rst b/docs/changelog.rst
index fad853fee..ce6442c3a 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -160,6 +160,7 @@ New features:
like the other lossless formats.
* Add support for `barcode` field.
:bug:`3172`
+* :doc:`/plugins/smartplaylist`: Add new config option `smartplaylist.fields`.
Bug fixes:
diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst
index 0da487b03..06b4bd256 100644
--- a/docs/plugins/index.rst
+++ b/docs/plugins/index.rst
@@ -313,6 +313,9 @@ Interoperability
Automatically notifies `Subsonic`_ whenever the beets
library changes.
+`webm3u`_
+ Serves the (:doc:`smartplaylist <smartplaylist>` plugin generated) M3U
+ playlists via HTTP.
.. _AURA: https://auraspec.readthedocs.io
.. _Emby: https://emby.media
@@ -321,6 +324,7 @@ Interoperability
.. _Kodi: https://kodi.tv
.. _Sonos: https://sonos.com
.. _Subsonic: http://www.subsonic.org/
+.. _webm3u: https://github.com/mgoltzsche/beets-webm3u
Miscellaneous
-------------
diff --git a/docs/plugins/smartplaylist.rst b/docs/plugins/smartplaylist.rst
index a40d18882..057f9910d 100644
--- a/docs/plugins/smartplaylist.rst
+++ b/docs/plugins/smartplaylist.rst
@@ -123,6 +123,12 @@ other configuration options are:
When this option is specified, the local path-related options ``prefix``,
``relative_to``, ``forward_slash`` and ``urlencode`` are ignored.
- **output**: Specify the playlist format: m3u|m3u8. Default ``m3u``.
+- **fields**: Specify the names of the additional item fields to export into
+ the playlist. This allows using e.g. the ``id`` field within other tools such
+ as the `webm3u`_ plugin.
+ To use this option, you must set the ``output`` option to ``m3u8``.
+
+.. _webm3u: https://github.com/mgoltzsche/beets-webm3u
For many configuration options, there is a corresponding CLI option, e.g.
``--playlist-dir``, ``--relative-to``, ``--prefix``, ``--forward-slash``,
diff --git a/test/plugins/test_smartplaylist.py b/test/plugins/test_smartplaylist.py
index ee3bfb8ce..15841640b 100644
--- a/test/plugins/test_smartplaylist.py
+++ b/test/plugins/test_smartplaylist.py
@@ -241,6 +241,58 @@ class SmartPlaylistTest(_common.TestCase):
+ b"http://beets:8337/files/tagada.mp3\n",
)
+ def test_playlist_update_output_m3u8_fields(self):
+ spl = SmartPlaylistPlugin()
+
+ i = MagicMock()
+ type(i).artist = PropertyMock(return_value="Fake Artist")
+ type(i).title = PropertyMock(return_value="fake Title")
+ type(i).length = PropertyMock(return_value=300.123)
+ type(i).path = PropertyMock(return_value=b"/tagada.mp3")
+ a = {"id": 456, "genre": "Fake Genre"}
+ i.__getitem__.side_effect = a.__getitem__
+ i.evaluate_template.side_effect = lambda pl, _: pl.replace(
+ b"$title",
+ b"ta:ga:da",
+ ).decode()
+
+ lib = Mock()
+ lib.replacements = CHAR_REPLACE
+ lib.items.return_value = [i]
+ lib.albums.return_value = []
+
+ q = Mock()
+ a_q = Mock()
+ pl = b"$title-my<playlist>.m3u", (q, None), (a_q, None)
+ spl._matched_playlists = [pl]
+
+ dir = bytestring_path(mkdtemp())
+ config["smartplaylist"]["output"] = "m3u8"
+ config["smartplaylist"]["relative_to"] = False
+ config["smartplaylist"]["playlist_dir"] = py3_path(dir)
+ config["smartplaylist"]["fields"] = ["id", "genre"]
+ try:
+ spl.update_playlists(lib)
+ except Exception:
+ rmtree(syspath(dir))
+ raise
+
+ lib.items.assert_called_once_with(q, None)
+ lib.albums.assert_called_once_with(a_q, None)
+
+ m3u_filepath = path.join(dir, b"ta_ga_da-my_playlist_.m3u")
+ self.assertExists(m3u_filepath)
+ with open(syspath(m3u_filepath), "rb") as f:
+ content = f.read()
+ rmtree(syspath(dir))
+
+ self.assertEqual(
+ content,
+ b"#EXTM3U\n"
+ + b'#EXTINF:300 id="456" genre="Fake Genre",Fake Artist - fake Title\n'
+ + b"/tagada.mp3\n",
+ )
+
def test_playlist_update_uri_format(self):
spl = SmartPlaylistPlugin()