Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions beetsplug/smartplaylist.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def __init__(self) -> None:
super().__init__()
self.config.add(
{
"dest_regen": False,
"relative_to": None,
"playlist_dir": ".",
"auto": True,
Expand Down Expand Up @@ -100,6 +101,13 @@ def commands(self) -> list[ui.Subcommand]:
type="string",
help="directory to write the generated playlist files to.",
)
spl_update.parser.add_option(
"--dest-regen",
action="store_true",
dest="dest_regen",
help="regenerate the destination path as 'move' or 'convert' "
"commands would do.",
)
spl_update.parser.add_option(
"--relative-to",
dest="relative_to",
Expand Down Expand Up @@ -267,6 +275,7 @@ def update_playlists(self, lib: Library, pretend: bool = False) -> None:
)
tpl = self.config["uri_format"].get()
prefix = bytestring_path(self.config["prefix"].as_str())
dest_regen = self.config["dest_regen"].get()
relative_to = self.config["relative_to"].get()
if relative_to:
relative_to = normpath(relative_to)
Expand Down Expand Up @@ -318,6 +327,8 @@ def update_playlists(self, lib: Library, pretend: bool = False) -> None:
if tpl:
item_uri = tpl.replace("$id", str(item.id)).encode("utf-8")
else:
if dest_regen is True:
item_uri = item.destination()
if relative_to:
item_uri = os.path.relpath(item_uri, relative_to)
if self.config["forward_slash"].get():
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ New features
- :doc:`plugins/discogs`: Add :conf:`plugins.discogs:extra_tags` option to use
additional tags (such as ``barcode``, ``catalognum``, ``country``, ``label``,
``media``, and ``year``) in Discogs search queries.
- :doc:`plugins/smartplaylist`: Add new configuration option ``dest_regen`` to
regenerate items' path in the generated playlist instead of using those in the
library. This is useful when items have been imported in don't copy-move (``-C
-M``) mode in the library but are later passed through the ``convert`` plugin
which will regenerate new paths according to the Beets path format.

Bug fixes
~~~~~~~~~
Expand Down
16 changes: 11 additions & 5 deletions docs/plugins/smartplaylist.rst
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ options are:
``yes``.
- **playlist_dir**: Where to put the generated playlist files. Default: The
current working directory (i.e., ``'.'``).
- **dest_regen**: Regenerate the destination path as ``move`` or ``convert``
commands would do. This operation will happen before ``relative_to`` and
``prefix``. Helpful to generate playlists compatible with the ``convert``
plugin when items have been imported with the ``-C -M`` options. Default:
``false``.
- **relative_to**: Generate paths in the playlist files relative to a base
directory. If you intend to use this plugin to generate playlists for MPD,
point this to your MPD music directory. Default: Use absolute paths.
Expand All @@ -167,15 +172,16 @@ options are:
that will be written to the m3u file. Default: ``false``.
- **uri_format**: Template with an ``$id`` placeholder used generate a 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.
specified, the local path-related options ``dest_regen``, ``prefix``,
``relative_to``, ``forward_slash`` and ``urlencode`` are ignored.
- **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``.

For many configuration options, there is a corresponding CLI option, e.g.
``--playlist-dir``, ``--relative-to``, ``--prefix``, ``--forward-slash``,
``--urlencode``, ``--uri-format``, ``--output``, ``--pretend-paths``. CLI
options take precedence over those specified within the configuration file.
``--playlist-dir``, ``--dest-regen``, ``--relative-to``, ``--prefix``,
``--forward-slash``, ``--urlencode``, ``--uri-format``, ``--output``,
``--pretend-paths``. CLI options take precedence over those specified within the
configuration file.
83 changes: 83 additions & 0 deletions test/plugins/test_smartplaylist.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

# TODO: Tests in this fire are very bad. Stop using Mocks in this module.

from os import path, remove
from pathlib import Path
Expand Down Expand Up @@ -457,6 +458,88 @@ def items_side_effect(query, sort):
# Verify i2 is not duplicated
assert content.count(b"/item2.mp3") == 1

def test_playlist_update_dest_regen(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)
# Set a path which is not equal to the one returned by `item.destination`.
type(i).path = PropertyMock(
return_value=b"/imported/path/with/dont/move/tagada.mp3"
)
# Set a path which would be equal to the one returned by `item.destination`.
type(i).destination = PropertyMock(return_value=lambda: 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<playlist>.m3u", (q, None), (a_q, None)
spl._matched_playlists = {pl}

dir = mkdtemp()
config["smartplaylist"]["output"] = "extm3u"
config["smartplaylist"]["prefix"] = "http://beets:8337/files"
config["smartplaylist"]["relative_to"] = False
config["smartplaylist"]["playlist_dir"] = str(dir)

# Test when `dest_regen` is set to True:
# Intended behavior is to use the path of `i.destination`.

config["smartplaylist"]["dest_regen"] = True
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(dir, "ta_ga_da-my_playlist_.m3u")
assert m3u_filepath.exists()
with open(syspath(m3u_filepath), "rb") as f:
content = f.read()
rmtree(syspath(dir))

assert content == (
b"#EXTM3U\n"
b"#EXTINF:300,fake artist - fake title\n"
b"http://beets:8337/files/tagada.mp3\n"
)

# Test when `dest_regen` is set to False:
# Intended behavior is to use the path of `i.path`.

config["smartplaylist"]["dest_regen"] = False

try:
spl.update_playlists(lib)
except Exception:
rmtree(syspath(dir))
raise

m3u_filepath = Path(dir, "ta_ga_da-my_playlist_.m3u")
assert m3u_filepath.exists()
with open(syspath(m3u_filepath), "rb") as f:
content = f.read()
rmtree(syspath(dir))

assert content == (
b"#EXTM3U\n"
b"#EXTINF:300,fake artist - fake title\n"
b"http://beets:8337/files/imported/path/with/dont/move/tagada.mp3\n"
)


class SmartPlaylistCLITest(IOMixin, PluginTestCase):
plugin = "smartplaylist"
Expand Down
Loading