summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJ0J0 Todos <2733783+JOJ0@users.noreply.github.com>2024-04-20 08:04:32 +0200
committerGitHub <noreply@github.com>2024-04-20 08:04:32 +0200
commitd5101778206fa4f0ea2b6548d14f93728fb7c9d3 (patch)
tree46ebd6451e8107b184314a270b557a98bf825c91
parent3adca9769b68f93bd2d80983125a10ab5e299400 (diff)
parent2e9308f713cc692b694b9aa1d449f837fc76413d (diff)
Merge pull request #5121 from mgoltzsche/generate-playlist-item-attributes
-rw-r--r--beetsplug/smartplaylist.py37
-rw-r--r--docs/changelog.rst1
-rw-r--r--docs/plugins/index.rst22
-rw-r--r--docs/plugins/smartplaylist.rst41
-rw-r--r--test/plugins/test_smartplaylist.py56
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()