mirror of
https://github.com/beetbox/beets.git
synced 2026-03-01 02:34:15 +01:00
Fix multi-value delimiter handling in templates
- Use '\␀' as the DB delimiter while formatting lists with '; ' for templates. - Update DelimitedString parsing to accept both separators: * '\␀' for the values from the DB * '; ' for the rest of parsed values (for example `beet modify genres="eletronic; jazz"`) - Refresh %first docs and tests to reflect multi-value field behavior.
This commit is contained in:
parent
13e978ca0e
commit
9d237d10fc
8 changed files with 69 additions and 27 deletions
|
|
@ -288,25 +288,37 @@ class String(BaseString[str, Any]):
|
|||
|
||||
|
||||
class DelimitedString(BaseString[list[str], list[str]]):
|
||||
"""A list of Unicode strings, represented in-database by a single string
|
||||
r"""A list of Unicode strings, represented in-database by a single string
|
||||
containing delimiter-separated values.
|
||||
|
||||
In template evaluation the list is formatted by joining the values with
|
||||
a fixed '; ' delimiter regardless of the database delimiter. That is because
|
||||
the '\␀' character used for multi-value fields is mishandled on Windows
|
||||
as it contains a backslash character.
|
||||
"""
|
||||
|
||||
model_type = list[str]
|
||||
fmt_delimiter = "; "
|
||||
|
||||
def __init__(self, delimiter: str):
|
||||
self.delimiter = delimiter
|
||||
def __init__(self, db_delimiter: str):
|
||||
self.db_delimiter = db_delimiter
|
||||
|
||||
def format(self, value: list[str]):
|
||||
return self.delimiter.join(value)
|
||||
return self.fmt_delimiter.join(value)
|
||||
|
||||
def parse(self, string: str):
|
||||
if not string:
|
||||
return []
|
||||
return string.split(self.delimiter)
|
||||
|
||||
delimiter = (
|
||||
self.db_delimiter
|
||||
if self.db_delimiter in string
|
||||
else self.fmt_delimiter
|
||||
)
|
||||
return string.split(delimiter)
|
||||
|
||||
def to_sql(self, model_value: list[str]):
|
||||
return self.delimiter.join(model_value)
|
||||
return self.db_delimiter.join(model_value)
|
||||
|
||||
|
||||
class Boolean(Type):
|
||||
|
|
@ -464,7 +476,7 @@ NULL_FLOAT = NullFloat()
|
|||
STRING = String()
|
||||
BOOLEAN = Boolean()
|
||||
DATE = DateType()
|
||||
SEMICOLON_SPACE_DSV = DelimitedString(delimiter="; ")
|
||||
SEMICOLON_SPACE_DSV = DelimitedString("; ")
|
||||
|
||||
# Will set the proper null char in mediafile
|
||||
MULTI_VALUE_DSV = DelimitedString(delimiter="\\␀")
|
||||
MULTI_VALUE_DSV = DelimitedString("\\␀")
|
||||
|
|
|
|||
|
|
@ -1546,8 +1546,8 @@ class DefaultTemplateFunctions:
|
|||
s: the string
|
||||
count: The number of items included
|
||||
skip: The number of items skipped
|
||||
sep: the separator. Usually is '; ' (default) or '/ '
|
||||
join_str: the string which will join the items, default '; '.
|
||||
sep: the separator
|
||||
join_str: the string which will join the items
|
||||
"""
|
||||
skip = int(skip)
|
||||
count = skip + int(count)
|
||||
|
|
|
|||
|
|
@ -21,9 +21,13 @@ Unreleased
|
|||
For plugin developers
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
..
|
||||
Other changes
|
||||
~~~~~~~~~~~~~
|
||||
Other changes
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
- :ref:`modify-cmd`: Use the following separator to delimite multiple field
|
||||
values: |semicolon_space|. For example ``beet modify albumtypes="album; ep"``.
|
||||
Previously, ``\␀`` was used as a separator. This applies to fields such as
|
||||
``artists``, ``albumtypes`` etc.
|
||||
|
||||
2.6.2 (February 22, 2026)
|
||||
-------------------------
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ man_pages = [
|
|||
]
|
||||
|
||||
# Global substitutions that can be used anywhere in the documentation.
|
||||
rst_epilog = """
|
||||
rst_epilog = r"""
|
||||
.. |Album| replace:: :class:`~beets.library.models.Album`
|
||||
.. |AlbumInfo| replace:: :class:`beets.autotag.hooks.AlbumInfo`
|
||||
.. |BeetsPlugin| replace:: :class:`beets.plugins.BeetsPlugin`
|
||||
|
|
@ -108,6 +108,7 @@ rst_epilog = """
|
|||
.. |Library| replace:: :class:`~beets.library.library.Library`
|
||||
.. |Model| replace:: :class:`~beets.dbcore.db.Model`
|
||||
.. |TrackInfo| replace:: :class:`beets.autotag.hooks.TrackInfo`
|
||||
.. |semicolon_space| replace:: :literal:`; \ `
|
||||
"""
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -267,6 +267,9 @@ Values can also be *templates*, using the same syntax as :doc:`path formats
|
|||
artist sort name into the artist field for all your tracks, and ``beet modify
|
||||
title='$track $title'`` will add track numbers to their title metadata.
|
||||
|
||||
To adjust a multi-valued field, such as ``genres``, separate the values with
|
||||
|semicolon_space|. For example, ``beet modify genres="rock; pop"``.
|
||||
|
||||
The ``-a`` option changes to querying album fields instead of track fields and
|
||||
also enables to operate on albums in addition to the individual tracks. Without
|
||||
this flag, the command will only change *track-level* data, even if all the
|
||||
|
|
|
|||
|
|
@ -75,11 +75,34 @@ These functions are built in to beets:
|
|||
- ``%time{date_time,format}``: Return the date and time in any format accepted
|
||||
by strftime_. For example, to get the year some music was added to your
|
||||
library, use ``%time{$added,%Y}``.
|
||||
- ``%first{text}``: Returns the first item, separated by ``;`` (a semicolon
|
||||
followed by a space). You can use ``%first{text,count,skip}``, where ``count``
|
||||
is the number of items (default 1) and ``skip`` is number to skip (default 0).
|
||||
You can also use ``%first{text,count,skip,sep,join}`` where ``sep`` is the
|
||||
separator, like ``;`` or ``/`` and join is the text to concatenate the items.
|
||||
- ``%first{text,count,skip,sep,join}``: Extract a subset of items from a
|
||||
delimited string. Splits ``text`` by ``sep``, skips the first ``skip`` items,
|
||||
then returns the next ``count`` items joined by ``join``.
|
||||
|
||||
This is especially useful for multi-valued fields like ``artists`` or
|
||||
``genres`` where you may only want the first artist or a limited number of
|
||||
genres in a path.
|
||||
|
||||
Defaults:
|
||||
|
||||
..
|
||||
Comically, you need to follow |semicolon_space| with some punctuation to
|
||||
make sure it gets rendered correctly as '; ' in the docs.
|
||||
|
||||
- **count**: 1,
|
||||
- **skip**: 0,
|
||||
- **sep**: |semicolon_space|,
|
||||
- **join**: |semicolon_space|.
|
||||
|
||||
Examples:
|
||||
|
||||
::
|
||||
|
||||
%first{$genres} → returns the first genre
|
||||
%first{$genres,2} → returns the first two genres, joined by "; "
|
||||
%first{$genres,2,1} → skips the first genre, returns the next two
|
||||
%first{$genres,2,0, , -> } → splits by space, joins with " -> "
|
||||
|
||||
- ``%ifdef{field}``, ``%ifdef{field,truetext}`` or
|
||||
``%ifdef{field,truetext,falsetext}``: Checks if an flexible attribute
|
||||
``field`` is defined. If it exists, then return ``truetext`` or ``field``
|
||||
|
|
|
|||
|
|
@ -688,14 +688,14 @@ class DestinationFunctionTest(BeetsTestCase, PathFormattingMixin):
|
|||
self._assert_dest(b"/base/not_played")
|
||||
|
||||
def test_first(self):
|
||||
self.i.genres = "Pop; Rock; Classical Crossover"
|
||||
self._setf("%first{$genres}")
|
||||
self._assert_dest(b"/base/Pop")
|
||||
self.i.albumtypes = ["album", "compilation"]
|
||||
self._setf("%first{$albumtypes}")
|
||||
self._assert_dest(b"/base/album")
|
||||
|
||||
def test_first_skip(self):
|
||||
self.i.genres = "Pop; Rock; Classical Crossover"
|
||||
self._setf("%first{$genres,1,2}")
|
||||
self._assert_dest(b"/base/Classical Crossover")
|
||||
self.i.albumtype = "album; ep; compilation"
|
||||
self._setf("%first{$albumtype,1,2}")
|
||||
self._assert_dest(b"/base/compilation")
|
||||
|
||||
def test_first_different_sep(self):
|
||||
self._setf("%first{Alice / Bob / Eve,2,0, / , & }")
|
||||
|
|
|
|||
|
|
@ -96,8 +96,7 @@ class TestPluginRegistration(IOMixin, PluginTestCase):
|
|||
item.add(self.lib)
|
||||
|
||||
out = self.run_with_output("ls", "-f", "$multi_value")
|
||||
delimiter = types.MULTI_VALUE_DSV.delimiter
|
||||
assert out == f"one{delimiter}two{delimiter}three\n"
|
||||
assert out == "one; two; three\n"
|
||||
|
||||
|
||||
class PluginImportTestCase(ImportHelper, PluginTestCase):
|
||||
|
|
|
|||
Loading…
Reference in a new issue