diff --git a/beets/dbcore.py b/beets/dbcore.py index cda9eb0a5..7cd5a2a03 100644 --- a/beets/dbcore.py +++ b/beets/dbcore.py @@ -6,7 +6,6 @@ import sqlite3 import contextlib import beets -from beets import util from beets.util.functemplate import Template @@ -433,29 +432,17 @@ class FieldQuery(Query): """ raise NotImplementedError() - @classmethod - def _raw_value_match(cls, pattern, value): - """Determine whether the value matches the pattern. The value - may have any type. - """ - return cls.value_match(pattern, util.as_string(value)) - def match(self, item): - return self._raw_value_match(self.pattern, item.get(self.field)) + return self.value_match(self.pattern, item.get(self.field)) class MatchQuery(FieldQuery): """A query that looks for exact matches in an item field.""" def col_clause(self): - pattern = self.pattern - if self.field == 'path': - pattern = buffer(util.bytestring_path(pattern)) - return self.field + " = ?", [pattern] + return self.field + " = ?", [self.pattern] - # We override the "raw" version here as a special case because we - # want to compare objects before conversion. @classmethod - def _raw_value_match(cls, pattern, value): + def value_match(cls, pattern, value): return pattern == value diff --git a/beets/importer.py b/beets/importer.py index 9ef949e63..ed2d36088 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -24,6 +24,7 @@ from collections import defaultdict from beets import autotag from beets import library +from beets import dbcore from beets import plugins from beets import util from beets import config @@ -65,7 +66,7 @@ def _duplicate_check(lib, task): found_albums = [] cur_paths = set(i.path for i in task.items if i) - for album_cand in lib.albums(library.MatchQuery('albumartist', artist)): + for album_cand in lib.albums(dbcore.MatchQuery('albumartist', artist)): if album_cand.album == album: # Check whether the album is identical in contents, in which # case it is not a duplicate (will be replaced). @@ -84,8 +85,8 @@ def _item_duplicate_check(lib, task): found_items = [] query = library.AndQuery(( - library.MatchQuery('artist', artist), - library.MatchQuery('title', title), + dbcore.MatchQuery('artist', artist), + dbcore.MatchQuery('title', title), )) for other_item in lib.items(query): # Existing items not considered duplicates. @@ -751,7 +752,7 @@ def apply_choices(session): task.replaced_items = defaultdict(list) for item in items: dup_items = session.lib.items( - library.MatchQuery('path', item.path) + library.BytesQuery('path', item.path) ) for dup_item in dup_items: task.replaced_items[item].append(dup_item) diff --git a/beets/library.py b/beets/library.py index bf1090c0d..a88492641 100644 --- a/beets/library.py +++ b/beets/library.py @@ -696,7 +696,26 @@ class Album(LibModel): # Query abstraction hierarchy. -class SubstringQuery(dbcore.FieldQuery): +class StringFieldQuery(dbcore.FieldQuery): + """A FieldQuery that converts values to strings before matching + them. + """ + @classmethod + def value_match(cls, pattern, value): + """Determine whether the value matches the pattern. The value + may have any type. + """ + return cls.string_match(pattern, util.as_string(value)) + + @classmethod + def string_match(cls, pattern, value): + """Determine whether the value matches the pattern. Both + arguments are strings. Subclasses implement this method. + """ + raise NotImplementedError() + + +class SubstringQuery(StringFieldQuery): """A query that matches a substring in a specific item field.""" def col_clause(self): search = '%' + (self.pattern.replace('\\','\\\\').replace('%','\\%') @@ -706,16 +725,16 @@ class SubstringQuery(dbcore.FieldQuery): return clause, subvals @classmethod - def value_match(cls, pattern, value): + def string_match(cls, pattern, value): return pattern.lower() in value.lower() -class RegexpQuery(dbcore.FieldQuery): +class RegexpQuery(StringFieldQuery): """A query that matches a regular expression in a specific item field. """ @classmethod - def value_match(cls, pattern, value): + def string_match(cls, pattern, value): try: res = re.search(pattern, value) except re.error: @@ -735,6 +754,27 @@ class BooleanQuery(dbcore.MatchQuery): self.pattern = int(self.pattern) +class BytesQuery(dbcore.MatchQuery): + """Match a raw bytes field (i.e., a path). This is a necessary hack + to distinguish between the common case, matching Unicode strings, + and the special case in which we match bytes. + """ + def __init__(self, field, pattern): + super(BytesQuery, self).__init__(field, pattern) + + # Use a buffer representation of the pattern for SQLite + # matching. This instructs SQLite to treat the blob as binary + # rather than encoded Unicode. + if isinstance(self.pattern, bytes): + self.buf_pattern = buffer(self.pattern) + elif isinstance(self.battern, buffer): + self.buf_pattern = self.pattern + self.pattern = bytes(self.pattern) + + def col_clause(self): + return self.field + " = ?", [self.buf_pattern] + + class NumericQuery(dbcore.FieldQuery): """Matches numeric fields. A syntax using Ruby-style range ellipses (``..``) lets users specify one- or two-sided ranges. For example, diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index 1faa5f1b9..ec7e02e63 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -1017,7 +1017,7 @@ class Server(BaseServer): def cmd_find(self, conn, *kv): """Perform an exact match for items.""" - query = self._metadata_query(beets.library.MatchQuery, + query = self._metadata_query(beets.dbcore.MatchQuery, None, kv) for item in self.lib.items(query): @@ -1028,7 +1028,7 @@ class Server(BaseServer): filtered by matching match_tag to match_term. """ show_tag_canon, show_key = self._tagtype_lookup(show_tag) - query = self._metadata_query(beets.library.MatchQuery, None, kv) + query = self._metadata_query(beets.dbcore.MatchQuery, None, kv) clause, subvals = query.clause() statement = 'SELECT DISTINCT ' + show_key + \ @@ -1047,7 +1047,7 @@ class Server(BaseServer): _, key = self._tagtype_lookup(tag) songs = 0 playtime = 0.0 - for item in self.lib.items(beets.library.MatchQuery(key, value)): + for item in self.lib.items(beets.dbcore.MatchQuery(key, value)): songs += 1 playtime += item.length yield u'songs: ' + unicode(songs) diff --git a/beetsplug/fuzzy.py b/beetsplug/fuzzy.py index 21d1c38e2..d8db21668 100644 --- a/beetsplug/fuzzy.py +++ b/beetsplug/fuzzy.py @@ -16,14 +16,14 @@ """ from beets.plugins import BeetsPlugin -from beets.library import FieldQuery +from beets.library import StringFieldQuery import beets import difflib -class FuzzyQuery(FieldQuery): +class FuzzyQuery(StringFieldQuery): @classmethod - def value_match(self, pattern, val): + def string_match(self, pattern, val): # smartcase if pattern.islower(): val = val.lower() diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py index 3b24f75ad..9a88b5778 100644 --- a/beetsplug/mpdstats.py +++ b/beetsplug/mpdstats.py @@ -165,7 +165,7 @@ class MPDStats(object): def get_item(self, path): """Return the beets item related to path. """ - query = library.MatchQuery('path', path) + query = library.BytesQuery('path', path) item = self.lib.items(query).get() if item: return item