diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 12a1c92181..4c7acea866 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 @@ def __init__(self): "auto": True, "playlists": [], "uri_format": None, + "attributes": [], "forward_slash": False, "prefix": "", "urlencode": False, @@ -297,7 +299,7 @@ def update_playlists(self, lib, pretend=False): 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 @@ def update_playlists(self, lib, pretend=False): raise Exception(msg.format(pl_format)) m3u8 = pl_format == "m3u8" with open(syspath(m3u_path), "wb") as f: + fields = [] if m3u8: + keys = self.config["attributes"].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 @@ def update_playlists(self, lib, pretend=False): 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 bef91e21d0..a7cc2009d5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -153,6 +153,7 @@ New features: * :doc:`/plugins/smartplaylist`: Add new option `smartplaylist.uri_format`. * Sorted the default configuration file into categories. :bug:`4987` +* :doc:`/plugins/smartplaylist`: Add new config option `smartplaylist.attributes`. Bug fixes: diff --git a/docs/plugins/smartplaylist.rst b/docs/plugins/smartplaylist.rst index a40d188823..b4bdec6567 100644 --- a/docs/plugins/smartplaylist.rst +++ b/docs/plugins/smartplaylist.rst @@ -122,6 +122,8 @@ other configuration options are: playlist item URI, e.g. ``http://beets:8337/item/$id/file``. When this option is specified, the local path-related options ``prefix``, ``relative_to``, ``forward_slash`` and ``urlencode`` are ignored. +- **attributes**: Specify the names of the additional item attributes to write + into the playlist. - **output**: Specify the playlist format: m3u|m3u8. Default ``m3u``. For many configuration options, there is a corresponding CLI option, e.g. diff --git a/test/plugins/test_smartplaylist.py b/test/plugins/test_smartplaylist.py index ee3bfb8ce7..a99eee4bd6 100644 --- a/test/plugins/test_smartplaylist.py +++ b/test/plugins/test_smartplaylist.py @@ -241,6 +241,58 @@ def test_playlist_update_output_m3u8(self): + b"http://beets:8337/files/tagada.mp3\n", ) + def test_playlist_update_output_m3u8_attrs(self): + spl = SmartPlaylistPlugin() + + i = MagicMock() + type(i).id = PropertyMock(return_value=123) + type(i).artist = PropertyMock(return_value="Fake Artist") + type(i).title = PropertyMock(return_value="fake Title") + type(i).genre = PropertyMock(return_value="Fake Genre") + type(i).length = PropertyMock(return_value=300.123) + type(i).path = PropertyMock(return_value=b"/tagada.mp3") + 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.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"]["attributes"] = ["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="123" genre="Fake Genre",Fake Artist - fake Title\n' + + b"http://beets:8337/files/tagada.mp3\n", + ) + def test_playlist_update_uri_format(self): spl = SmartPlaylistPlugin()