mirror of
https://github.com/beetbox/beets.git
synced 2026-01-05 15:33:15 +01:00
Merge pull request #5121 from mgoltzsche/generate-playlist-item-attributes
This commit is contained in:
commit
d510177820
5 changed files with 141 additions and 16 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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``,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue