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
10 changes: 6 additions & 4 deletions beetsplug/advancedrewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from beets.plugins import BeetsPlugin
from beets.ui import UserError

from .rewrite import apply_rewrite_rules


def rewriter(field, simple_rules, advanced_rules):
"""Template field function factory.
Expand All @@ -38,10 +40,10 @@ def rewriter(field, simple_rules, advanced_rules):

def fieldfunc(item):
value = item._values_fixed[field]
for pattern, replacement in simple_rules:
if pattern.match(value.lower()):
# Rewrite activated.
return replacement
if (new_value := apply_rewrite_rules(value, simple_rules)) != value:
# Rewrite activated.
return new_value

Comment thread
snejus marked this conversation as resolved.
for query, replacement in advanced_rules:
if query.match(item):
# Rewrite activated.
Expand Down
40 changes: 33 additions & 7 deletions beetsplug/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,42 @@

import re
from collections import defaultdict
from functools import singledispatch
from typing import Any, TypeVar

from beets import library, ui
from beets.plugins import BeetsPlugin

T = TypeVar("T")


@singledispatch
def rewrite_value(value: Any, pat: re.Pattern[str], repl: str) -> Any:
"""Rewrite a value if it matches the given pattern."""
return value


@rewrite_value.register
def _(value: str, pat: re.Pattern[str], repl: str) -> str:
if pat.match(value.lower()):
return repl
return value


@rewrite_value.register(list)
def _(value: list[str], pat: re.Pattern[str], repl: str) -> list[str]:
return [rewrite_value(v, pat, repl) for v in value]


def apply_rewrite_rules(
value: T, rules: list[tuple[re.Pattern[str], str]]
) -> T:
"""Apply all matching rewrite rules to the given value."""
for pattern, replacement in rules:
value = rewrite_value(value, pattern, replacement)

return value
Comment thread
snejus marked this conversation as resolved.


def rewriter(field, rules):
"""Create a template field function that rewrites the given field
Expand All @@ -30,13 +62,7 @@ def rewriter(field, rules):
"""

def fieldfunc(item):
value = item._values_fixed[field]
for pattern, replacement in rules:
if pattern.match(value.lower()):
# Rewrite activated.
return replacement
# Not activated; return original value.
return value
return apply_rewrite_rules(item._values_fixed[field], rules)

return fieldfunc

Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ Bug fixes
plural field names. :bug:`6483`
- :doc:`plugins/fetchart`: Error when a configured source does not exist or
sources configuration is empty. :bug:`6336`
- :doc:`plugins/rewrite` :doc:`plugins/advancedrewrite`: Fix rewriting
multi-valued fields such as ``genres`` by applying rules to each matching list
entry. Additionally, apply rewrite rules in config order, so that multiple
rules can be applied to the same field. :bug:`6515`

For plugin developers
~~~~~~~~~~~~~~~~~~~~~
Expand Down
3 changes: 3 additions & 0 deletions docs/plugins/advancedrewrite.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ replace a single field:
advancedrewrite:
- artist ODD EYE CIRCLE: 이달의 소녀 오드아이써클

As with :doc:`/plugins/rewrite`, simple rules applied to a multi-valued field
rewrite only the matching list entries.

The advanced syntax consists of a query to match against, as well as a map of
replacements to apply. For example, to credit all songs of ODD EYE CIRCLE before
2023 to their original group name, you can use the following rule:
Expand Down
21 changes: 21 additions & 0 deletions docs/plugins/rewrite.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ might use:
rewrite:
artist .*jimi hendrix.*: Jimi Hendrix

Rules also apply to matching elements in multi-valued fields. For example, this
rewrites the ``rock`` entry in ``genres`` while leaving other genre values
unchanged:

::

rewrite:
genres rock: Classic Rock

Rules are applied in the order they appear in the config. This means a
multi-valued field can have several entries rewritten by different rules:

::

rewrite:
genres rock: Classic Rock
genres pop: Pop

With this configuration, a ``genres`` value such as ``rock; pop; techno`` is
rewritten to ``Classic Rock; Pop; techno``.

As a convenience, the plugin applies patterns for the ``artist`` field to the
``albumartist`` field as well. (Otherwise, you would probably want to duplicate
every rule for ``artist`` and ``albumartist``.)
Expand Down
6 changes: 6 additions & 0 deletions test/plugins/test_advancedrewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ def test_simple_rewrite_example(self):

assert item.artist == "이달의 소녀 오드아이써클"

def test_list_field(self):
with self.configure_plugin([{"genres rock": "techno"}]):
item = self.add_item(genres=["rock", "pop"])

assert item.genres == ["techno", "pop"]
Comment thread
snejus marked this conversation as resolved.

def test_advanced_rewrite_example(self):
with self.configure_plugin(
[
Expand Down
75 changes: 75 additions & 0 deletions test/plugins/test_rewrite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import pytest

from beets.test.helper import PluginTestCase
from beets.ui import UserError
from beetsplug.rewrite import RewritePlugin


class RewritePluginTest(PluginTestCase):
plugin = "rewrite"
preload_plugin = False

def test_artist_rewrite_applies_to_artist_albumartist_and_album_fields(
self,
):
with self.configure_plugin({"artist .*jimi hendrix.*": "Jimi Hendrix"}):
item = self.add_item(
artist="The Jimi Hendrix Experience",
albumartist="The Jimi Hendrix Experience",
)
album = self.lib.add_album([item])

assert item.artist == "Jimi Hendrix"
assert item.albumartist == "Jimi Hendrix"
assert album.evaluate_template("$albumartist") == "Jimi Hendrix"

def test_rewrite_all_matching_rules(self):
with self.configure_plugin(
{
"artist .*hendrix.*": "hendrix catalog",
"artist .*catalog.*": "Experience catalog",
}
):
item = self.add_item(
artist="The Jimi Hendrix Experience",
albumartist="The Jimi Hendrix Experience",
)

assert item.artist == "Experience catalog"

def test_rewrite_is_case_insensitive_and_leaves_non_matches_unchanged(
self,
):
with self.configure_plugin(
{"artist odd eye circle": "LOONA / ODD EYE CIRCLE"}
):
matching_item = self.add_item(
artist="ODD EYE CIRCLE",
albumartist="ODD EYE CIRCLE",
)
other_item = self.add_item(artist="ARTMS", albumartist="ARTMS")

assert matching_item.artist == "LOONA / ODD EYE CIRCLE"
assert other_item.artist == "ARTMS"

def test_rewrite_applied_to_all_list_values(self):
with self.configure_plugin(
{"genres rock": "Classic Rock", "genres pop": "Pop"}
):
item = self.add_item(genres=["rock", "pop", "techno"])

assert item.genres == ["Classic Rock", "Pop", "techno"]

Comment thread
snejus marked this conversation as resolved.
def test_invalid_rewrite_spec_raises_user_error(self):
self.config[self.plugin].set({"artist": "Jimi Hendrix"})

with pytest.raises(UserError, match="invalid rewrite specification"):
RewritePlugin()

def test_invalid_field_name_raises_user_error(self):
self.config[self.plugin].set({"not_a_field rock": "Classic Rock"})

with pytest.raises(
UserError, match="invalid field name \\(not_a_field\\) in rewriter"
):
RewritePlugin()
Loading