Repository: beetbox/beets

Issue:
remove [] from %aunique
%aunique in a path will by default add [] around the disambiguator.  If a disambiguator is found on one album but is empty on the other, then the album name will end with [].

There may be a reason to have the brackets as default, but it seems to me a better option for the user to set any characters wanted around the disambiguator string in the path format itself.

I can do a PR, but I figured it would be easier just to post here and discuss if necessary.  Here's the line of code:

https://github.com/beetbox/beets/blob/958ad430fc4854b2589ec3a2726b7ee269e5322e/beets/library.py#L1511


Discussion:
Sounds reasonable! How would you propose to set up the option? As an additional argument to the %aunique function?
I simply meant to remove the [] from the code, and then anyone can do, for example, this in the config:

`$artist/$album[%aunique{}]/$track. $title` or `$artist/$album(%aunique{})...`

Would that work?

Alas, not quite—the reason the brackets are hard-coded is so they don't show up if no disambiguation is necessary. Those examples would put empty () and [] pairs at the end of every album directory.
Of course, that's exactly what I wanted to avoid, and I made it worse!

I'll take a look and see about how to do this within the function.

Resolving patch:
diff --git a/beets/library.py b/beets/library.py
--- a/beets/library.py
+++ b/beets/library.py
@@ -1447,13 +1447,15 @@ def tmpl_time(s, fmt):
         cur_fmt = beets.config['time_format'].as_str()
         return time.strftime(fmt, time.strptime(s, cur_fmt))
 
-    def tmpl_aunique(self, keys=None, disam=None):
+    def tmpl_aunique(self, keys=None, disam=None, bracket=None):
         """Generate a string that is guaranteed to be unique among all
         albums in the library who share the same set of keys. A fields
         from "disam" is used in the string if one is sufficient to
         disambiguate the albums. Otherwise, a fallback opaque value is
         used. Both "keys" and "disam" should be given as
-        whitespace-separated lists of field names.
+        whitespace-separated lists of field names, while "bracket" is a
+        pair of characters to be used as brackets surrounding the
+        disambiguator or empty to have no brackets.
         """
         # Fast paths: no album, no item or library, or memoized value.
         if not self.item or not self.lib:
@@ -1467,9 +1469,20 @@ def tmpl_aunique(self, keys=None, disam=None):
 
         keys = keys or 'albumartist album'
         disam = disam or 'albumtype year label catalognum albumdisambig'
+        if bracket is None:
+            bracket = '[]'
         keys = keys.split()
         disam = disam.split()
 
+        # Assign a left and right bracket or leave blank if argument is empty.
+        if len(bracket) == 2:
+            bracket = list(bracket)
+            bracket_l = bracket[0]
+            bracket_r = bracket[1]
+        else:
+            bracket_l = u''
+            bracket_r = u''
+
         album = self.lib.get_album(self.item)
         if not album:
             # Do nothing for singletons.
@@ -1502,13 +1515,19 @@ def tmpl_aunique(self, keys=None, disam=None):
 
         else:
             # No disambiguator distinguished all fields.
-            res = u' {0}'.format(album.id)
+            res = u' {1}{0}{2}'.format(album.id, bracket_l, bracket_r)
             self.lib._memotable[memokey] = res
             return res
 
         # Flatten disambiguation value into a string.
         disam_value = album.formatted(True).get(disambiguator)
-        res = u' [{0}]'.format(disam_value)
+
+        # Return empty string if disambiguator is empty.
+        if disam_value:
+            res = u' {1}{0}{2}'.format(disam_value, bracket_l, bracket_r)
+        else:
+            res = u''
+
         self.lib._memotable[memokey] = res
         return res
 


Test patch:
diff --git a/test/test_library.py b/test/test_library.py
--- a/test/test_library.py
+++ b/test/test_library.py
@@ -713,8 +713,8 @@ def test_use_fallback_numbers_when_identical(self):
         album2.year = 2001
         album2.store()
 
-        self._assert_dest(b'/base/foo 1/the title', self.i1)
-        self._assert_dest(b'/base/foo 2/the title', self.i2)
+        self._assert_dest(b'/base/foo [1]/the title', self.i1)
+        self._assert_dest(b'/base/foo [2]/the title', self.i2)
 
     def test_unique_falls_back_to_second_distinguishing_field(self):
         self._setf(u'foo%aunique{albumartist album,month year}/$title')
@@ -730,6 +730,24 @@ def test_unique_sanitized(self):
         self._setf(u'foo%aunique{albumartist album,albumtype}/$title')
         self._assert_dest(b'/base/foo [foo_bar]/the title', self.i1)
 
+    def test_drop_empty_disambig_string(self):
+        album1 = self.lib.get_album(self.i1)
+        album1.albumdisambig = None
+        album2 = self.lib.get_album(self.i2)
+        album2.albumdisambig = u'foo'
+        album1.store()
+        album2.store()
+        self._setf(u'foo%aunique{albumartist album,albumdisambig}/$title')
+        self._assert_dest(b'/base/foo/the title', self.i1)
+
+    def test_change_brackets(self):
+        self._setf(u'foo%aunique{albumartist album,year,()}/$title')
+        self._assert_dest(b'/base/foo (2001)/the title', self.i1)
+
+    def test_remove_brackets(self):
+        self._setf(u'foo%aunique{albumartist album,year,}/$title')
+        self._assert_dest(b'/base/foo 2001/the title', self.i1)
+
 
 class PluginDestinationTest(_common.TestCase):
     def setUp(self):