From bcc8903036bb4e3a28ab85e5896da7777294ef33 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 21 Aug 2022 10:27:31 -0700 Subject: [PATCH] Refactor query utilities We now use somewhat more general query constructors in `dbcore`, avoiding the need for somewhat special-purpose `duplicates` methods on the model objects. --- beets/dbcore/db.py | 26 ++++++++++++++++++-------- beets/importer.py | 24 ++++++++++++++++-------- beets/library.py | 10 ---------- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 5a5c50057f..c181f7b335 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -27,7 +27,7 @@ from beets.util import functemplate from beets.util import py3_path from beets.dbcore import types -from .query import MatchQuery, NullSort, TrueQuery +from .query import MatchQuery, NullSort, TrueQuery, AndQuery from collections.abc import Mapping @@ -641,14 +641,24 @@ def set_parse(self, key, string): """ self[key] = self._parse(key, string) + # Convenient queries. + + @classmethod + def field_query(cls, field, pattern, query_cls=MatchQuery): + """Get a `FieldQuery` for this model.""" + return query_cls(field, pattern, field in cls._fields) + @classmethod - def construct_match_queries(cls, **info): - subqueries = [] - for key, value in info.items(): - # Use slow queries for flexible attributes. - fast = key in cls._fields - subqueries.append(MatchQuery(key, value, fast)) - return subqueries + def all_fields_query(cls, pats, query_cls=MatchQuery): + """Get a query that matches many fields with different patterns. + + `pats` should be a mapping from field names to patterns. The + resulting query is a conjunction ("and") of per-field queries + for all of these field/pattern pairs. + """ + subqueries = [cls.field_query(k, v, query_cls) + for k, v in pats.items()] + return AndQuery(subqueries) # Database controller and supporting interfaces. diff --git a/beets/importer.py b/beets/importer.py index 321ea63004..608cf102b2 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -675,16 +675,20 @@ def find_duplicates(self, lib): # As-is import with no artist. Skip check. return [] - # Create a temporary Album so computed fields are available for - # duplicate detection. + # Construct a query to find duplicates with this metadata. We + # use a temporary Album object to generate any computed fields. tmp_album = library.Album(lib, **info) + keys = config['import']['duplicate_keys']['album'].as_str_seq() + dup_query = library.Album.all_fields_query({ + key: tmp_album.get(key) + for key in keys + }) # Don't count albums with the same files as duplicates. task_paths = {i.path for i in self.items if i} duplicates = [] - keys = config['import']['duplicate_keys']['album'].as_str_seq() - for album in tmp_album.duplicates(*keys): + for album in lib.albums(dup_query): # Check whether the album paths are all present in the task # i.e. album is being completely re-imported by the task, # in which case it is not a duplicate (will be replaced). @@ -930,16 +934,20 @@ def find_duplicates(self, lib): """ info = self.chosen_info() - # Use a temporary Item to provide computed fields. + # Query for existing items using the same metadata. We use a + # temporary `Item` object to generate any computed fields. tmp_item = library.Item(lib, **info) + keys = config['import']['duplicate_keys']['single'].as_str_seq() + dup_query = library.Album.all_fields_query({ + key: tmp_item.get(key) + for key in keys + }) found_items = [] - keys = config['import']['duplicate_keys']['single'].as_str_seq() - for other_item in tmp_item.duplicates(*keys): + for other_item in lib.items(dup_query): # Existing items not considered duplicates. if other_item.path != self.item.path: found_items.append(other_item) - return found_items duplicate_items = find_duplicates diff --git a/beets/library.py b/beets/library.py index 9442a3647a..69fcd34cfa 100644 --- a/beets/library.py +++ b/beets/library.py @@ -607,11 +607,6 @@ def from_path(cls, path): i.mtime = i.current_mtime() # Initial mtime. return i - def duplicates(self, *keys): - info = {key: self.get(key) for key in keys} - subqueries = self.construct_match_queries(**info) - return self._db.items(dbcore.AndQuery(subqueries)) - def __setitem__(self, key, value): """Set the item's value for a standard field or a flexattr.""" # Encode unicode paths and read buffers. @@ -1147,11 +1142,6 @@ def _getters(cls): getters['albumtotal'] = Album._albumtotal return getters - def duplicates(self, *keys): - info = {key: self.get(key) for key in keys} - subqueries = self.construct_match_queries(**info) - return self._db.albums(dbcore.AndQuery(subqueries)) - def items(self): """Return an iterable over the items associated with this album.