Merge pull request #5121 from mgoltzsche/generate-playlist-item-attributes

This commit is contained in:
J0J0 Todos 2024-04-20 08:04:32 +02:00 committed by GitHub
commit d510177820
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 141 additions and 16 deletions

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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``,

View file

@ -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()