diff options
author | J0J0 Todos <2733783+JOJ0@users.noreply.github.com> | 2024-04-20 08:04:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-20 08:04:32 +0200 |
commit | d5101778206fa4f0ea2b6548d14f93728fb7c9d3 (patch) | |
tree | 46ebd6451e8107b184314a270b557a98bf825c91 | |
parent | 3adca9769b68f93bd2d80983125a10ab5e299400 (diff) | |
parent | 2e9308f713cc692b694b9aa1d449f837fc76413d (diff) |
Merge pull request #5121 from mgoltzsche/generate-playlist-item-attributes
-rw-r--r-- | beetsplug/smartplaylist.py | 37 | ||||
-rw-r--r-- | docs/changelog.rst | 1 | ||||
-rw-r--r-- | docs/plugins/index.rst | 22 | ||||
-rw-r--r-- | docs/plugins/smartplaylist.rst | 41 | ||||
-rw-r--r-- | test/plugins/test_smartplaylist.py | 56 |
5 files changed, 141 insertions, 16 deletions
diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 12a1c9218..9df2cca64 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, @@ -119,7 +121,7 @@ class SmartPlaylistPlugin(BeetsPlugin): spl_update.parser.add_option( "--output", type="string", - help="specify the playlist format: m3u|m3u8.", + help="specify the playlist format: m3u|extm3u.", ) spl_update.func = self.update_cmd return [spl_update] @@ -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: @@ -311,22 +313,29 @@ class SmartPlaylistPlugin(BeetsPlugin): ) mkdirall(m3u_path) pl_format = self.config["output"].get() - if pl_format != "m3u" and pl_format != "m3u8": + if pl_format != "m3u" and pl_format != "extm3u": msg = "Unsupported output format '{}' provided! " - msg += "Supported: m3u, m3u8" + msg += "Supported: m3u, extm3u" raise Exception(msg.format(pl_format)) - m3u8 = pl_format == "m3u8" + extm3u = pl_format == "extm3u" with open(syspath(m3u_path), "wb") as f: - if m3u8: + keys = [] + if extm3u: + 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 + if extm3u: + 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 f8933e5d8..c0a7c0b41 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -162,6 +162,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..6705344c9 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -422,6 +422,10 @@ Here are a few of the plugins written by the beets community: `beets-autofix`_ Automates repetitive tasks to keep your library in order. +`beets-autogenre`_ + Assigns genres to your library items using the :doc:`lastgenre <lastgenre>` + and `beets-xtractor`_ plugins as well as additional rules. + `beets-audible`_ Adds Audible as a tagger data source and provides other features for managing audiobook collections. @@ -434,7 +438,9 @@ Here are a few of the plugins written by the beets community: Enables **bandcamp.com** autotagger with a fairly extensive amount of metadata. `beetstream`_ - Is server implementation of the `SubSonic API`_ specification, allowing you to stream your music on a multitude of clients. + Server implementation of the `Subsonic API`_ specification, serving the + beets library and (:doc:`smartplaylist <smartplaylist>` plugin generated) + M3U playlists, allowing you to stream your music on a multitude of clients. `beets-bpmanalyser`_ Analyses songs and calculates their tempo (BPM). @@ -508,6 +514,15 @@ Here are a few of the plugins written by the beets community: `beets-usertag`_ Lets you use keywords to tag and organize your music. +`beets-webm3u`_ + Serves the (:doc:`smartplaylist <smartplaylist>` plugin generated) M3U + playlists via HTTP. + +`beets-webrouter`_ + Serves multiple beets webapps (e.g. :doc:`web <web>`, `beets-webm3u`_, + `beetstream`_, :doc:`aura <aura>`) using a single command/process/host/port, + each under a different path. + `whatlastgenre`_ Fetches genres from various music sites. @@ -529,7 +544,7 @@ Here are a few of the plugins written by the beets community: .. _beets-barcode: https://github.com/8h2a/beets-barcode .. _beetcamp: https://github.com/snejus/beetcamp .. _beetstream: https://github.com/BinaryBrain/Beetstream -.. _SubSonic API: http://www.subsonic.org/pages/api.jsp +.. _Subsonic API: http://www.subsonic.org/pages/api.jsp .. _beets-check: https://github.com/geigerzaehler/beets-check .. _beets-copyartifacts: https://github.com/adammillerio/beets-copyartifacts .. _dsedivec: https://github.com/dsedivec/beets-plugins @@ -568,3 +583,6 @@ Here are a few of the plugins written by the beets community: .. _beets-audible: https://github.com/Neurrone/beets-audible .. _beets-more: https://forgejo.sny.sh/sun/beetsplug/src/branch/main/more .. _beets-mpd-utils: https://github.com/thekakkun/beets-mpd-utils +.. _beets-webm3u: https://github.com/mgoltzsche/beets-webm3u +.. _beets-webrouter: https://github.com/mgoltzsche/beets-webrouter +.. _beets-autogenre: https://github.com/mgoltzsche/beets-autogenre diff --git a/docs/plugins/smartplaylist.rst b/docs/plugins/smartplaylist.rst index a40d18882..af7a09f03 100644 --- a/docs/plugins/smartplaylist.rst +++ b/docs/plugins/smartplaylist.rst @@ -93,6 +93,38 @@ The ``pretend_paths`` configuration option sets whether the items should be displayed as per the user's ``format_item`` setting or what the file paths as they would be written to the m3u file look like. +In case you want to export additional fields from the beets database into the +generated playlists, you can do so by specifying them within the ``fields`` +configuration option and setting the ``output`` option to ``extm3u``. +For instance the following configuration exports the ``id`` and ``genre`` +fields: + + smartplaylist: + playlist_dir: /data/playlists + relative_to: /data/playlists + output: extm3u + fields: + + - id + - genre + + playlists: + + - name: all.m3u + query: '' + +A resulting ``all.m3u`` file could look as follows: + + #EXTM3U + #EXTINF:805 id="1931" genre="Jazz",Miles Davis - Autumn Leaves + ../music/Albums/Miles Davis/Autumn Leaves/02 Autumn Leaves.mp3 + +To give a usage example, the `webm3u`_ and `Beetstream`_ plugins read the +exported ``id`` field, allowing you to serve your local m3u playlists via HTTP. + +.. _Beetstream: https://github.com/BinaryBrain/Beetstream +.. _webm3u: https://github.com/mgoltzsche/beets-webm3u + Configuration ------------- @@ -122,7 +154,14 @@ 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. -- **output**: Specify the playlist format: m3u|m3u8. Default ``m3u``. +- **output**: Specify the playlist format: m3u|extm3u. 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`_ and `Beetstream`_ plugins. + To use this option, you must set the ``output`` option to ``extm3u``. + +.. _Beetstream: https://github.com/BinaryBrain/Beetstream +.. _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..470350f03 100644 --- a/test/plugins/test_smartplaylist.py +++ b/test/plugins/test_smartplaylist.py @@ -191,7 +191,7 @@ class SmartPlaylistTest(_common.TestCase): self.assertEqual(content, b"/tagada.mp3\n") - def test_playlist_update_output_m3u8(self): + def test_playlist_update_output_extm3u(self): spl = SmartPlaylistPlugin() i = MagicMock() @@ -215,7 +215,7 @@ class SmartPlaylistTest(_common.TestCase): spl._matched_playlists = [pl] dir = bytestring_path(mkdtemp()) - config["smartplaylist"]["output"] = "m3u8" + config["smartplaylist"]["output"] = "extm3u" config["smartplaylist"]["prefix"] = "http://beets:8337/files" config["smartplaylist"]["relative_to"] = False config["smartplaylist"]["playlist_dir"] = py3_path(dir) @@ -241,6 +241,58 @@ class SmartPlaylistTest(_common.TestCase): + b"http://beets:8337/files/tagada.mp3\n", ) + def test_playlist_update_output_extm3u_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"] = "extm3u" + 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() |