diff --git a/beets/test/helper.py b/beets/test/helper.py index 367d2b5af..124063d76 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -503,12 +503,8 @@ class PluginMixin: Album._queries = getattr(Album, "_original_queries", {}) @contextmanager - def configure_plugin(self, config: list[Any] | dict[str, Any]): - if isinstance(config, list): - beets.config[self.plugin] = config - else: - for key, value in config.items(): - beets.config[self.plugin][key] = value + def configure_plugin(self, config: Any): + beets.config[self.plugin].set(config) self.load_plugins(self.plugin) yield diff --git a/beetsplug/substitute.py b/beetsplug/substitute.py index 94b790075..a89d0af16 100644 --- a/beetsplug/substitute.py +++ b/beetsplug/substitute.py @@ -34,8 +34,7 @@ class Substitute(BeetsPlugin): """Do the actual replacing.""" if text: for pattern, replacement in self.substitute_rules: - if pattern.match(text.lower()): - return replacement + text = pattern.sub(replacement, text) return text else: return "" @@ -47,10 +46,8 @@ class Substitute(BeetsPlugin): substitute rules. """ super().__init__() - self.substitute_rules = [] self.template_funcs["substitute"] = self.tmpl_substitute - - for key, view in self.config.items(): - value = view.as_str() - pattern = re.compile(key.lower()) - self.substitute_rules.append((pattern, value)) + self.substitute_rules = [ + (re.compile(key, flags=re.IGNORECASE), value) + for key, value in self.config.flatten().items() + ] diff --git a/docs/changelog.rst b/docs/changelog.rst index 9642fd06c..88fddfbbb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,6 +31,9 @@ New features: * Beets now uses ``platformdirs`` to determine the default music directory. This location varies between systems -- for example, users can configure it on Unix systems via ``user-dirs.dirs(5)``. +* :doc:`/plugins/substitute`: Allow the replacement string to use capture groups + from the match. It is thus possible to create more general rules, applying to + many different artists at once. Bug fixes: diff --git a/docs/plugins/substitute.rst b/docs/plugins/substitute.rst index b443f27ac..87ee2ad45 100644 --- a/docs/plugins/substitute.rst +++ b/docs/plugins/substitute.rst @@ -11,13 +11,34 @@ the ``rewrite`` plugin modifies the metadata, this plugin does not. Enable the ``substitute`` plugin (see :ref:`using-plugins`), then make a ``substitute:`` section in your config file to contain your rules. Each rule consists of a case-insensitive regular expression pattern, and a -replacement value. For example, you might use: +replacement string. For example, you might use: + +.. code-block:: yaml substitute: - .*jimi hendrix.*: Jimi Hendrix + .*jimi hendrix.*: Jimi Hendrix + +The replacement can be an expression utilising the matched regex, allowing us +to create more general rules. Say for example, we want to sort all albums by +multiple artists into the directory of the first artist. We can thus capture +everything before the first ``,``, `` &`` or `` and``, and use this capture +group in the output, discarding the rest of the string. + +.. code-block:: yaml + + substitute: + ^(.*?)(,| &| and).*: \1 + +This would handle all the below cases in a single rule: + + Bob Dylan and The Band -> Bob Dylan + Neil Young & Crazy Horse -> Neil Young + James Yorkston, Nina Persson & The Second Hand Orchestra -> James Yorkston To apply the substitution, you have to call the function ``%substitute{}`` in the paths section. For example: - + +.. code-block:: yaml + paths: - default: %substitute{$albumartist}/$year - $album%aunique{}/$track - $title \ No newline at end of file + default: \%substitute{$albumartist}/$year - $album\%aunique{}/$track - $title diff --git a/test/plugins/test_substitute.py b/test/plugins/test_substitute.py new file mode 100644 index 000000000..48014e231 --- /dev/null +++ b/test/plugins/test_substitute.py @@ -0,0 +1,90 @@ +# This file is part of beets. +# Copyright 2024, Nicholas Boyd Isacsson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Test the substitute plugin regex functionality.""" + +from beets.test.helper import PluginTestCase +from beetsplug.substitute import Substitute + + +class SubstitutePluginTest(PluginTestCase): + plugin = "substitute" + preload_plugin = False + + def run_substitute(self, config, cases): + with self.configure_plugin(config): + for input, expected in cases: + assert Substitute().tmpl_substitute(input) == expected + + def test_simple_substitute(self): + self.run_substitute( + { + "a": "x", + "b": "y", + "c": "z", + }, + [("a", "x"), ("b", "y"), ("c", "z")], + ) + + def test_case_insensitivity(self): + self.run_substitute({"a": "x"}, [("A", "x")]) + + def test_unmatched_input_preserved(self): + self.run_substitute({"a": "x"}, [("c", "c")]) + + def test_regex_to_static(self): + self.run_substitute( + {".*jimi hendrix.*": "Jimi Hendrix"}, + [("The Jimi Hendrix Experience", "Jimi Hendrix")], + ) + + def test_regex_capture_group(self): + self.run_substitute( + {"^(.*?)(,| &| and).*": r"\1"}, + [ + ("King Creosote & Jon Hopkins", "King Creosote"), + ( + "Michael Hurley, The Holy Modal Rounders, Jeffrey Frederick & " + + "The Clamtones", + "Michael Hurley", + ), + ("James Yorkston and the Athletes", "James Yorkston"), + ], + ) + + def test_partial_substitution(self): + self.run_substitute({r"\.": ""}, [("U.N.P.O.C.", "UNPOC")]) + + def test_rules_applied_in_definition_order(self): + self.run_substitute( + { + "a": "x", + "[ab]": "y", + "b": "z", + }, + [ + ("a", "x"), + ("b", "y"), + ], + ) + + def test_rules_applied_in_sequence(self): + self.run_substitute( + {"a": "b", "b": "c", "d": "a"}, + [ + ("a", "c"), + ("b", "c"), + ("d", "a"), + ], + )