Repository: beetbox/beets

Issue:
rewrite: Clarify the scope of the wide-ranging effects of the plugin, which can sometimes include modifying actual metadata
Hi guys. In the documentation of rewrite plugin it is written that "Note that this plugin only applies to templating; it does not modify files' metadata tags or the values tracked by beets' library database.", but it seems that it modifies the tags.

Without rewrite plugin I import an album by "Buckethead & Friends" and I obtain
```
beet ls enter the chicken --format '$albumartist - $title'
Buckethead & Friends - Intro
Buckethead & Friends - Nottingham Lace
Buckethead & Friends - Coma
Buckethead & Friends - Waiting Hare
Buckethead & Friends - The Hand
Buckethead & Friends - Funbus
Buckethead & Friends - Botnus
Buckethead & Friends - Running From the Light
Buckethead & Friends - We Are One
Buckethead & Friends - Three Fingers
Buckethead & Friends - Interlude
```

But enabling the rewrite plugin with this config file:
```
import:
    move: yes

directory: /media/Esterno/Organizzati

plugins: inline convert	rewrite

rewrite:
    albumartist .*buckethead.*: Buckethead

paths:
    default: $albumartist/$original_year - $album%aunique{}/%if{$multidisc,Disc $disc/}$track - $title
    singleton: $albumartist/$original_year - $album%aunique{}/%if{$multidisc,Disc $disc/}$track - $title
    comp: $albumartist/$original_year - $album%aunique{}/%if{$multidisc,Disc $disc/}$track - $title

item_fields:
    multidisc: 1 if disctotal > 1 else 0

convert:
    formats:
        mp3: ffmpeg -i $source -y -vn -aq 0 $dest
```

I obtain
```
beet ls enter the chicken --format '$albumartist - $title'
Buckethead - Intro
Buckethead - Nottingham Lace
Buckethead - Coma
Buckethead - Waiting Hare
Buckethead - The Hand
Buckethead - Funbus
Buckethead - Botnus
Buckethead - Running From the Light
Buckethead - We Are One
Buckethead - Three Fingers
Buckethead - Interlude
```

And inspecting the tags with easytag, the albumartist is simply Buckethead. I would like to maintain the correct tags, but change the saving path.

Thanks.
  


Discussion:
I took a quick glance at the plugin code, and I believe that the documentation is rather misleading. The way it seems to work, it does indeed not modify the database initially (not entirely sure about the tags).

However the full scope of the rewriting is not well controlled. For example, the database entry will be rewritten if the field is read (this retrieves the modified version) and then stored back without further changes. I guess that would happen when using the `edit` plugin even when the field is not actually changed, but diplayed in the file. For the same reason, `beet ls` also shows the rewritten version even if the database has not been changed.

I'm not sure how to deal with this (I also don't use the plugin, so I'm not exactly familiar with its usecases), but the current behaviour does not appear very sensible to me, given the analysis above is correct (which I did not test). I guess the plugin should really somehow hook into the template formatting process, and not modify field retrieval in general.
Indeed! That's a good summary of the issues, @wisp3rwind. The plugin doesn't just rewrite fields for paths; it rewrites them for *all field lookups in beets*. That is a pretty big hammer.

If anyone's interested in digging deeper and finding a more limited way to accomplish something similar, I'd be happy to help out.
I'm not a Python expert, but I wrote a simple plug-in to manage this situation. Probably can be done better, but this is the code I'm using at the moment:

```
from beets.plugins import BeetsPlugin
import re

_substitute_rules = []

class Substitute(BeetsPlugin):
    def __init__(self):
        super(Substitute, self).__init__()
        self.template_funcs['substitute'] = _tmpl_substitute

        for key, view in self.config.items():
            value = view.as_str()
            pattern = re.compile(key.lower())
            _substitute_rules.append((pattern, value))

def _tmpl_substitute(text):
    if text:
         for pattern, replacement in _substitute_rules:
              if pattern.match(text.lower()):
                  return replacement
         return text
    else:
         return u''
```

in the config file:
```
paths:
  default: %substitute{$albumartist}/$year - $album%aunique{}/%if{$multidisc,Disc $disc/}$track - $title
...
substitute:
    pattern: substitution string
```

Maybe something similar can be added to default plugins...
That’s a cool idea! It’s a nice, compact plugin that would fit in as a `rewrite` alternative. We could totally consider shipping it with beets.
Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

It seems that not a lot has changed about this. The documentation is still misleading, and while I like how the rewrite plugin works right now for most use-cases, it would be good to have an alternative plugin that does what's specified in the docs.

For example, I have recently changed my `replace` config to not replace trailing dot characters with underscores anymore since that isn't necessary on my Linux system.
However, I'd want to keep the location for previously imported artists/artists unchanged because my music player doesn't like file location changes (it resets import dates and breaks playlists).
Thus, I thought about just doing an `artist R.E.M.: R.E.M_` in the `rewrite` section; however, I was pretty surprised that this also changed the tags in the files. Not exactly what I wanted here.

Shipping something like the above plugin would be handy in this case.
Why is this one closed? Situation with plugin behaviour/docs is unchanged.
The main reason is that we didn't settle concretely on a path forward. Maybe we should make this specifically about fixing the docs and reopen it?
I think that would be a good idea. What about the substitute plugin that was suggested above? Do you think that would make sense to have a PR opened for it, if @fdaniele85 agrees?
> I think that would be a good idea. What about the substitute plugin that was suggested above? Do you think that would make sense to have a PR opened for it, if @fdaniele85 agrees?

I agree
> > I think that would be a good idea. What about the substitute plugin that was suggested above? Do you think that would make sense to have a PR opened for it, if @fdaniele85 agrees?
> 
> I agree

That would be great, we will have a plugin that does what is advertised finally :grinning: 

Resolving patch:
diff --git a/beetsplug/substitute.py b/beetsplug/substitute.py
new file mode 100644
--- /dev/null
+++ b/beetsplug/substitute.py
@@ -0,0 +1,54 @@
+# This file is part of beets.
+# Copyright 2023, Daniele Ferone.
+#
+# 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.
+
+"""The substitute plugin module.
+
+Uses user-specified substitution rules to canonicalize names for path formats.
+"""
+
+import re
+from beets.plugins import BeetsPlugin
+
+
+class Substitute(BeetsPlugin):
+    """The substitute plugin class.
+
+    Create a template field function that subsitute the given field with the
+    given substitution rules. ``rules`` must be a list of (pattern,
+    replacement) pairs.
+    """
+    def tmpl_substitute(self, text):
+        """Do the actual replacing."""
+        if text:
+            for pattern, replacement in self.substitute_rules:
+                if pattern.match(text.lower()):
+                    return replacement
+            return text
+        else:
+            return u''
+
+    def __init__(self):
+        """Initialize the substitute plugin.
+
+        Get the configuration, register template function and create list of
+        substitute rules.
+        """
+        super(Substitute, self).__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))


Test patch: