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
76 changes: 41 additions & 35 deletions beetsplug/smartplaylist.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from beets.plugins import send as send_event
from beets.util import (
bytestring_path,
displayable_path,
mkdirall,
normpath,
path_as_posix,
Expand Down Expand Up @@ -64,7 +63,7 @@ def __init__(self) -> None:
"forward_slash": False,
"prefix": "",
"urlencode": False,
"pretend_paths": False,
"format": "$artist - $title",
"output": "m3u",
}
)
Expand All @@ -89,23 +88,26 @@ def commands(self) -> list[ui.Subcommand]:
help="display query results but don't write playlist files.",
)
spl_update.parser.add_option(
"--pretend-paths",
action="store_true",
dest="pretend_paths",
help="in pretend mode, log the playlist item URIs/paths.",
"-f",
"--format",
type="string",
default=self.config["format"].get(),
help="print per-track log lines with custom format",
Comment thread
snejus marked this conversation as resolved.
)
spl_update.parser.add_option(
"-d",
"--playlist-dir",
dest="playlist_dir",
metavar="PATH",
type="string",
default=self.config["playlist_dir"].get(),
help="directory to write the generated playlist files to.",
)
spl_update.parser.add_option(
"--dest-regen",
action="store_true",
dest="dest_regen",
default=self.config["dest_regen"].get(bool),
help="regenerate the destination path as 'move' or 'convert' "
"commands would do.",
)
Expand All @@ -114,33 +116,39 @@ def commands(self) -> list[ui.Subcommand]:
dest="relative_to",
metavar="PATH",
type="string",
default=self.config["relative_to"].get(),
help="generate playlist item paths relative to this path.",
)
spl_update.parser.add_option(
"--prefix",
type="string",
default=self.config["prefix"].get(),
help="prepend string to every path in the playlist file.",
)
spl_update.parser.add_option(
"--forward-slash",
action="store_true",
dest="forward_slash",
default=self.config["forward_slash"].get(bool),
help="force forward slash in paths within playlists.",
)
spl_update.parser.add_option(
"--urlencode",
action="store_true",
default=self.config["urlencode"].get(bool),
help="URL-encode all paths.",
)
spl_update.parser.add_option(
"--uri-format",
dest="uri_format",
type="string",
default=self.config["uri_format"].get(),
help="playlist item URI template, e.g. http://beets:8337/item/$id/file.",
)
spl_update.parser.add_option(
"--output",
type="string",
default=self.config["output"].get(),
help="specify the playlist format: m3u|extm3u.",
)
spl_update.func = self.update_cmd
Expand Down Expand Up @@ -172,14 +180,9 @@ def update_cmd(self, lib: Library, opts: Any, args: list[str]) -> None:
else:
self._matched_playlists = self._unmatched_playlists

self.__apply_opts_to_config(opts)
self.config.set(vars(opts))
self.update_playlists(lib, opts.pretend)

def __apply_opts_to_config(self, opts: Any) -> None:
for k, v in opts.__dict__.items():
if v is not None and k in self.config:
self.config[k] = v

def _parse_one_query(
self, playlist: dict[str, Any], key: str, model_cls: type
) -> tuple[PlaylistQuery, Sort | None]:
Expand Down Expand Up @@ -263,35 +266,29 @@ def db_change(self, lib: Library, model: Item | Album) -> None:
self._unmatched_playlists -= self._matched_playlists

def update_playlists(self, lib: Library, pretend: bool = False) -> None:
if pretend:
self._log.info(
"Showing query results for {} smart playlists...",
len(self._matched_playlists),
)
else:
self._log.info(
"Updating {} smart playlists...", len(self._matched_playlists)
)
self._log.info(
"Updating {} smart playlists...",
len(self._matched_playlists),
)

playlist_dir = bytestring_path(
self.config["playlist_dir"].as_filename()
)
tpl = self.config["uri_format"].get()
prefix = bytestring_path(self.config["prefix"].as_str())
dest_regen = self.config["dest_regen"].get()
dest_regen = self.config["dest_regen"].get(bool)
relative_to = self.config["relative_to"].get()
if relative_to:
relative_to = normpath(relative_to)

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

