Skip to content

Commit 971de16

Browse files
authored
Merge branch 'master' into master
2 parents c640967 + 61a4ba9 commit 971de16

4 files changed

Lines changed: 105 additions & 68 deletions

File tree

beetsplug/smartplaylist.py

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from beets.plugins import send as send_event
3030
from beets.util import (
3131
bytestring_path,
32-
displayable_path,
3332
mkdirall,
3433
normpath,
3534
path_as_posix,
@@ -64,7 +63,7 @@ def __init__(self) -> None:
6463
"forward_slash": False,
6564
"prefix": "",
6665
"urlencode": False,
67-
"pretend_paths": False,
66+
"format": "$artist - $title",
6867
"output": "m3u",
6968
}
7069
)
@@ -89,23 +88,26 @@ def commands(self) -> list[ui.Subcommand]:
8988
help="display query results but don't write playlist files.",
9089
)
9190
spl_update.parser.add_option(
92-
"--pretend-paths",
93-
action="store_true",
94-
dest="pretend_paths",
95-
help="in pretend mode, log the playlist item URIs/paths.",
91+
"-f",
92+
"--format",
93+
type="string",
94+
default=self.config["format"].get(),
95+
help="print per-track log lines with custom format",
9696
)
9797
spl_update.parser.add_option(
9898
"-d",
9999
"--playlist-dir",
100100
dest="playlist_dir",
101101
metavar="PATH",
102102
type="string",
103+
default=self.config["playlist_dir"].get(),
103104
help="directory to write the generated playlist files to.",
104105
)
105106
spl_update.parser.add_option(
106107
"--dest-regen",
107108
action="store_true",
108109
dest="dest_regen",
110+
default=self.config["dest_regen"].get(bool),
109111
help="regenerate the destination path as 'move' or 'convert' "
110112
"commands would do.",
111113
)
@@ -114,33 +116,39 @@ def commands(self) -> list[ui.Subcommand]:
114116
dest="relative_to",
115117
metavar="PATH",
116118
type="string",
119+
default=self.config["relative_to"].get(),
117120
help="generate playlist item paths relative to this path.",
118121
)
119122
spl_update.parser.add_option(
120123
"--prefix",
121124
type="string",
125+
default=self.config["prefix"].get(),
122126
help="prepend string to every path in the playlist file.",
123127
)
124128
spl_update.parser.add_option(
125129
"--forward-slash",
126130
action="store_true",
127131
dest="forward_slash",
132+
default=self.config["forward_slash"].get(bool),
128133
help="force forward slash in paths within playlists.",
129134
)
130135
spl_update.parser.add_option(
131136
"--urlencode",
132137
action="store_true",
138+
default=self.config["urlencode"].get(bool),
133139
help="URL-encode all paths.",
134140
)
135141
spl_update.parser.add_option(
136142
"--uri-format",
137143
dest="uri_format",
138144
type="string",
145+
default=self.config["uri_format"].get(),
139146
help="playlist item URI template, e.g. http://beets:8337/item/$id/file.",
140147
)
141148
spl_update.parser.add_option(
142149
"--output",
143150
type="string",
151+
default=self.config["output"].get(),
144152
help="specify the playlist format: m3u|extm3u.",
145153
)
146154
spl_update.func = self.update_cmd
@@ -172,14 +180,9 @@ def update_cmd(self, lib: Library, opts: Any, args: list[str]) -> None:
172180
else:
173181
self._matched_playlists = self._unmatched_playlists
174182

175-
self.__apply_opts_to_config(opts)
183+
self.config.set(vars(opts))
176184
self.update_playlists(lib, opts.pretend)
177185

178-
def __apply_opts_to_config(self, opts: Any) -> None:
179-
for k, v in opts.__dict__.items():
180-
if v is not None and k in self.config:
181-
self.config[k] = v
182-
183186
def _parse_one_query(
184187
self, playlist: dict[str, Any], key: str, model_cls: type
185188
) -> tuple[PlaylistQuery, Sort | None]:
@@ -263,35 +266,29 @@ def db_change(self, lib: Library, model: Item | Album) -> None:
263266
self._unmatched_playlists -= self._matched_playlists
264267

265268
def update_playlists(self, lib: Library, pretend: bool = False) -> None:
266-
if pretend:
267-
self._log.info(
268-
"Showing query results for {} smart playlists...",
269-
len(self._matched_playlists),
270-
)
271-
else:
272-
self._log.info(
273-
"Updating {} smart playlists...", len(self._matched_playlists)
274-
)
269+
self._log.info(
270+
"Updating {} smart playlists...",
271+
len(self._matched_playlists),
272+
)
275273

