Repository: beetbox/beets

Issue:
%aunique fails if a disambiguation is a non-fixed field
I recently encountered this behaviour while trying out paths with `%aunique{}` and the below config.
```yaml
paths:
    default: $albumartist/$albumartist - $album%ifdef{albumdisambig, - $albumdisambig}%aunique{albumartist album year, catalognum label} [$format]/%if{$multidisc,$disc-}$track. $title
```
This will produce the below output (part of).
```text
Franz Ferdinand - Franz Ferdinand [678] [MP3]
Franz Ferdinand - Franz Ferdinand [679] [FLAC]
```
This means that different format, which already produce a different path, still get deduplicated by `%aunique`, which is intended behaviour.
I would, however, want to avoid this and @sampsyo  referred to #2610, from which I slightly adapted this snippet:
```yaml
album_fields:
    albumformat: |
        format_list = []
        [format_list.append(item.format) for item in items if item.format not in format_list]
        return format_list
```
The snippet works for the `beet list` command, however it does not work as a _disambiguator_ for `%aunique`:
```yaml
paths:
    default: $albumartist/$albumartist - $album%ifdef{albumdisambig, - $albumdisambig}%aunique{albumartist album year albumformat, catalognum label} [$format]/%if{$multidisc,$disc-}$track. $title
```
And produces `_no such column∶ format_` in the file name.
I believe this is caused by this piece:
```py
        for key in keys:
            value = album.get(key, '')
            subqueries.append(dbcore.MatchQuery(key, value))
```
found in [`beets/library.py`](https://github.com/beetbox/beets/blob/34246a00e7be8da59ed4e0d659f7b980a5da7910/beets/library.py#L1525-1528).
My understanding is that the `key` gets queried to the database but not with the fields generated by the `inline` plugin. Would it be feasible to allow `inline` fields to be queried?

### Setup

* OS: Raspbian + Docker
* Python version: 3.6.1
* beets version: 1.4.5
* Turning off plugins made problem go away (yes/no): no
Use flexible attributes as aunique keys
### Use case

I would like to be able to use flexible attributes as aunique keys for disambiguation. One use case of this would be when having multiple versions of the same album in different resolutions: 16/44 and 24/96, for example. In this case beets should treat the same album in different resolutions as a different album and thus not add the disambiguators at the end of the album name if this occurs.
### Solution
Something like the following is how it should be implemented:

```
aunique:
    keys: original_year albumartist album album_bitdepth album_samplerate
    disambiguators: albumtype label catalognum albumdisambig releasegroupdisambig
    bracket: '{}'

album_fields:  
      album_bitdepth: |
       total = 0
       for item in items:
           total += item.bitdepth
       return round(total / len(items))
    album_samplerate: |
       total = 0
       for item in items:
           total += item.samplerate
       return round(total / len(items) / 1000)
```

### Alternatives
I [posted a question](https://discourse.beets.io/t/using-custom-fields-as-aunique-keys/1213) in the forums and it was suggested I file a feature request. I don't believe there are any alternatives

### Related issue
https://github.com/beetbox/beets/issues/2678
Use flexible attributes as aunique keys
### Use case

I would like to be able to use flexible attributes as aunique keys for disambiguation. One use case of this would be when having multiple versions of the same album in different resolutions: 16/44 and 24/96, for example. In this case beets should treat the same album in different resolutions as a different album and thus not add the disambiguators at the end of the album name if this occurs.
### Solution
Something like the following is how it should be implemented:

```
aunique:
    keys: original_year albumartist album album_bitdepth album_samplerate
    disambiguators: albumtype label catalognum albumdisambig releasegroupdisambig
    bracket: '{}'

album_fields:  
      album_bitdepth: |
       total = 0
       for item in items:
           total += item.bitdepth
       return round(total / len(items))
    album_samplerate: |
       total = 0
       for item in items:
           total += item.samplerate
       return round(total / len(items) / 1000)
```

### Alternatives
I [posted a question](https://discourse.beets.io/t/using-custom-fields-as-aunique-keys/1213) in the forums and it was suggested I file a feature request. I don't believe there are any alternatives

### Related issue
https://github.com/beetbox/beets/issues/2678


Discussion:
I've found responses like this `_no such column∶ format_` in my own use of aunique(). Have you tried a path format like this?

`%aunique{albumartist album year $albumformat, catalognum label}`

Notice the `$`. You need $ in the first set of disambiguators for any field defined with inline. You need to *not* use $ in the second set of disambiguators. Example:

`%aunique{albumartist album year $albumformat, cust_field catalognum label}`
This is my output after your suggestion:
```sh
$ beet ls -f '$albumartist - $album %aunique{albumartist album year $albumformat, catalognum label} [$format]' album::^Franz Ferdinand
Franz Ferdinand - Franz Ferdinand <no such column: 'MP3'> [MP3]
Franz Ferdinand - Franz Ferdinand <no such column: 'MP3'> [MP3]
Franz Ferdinand - Franz Ferdinand <no such column: 'FLAC'> [FLAC]
```
It could be something related to my setup though.
Thanks for the detailed report. This is in fact a bug—I don't think even a flexible attribute would work as a disambiguator as it stands. The problem is indeed with the MatchQuery construction that you pointed to. Namely, the problem is that the FieldQuery base class constructor defaults to `fast=True`, which only works for fixed fields: https://github.com/beetbox/beets/blob/34246a00e7be8da59ed4e0d659f7b980a5da7910/beets/dbcore/query.py#L102

To read more about fast and slow queries, check out [this blog post](http://beets.io/blog/flexattr.html). We need to pass `True` for the `fast` parameter when the field name is non-fixed.

The right fix is probably a helper function that will automatically choose the fast parameter based on the kind of field being queried.
Thanks!!

For posterity: I linked in the aforementioned forum thread where the change would have to take place. When constructing a MatchQuery, the aunique template function would need to possibly set the `fast` parameter to `False` for non-fixed attributes.
Thanks!!

For posterity: I linked in the aforementioned forum thread where the change would have to take place. When constructing a MatchQuery, the aunique template function would need to possibly set the `fast` parameter to `False` for non-fixed attributes.

Resolving patch:
diff --git a/beets/library.py b/beets/library.py
--- a/beets/library.py
+++ b/beets/library.py
@@ -1690,7 +1690,9 @@ def tmpl_aunique(self, keys=None, disam=None, bracket=None):
         subqueries = []
         for key in keys:
             value = album.get(key, '')
-            subqueries.append(dbcore.MatchQuery(key, value))
+            # Use slow queries for flexible attributes.
+            fast = key in album.item_keys
+            subqueries.append(dbcore.MatchQuery(key, value, fast))
         albums = self.lib.albums(dbcore.AndQuery(subqueries))
 
         # If there's only one album to matching these details, then do


Test patch:
diff --git a/test/test_library.py b/test/test_library.py
--- a/test/test_library.py
+++ b/test/test_library.py
@@ -791,6 +791,16 @@ def test_remove_brackets(self):
         self._setf('foo%aunique{albumartist album,year,}/$title')
         self._assert_dest(b'/base/foo 2001/the title', self.i1)
 
+    def test_key_flexible_attribute(self):
+        album1 = self.lib.get_album(self.i1)
+        album1.flex = 'flex1'
+        album2 = self.lib.get_album(self.i2)
+        album2.flex = 'flex2'
+        album1.store()
+        album2.store()
+        self._setf('foo%aunique{albumartist album flex,year}/$title')
+        self._assert_dest(b'/base/foo/the title', self.i1)
+
 
 class PluginDestinationTest(_common.TestCase):
     def setUp(self):