for playlist in self._matched_playlists:
matched_count = 0
name, (query, q_sort), (album_query, a_q_sort) = playlist
if pretend:
self._log.info("Results for playlist {}:", name)
else:
self._log.info("Creating playlist {}", name)
items = []

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

# As we allow tags in the m3u names, we'll need to iterate through
# the items and generate the correct m3u file names.
matched_items: list[tuple[Any, Any]] = []
for item in items:
m3u_name = item.evaluate_template(name, True)
m3u_name = sanitize_path(m3u_name, lib.replacements)
if m3u_name not in m3us:
m3us[m3u_name] = []
m3u_uris_by_name[m3u_name] = set()
item_uri = item.path
if tpl:
item_uri = tpl.replace("$id", str(item.id)).encode("utf-8")
Expand All @@ -334,20 +333,27 @@ def update_playlists(self, lib: Library, pretend: bool = False) -> None:
item_uri = item.destination()
if relative_to:
item_uri = os.path.relpath(item_uri, relative_to)
if self.config["forward_slash"].get():
if self.config["forward_slash"].get(bool):
item_uri = path_as_posix(item_uri)
if self.config["urlencode"]:
if self.config["urlencode"].get(bool):
item_uri = bytestring_path(
pathname2url(os.fsdecode(item_uri))
)
item_uri = prefix + item_uri

if item_uri not in m3us[m3u_name]:
if item_uri not in m3u_uris_by_name[m3u_name]:
m3u_uris_by_name[m3u_name].add(item_uri)
m3us[m3u_name].append(PlaylistItem(item, item_uri))
if pretend and self.config["pretend_paths"]:
print(displayable_path(item_uri))
elif pretend:
print(item)
matched_items.append((item, item_uri))
matched_count += 1

self._log.info(
"Creating playlist {}: {} tracks.", name, matched_count
)
for item, item_uri in matched_items:
self._log.debug(
item.evaluate_template(self.config["format"].as_str())
)

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

if pretend:
self._log.info(
"Displayed results for {} playlists",
"{} playlists would be updated",
len(self._matched_playlists),
)
else:
Expand Down
13 changes: 10 additions & 3 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ below!
Unreleased
----------

..
New features
~~~~~~~~~~~~
New features
~~~~~~~~~~~~

- :doc:`plugins/smartplaylist`: The ``splupdate`` command output is
restructured. The per-playlist summary now includes a track count. Per-track
details are shown only when ``-v`` flag is provided (``beet -v splupdate``).
The ``--pretend`` flag produces the same output but reports *"N playlists
would be updated"* instead of *"N playlists updated"*. The ``--format`` option
allows customizing the track line format. The ``--pretend-paths`` option was
removed (use ``--format='$path'`` instead). :bug:`6183`

..
Bug fixes
Expand Down
21 changes: 10 additions & 11 deletions docs/plugins/smartplaylist.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,17 @@ You can also use this plugin together with the :doc:`mpdupdate`, in order to
automatically notify MPD of the playlist change, by adding ``mpdupdate`` to the
``plugins`` line in your config file *after* the ``smartplaylist`` plugin.

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

::

$ beet splupdate --pretend BeatlesUniverse.m3u
$ beet -v splupdate --pretend BeatlesUniverse.m3u
Comment thread
snejus marked this conversation as resolved.

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.
The ``format`` configuration option defaults to ``$artist - $title``, and can be
set up to a custom format string.

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``
Expand Down Expand Up @@ -168,8 +167,6 @@ options are:
example, you could use the URL for a server where the music is stored.
Default: empty string.
- **urlencode**: URL-encode all paths. Default: ``no``.
- **pretend_paths**: When running with ``--pretend``, show the actual file paths
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 ``dest_regen``, ``prefix``,
Expand All @@ -179,9 +176,11 @@ options are:
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``.
- **format**: Specify the format a playlist item is logged in ``DEBUG`` level.
Default: ``$artist - $title``.

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