276274
playlist_dir = bytestring_path(
277275
self.config["playlist_dir"].as_filename()
278276
)
279277
tpl = self.config["uri_format"].get()
280278
prefix = bytestring_path(self.config["prefix"].as_str())
281-
dest_regen = self.config["dest_regen"].get()
279+
dest_regen = self.config["dest_regen"].get(bool)
282280
relative_to = self.config["relative_to"].get()
283281
if relative_to:
284282
relative_to = normpath(relative_to)
285283

286-
# Maps playlist filenames to lists of track filenames.
284+
# Maps playlist filenames to lists of track entries and URI sets used
285+
# to deduplicate output lines.
287286
m3us: dict[str, list[PlaylistItem]] = {}
287+
m3u_uris_by_name: dict[str, set[Any]] = {}
288288

289289
for playlist in self._matched_playlists:
290+
matched_count = 0
290291
name, (query, q_sort), (album_query, a_q_sort) = playlist
291-
if pretend:
292-
self._log.info("Results for playlist {}:", name)
293-
else:
294-
self._log.info("Creating playlist {}", name)
295292
items = []
296293

297294
# Handle tuple/list of queries (preserves order)
@@ -321,11 +318,13 @@ def update_playlists(self, lib: Library, pretend: bool = False) -> None:
321318

322319
# As we allow tags in the m3u names, we'll need to iterate through
323320
# the items and generate the correct m3u file names.
321+
matched_items: list[tuple[Any, Any]] = []
324322
for item in items:
325323
m3u_name = item.evaluate_template(name, True)
326324
m3u_name = sanitize_path(m3u_name, lib.replacements)
327325
if m3u_name not in m3us:
328326
m3us[m3u_name] = []
327+
m3u_uris_by_name[m3u_name] = set()
329328
item_uri = item.path
330329
if tpl:
331330
item_uri = tpl.replace("$id", str(item.id)).encode("utf-8")
@@ -334,20 +333,27 @@ def update_playlists(self, lib: Library, pretend: bool = False) -> None:
334333
item_uri = item.destination()
335334
if relative_to:
336335
item_uri = os.path.relpath(item_uri, relative_to)
337-
if self.config["forward_slash"].get():
336+
if self.config["forward_slash"].get(bool):
338337
item_uri = path_as_posix(item_uri)
339-
if self.config["urlencode"]:
338+
if self.config["urlencode"].get(bool):
340339
item_uri = bytestring_path(
341340
pathname2url(os.fsdecode(item_uri))
342341
)
343342
item_uri = prefix + item_uri
344343

345-
if item_uri not in m3us[m3u_name]:
344+
if item_uri not in m3u_uris_by_name[m3u_name]:
345+
m3u_uris_by_name[m3u_name].add(item_uri)
346346
m3us[m3u_name].append(PlaylistItem(item, item_uri))
347-
if pretend and self.config["pretend_paths"]:
348-
print(displayable_path(item_uri))
349-
elif pretend:
350-
print(item)
347+
matched_items.append((item, item_uri))
348+
matched_count += 1
349+
350+
self._log.info(
351+
"Creating playlist {}: {} tracks.", name, matched_count
352+
)
353+
for item, item_uri in matched_items:
354+
self._log.debug(
355+
item.evaluate_template(self.config["format"].as_str())
356+
)
351357

352358
if not pretend:
353359
# Write all of the accumulated track lists to files.
@@ -387,7 +393,7 @@ def update_playlists(self, lib: Library, pretend: bool = False) -> None:
387393

388394
if pretend:
389395
self._log.info(
390-
"Displayed results for {} playlists",
396+
"{} playlists would be updated",
391397
len(self._matched_playlists),
392398
)
393399
else:

docs/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ Unreleased
1212
New features
1313
~~~~~~~~~~~~
1414

15+
- :doc:`plugins/smartplaylist`: The ``splupdate`` command output is
16+
restructured. The per-playlist summary now includes a track count. Per-track
17+
details are shown only when ``-v`` flag is provided (``beet -v splupdate``).
18+
The ``--pretend`` flag produces the same output but reports *"N playlists
19+
would be updated"* instead of *"N playlists updated"*. The ``--format`` option
20+
allows customizing the track line format. The ``--pretend-paths`` option was
21+
removed (use ``--format='$path'`` instead). :bug:`6183`
1522
- :ref:`import-cmd`: When importing an archive (zip, tar, rar, or 7z) with
1623
``move: yes``, the source archive is now removed after a successful import.
1724
Archives are preserved if any file in the archive was not imported (e.g.

docs/plugins/smartplaylist.rst

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,17 @@ You can also use this plugin together with the :doc:`mpdupdate`, in order to
9595
automatically notify MPD of the playlist change, by adding ``mpdupdate`` to the
9696
``plugins`` line in your config file *after* the ``smartplaylist`` plugin.
9797

98-
While changing existing playlists in the beets configuration it can help to use
99-
the ``--pretend`` option to find out if the edits work as expected. The results
100-
of the queries will be printed out instead of being written to the playlist
101-
file.
98+
While working on smart playlist queries in the beets configuration it can help
99+
to use the ``--pretend`` option to find out if the edits work as expected before
100+
writing changes. To list the tracks matching the query, switch to the DEBUG log
101+
level.
102102

103103
::
104104

105-
$ beet splupdate --pretend BeatlesUniverse.m3u
105+
$ beet -v splupdate --pretend BeatlesUniverse.m3u
106106

107-
The ``pretend_paths`` configuration option sets whether the items should be
108-
displayed as per the user's ``format_item`` setting or what the file paths as
109-
they would be written to the m3u file look like.
107+
The ``format`` configuration option defaults to ``$artist - $title``, and can be
108+
set up to a custom format string.
110109

111110
In case you want to export additional fields from the beets database into the
112111
generated playlists, you can do so by specifying them within the ``fields``
@@ -168,8 +167,6 @@ options are:
168167
example, you could use the URL for a server where the music is stored.
169168
Default: empty string.
170169
- **urlencode**: URL-encode all paths. Default: ``no``.
171-
- **pretend_paths**: When running with ``--pretend``, show the actual file paths
172-
that will be written to the m3u file. Default: ``false``.
173170
- **uri_format**: Template with an ``$id`` placeholder used generate a playlist
174171
item URI, e.g. ``http://beets:8337/item/$id/file``. When this option is
175172
specified, the local path-related options ``dest_regen``, ``prefix``,
@@ -179,9 +176,11 @@ options are:
179176
playlist. This allows using e.g. the ``id`` field within other tools such as
180177
the webm3u_ and Beetstream_ plugins. To use this option, you must set the
181178
``output`` option to ``extm3u``.
179+
- **format**: Specify the format a playlist item is logged in ``DEBUG`` level.
180+
Default: ``$artist - $title``.
182181

183182
For many configuration options, there is a corresponding CLI option, e.g.
184183
``--playlist-dir``, ``--dest-regen``, ``--relative-to``, ``--prefix``,
185184
``--forward-slash``, ``--urlencode``, ``--uri-format``, ``--output``,
186-
``--pretend-paths``. CLI options take precedence over those specified within the
185+
``--format``. CLI options take precedence over those specified within the
187186
configuration file.

test/plugins/test_smartplaylist.py

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

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

17-
from os import path, remove
17+
import os
1818
from pathlib import Path
1919
from shutil import rmtree
2020
from tempfile import mkdtemp
@@ -171,9 +171,9 @@ def test_playlist_update(self):
171171
spl = SmartPlaylistPlugin()
172172

173173
i = Mock(path=b"/tagada.mp3")
174-
i.evaluate_template.side_effect = lambda pl, _: pl.replace(
175-
b"$title", b"ta:ga:da"
176-
).decode()
174+
i.evaluate_template.side_effect = lambda pl, *_: os.fsdecode(
175+
pl
176+
).replace("$title", "ta:ga:da")
177177

178178
lib = Mock()
179179
lib.replacements = CHAR_REPLACE
@@ -212,10 +212,9 @@ def test_playlist_update_output_extm3u(self):
212212
type(i).title = PropertyMock(return_value="fake title")
213213
type(i).length = PropertyMock(return_value=300.123)
214214
type(i).path = PropertyMock(return_value=b"/tagada.mp3")
215-
i.evaluate_template.side_effect = lambda pl, _: pl.replace(
216-
b"$title",
217-
b"ta:ga:da",
218-
).decode()
215+
i.evaluate_template.side_effect = lambda pl, *_: os.fsdecode(
216+
pl
217+
).replace("$title", "ta:ga:da")
219218

220219
lib = Mock()
221220
lib.replacements = CHAR_REPLACE
@@ -262,10 +261,9 @@ def test_playlist_update_output_extm3u_fields(self):
262261
type(i).path = PropertyMock(return_value=b"/tagada.mp3")
263262
a = {"id": 456, "genres": ["Rock", "Pop"]}
264263
i.__getitem__.side_effect = a.__getitem__
265-
i.evaluate_template.side_effect = lambda pl, _: pl.replace(
266-
b"$title",
267-
b"ta:ga:da",
268-
).decode()
264+
i.evaluate_template.side_effect = lambda pl, *_: os.fsdecode(
265+
pl
266+
).replace("$title", "ta:ga:da")
269267

270268
lib = Mock()
271269
lib.replacements = CHAR_REPLACE
@@ -308,9 +306,9 @@ def test_playlist_update_uri_format(self):
308306
i = MagicMock()
309307
type(i).id = PropertyMock(return_value=3)
310308
type(i).path = PropertyMock(return_value=b"/tagada.mp3")
311-
i.evaluate_template.side_effect = lambda pl, _: pl.replace(
312-
b"$title", b"ta:ga:da"
313-
).decode()
309+
i.evaluate_template.side_effect = lambda pl, *_: os.fsdecode(
310+
pl
311+
).replace("$title", "ta:ga:da")
314312

315313
lib = Mock()
316314
lib.replacements = CHAR_REPLACE
@@ -471,10 +469,9 @@ def test_playlist_update_dest_regen(self):
471469
)
472470
# Set a path which would be equal to the one returned by `item.destination`.
473471
type(i).destination = PropertyMock(return_value=lambda: b"/tagada.mp3")
474-
i.evaluate_template.side_effect = lambda pl, _: pl.replace(
475-
b"$title",
476-
b"ta:ga:da",
477-
).decode()
472+
i.evaluate_template.side_effect = lambda pl, *_: os.fsdecode(
473+
pl
474+
).replace("$title", "ta:ga:da")
478475

479476
lib = Mock()
480477
lib.replacements = CHAR_REPLACE
@@ -564,15 +561,15 @@ def test_splupdate(self):
564561
m3u_path = self.temp_dir_path / "my_playlist.m3u"
565562
assert m3u_path.exists()
566563
assert m3u_path.read_bytes() == self.item.path + b"\n"
567-
remove(syspath(m3u_path))
564+
os.remove(syspath(m3u_path))
568565

569566
self.run_with_output("splupdate", "my_playlist.m3u")
570567
assert m3u_path.read_bytes() == self.item.path + b"\n"
571-
remove(syspath(m3u_path))
568+
os.remove(syspath(m3u_path))
572569

573570
self.run_with_output("splupdate")
574571
for name in (b"my_playlist.m3u", b"all.m3u"):
575-
with open(path.join(self.temp_dir, name), "rb") as f:
572+
with open(os.path.join(self.temp_dir, name), "rb") as f:
576573
assert f.read() == self.item.path + b"\n"
577574

578575
def test_splupdate_unknown_playlist_error_is_sorted_and_quoted(self):
@@ -591,3 +588,31 @@ def test_splupdate_unknown_playlist_error_is_sorted_and_quoted(self):
591588
"No playlist matching any of "
592589
"'a one.m3u' 'rock'\"'\"'n roll.m3u' 'z last.m3u' found"
593590
)
591+
592+
def test_splupdate_log_output(self):
593+
with self.assertLogs("beets.smartplaylist", level="INFO") as logs:
594+
self.run_with_output("splupdate", "my_playlist")
595+
596+
output = "\n".join(logs.output)
597+
assert "Updating 1 smart playlists..." in output
598+
assert "Creating playlist my_playlist.m3u: 1 tracks." in output
599+
assert "1 playlists updated" in output
600+
601+
def test_splupdate_verbose_log_output(self):
602+
with self.assertLogs("beets.smartplaylist", level="DEBUG") as logs:
603+
self.run_with_output("splupdate", "my_playlist")
604+
605+
output = "\n".join(logs.output)
606+
assert "Updating 1 smart playlists..." in output
607+
assert "Creating playlist my_playlist.m3u: 1 tracks." in output
608+
assert "the ärtist - " in output
609+
assert "1 playlists updated" in output
610+
611+
def test_splupdate_pretend_log_output(self):
612+
with self.assertLogs("beets.smartplaylist", level="INFO") as logs:
613+
self.run_with_output("splupdate", "--pretend", "my_playlist")
614+
615+
output = "\n".join(logs.output)
616+
assert "Updating 1 smart playlists..." in output
617+
assert "Creating playlist my_playlist.m3u: 1 tracks." in output
618+
assert "1 playlists would be updated" in output

0 commit comments

Comments
 (0)