diff options
author | Max Goltzsche <max.goltzsche@gmail.com> | 2024-02-23 03:52:31 +0100 |
---|---|---|
committer | Max Goltzsche <max.goltzsche@gmail.com> | 2024-04-13 04:55:37 +0200 |
commit | c0afd3eb3c897b3651c52c04b37ce597dfe9d2ce (patch) | |
tree | f26580d8cffb6aa8267607cf4985aff5b116a8ac | |
parent | cc941df025d288d4bdf65967ef658103d15086a1 (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.py | 25 | ||||
-rw-r--r-- | docs/changelog.rst | 1 | ||||
-rw-r--r-- | docs/plugins/index.rst | 4 | ||||
-rw-r--r-- | docs/plugins/smartplaylist.rst | 6 | ||||
-rw-r--r-- | test/plugins/test_smartplaylist.py | 52 |
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() |