Skip to content

Commit

Permalink
Refactor query utilities
Browse files Browse the repository at this point in the history
We now use somewhat more general query constructors in `dbcore`,
avoiding the need for somewhat special-purpose `duplicates` methods on
the model objects.
  • Loading branch information
sampsyo committed Aug 21, 2022
1 parent ca38486 commit bcc8903
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 26 deletions.
26 changes: 18 additions & 8 deletions beets/dbcore/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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.
Expand Down
24 changes: 16 additions & 8 deletions beets/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down
10 changes: 0 additions & 10 deletions beets/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit bcc8903

Please sign in to comment.