singleton: queries

This commit is contained in:
Adrian Sampson 2011-04-09 16:13:12 -07:00
parent 7f1e4c2407
commit d63a9fd188
3 changed files with 52 additions and 12 deletions

3
NEWS
View file

@ -3,7 +3,8 @@
* Better support for singleton (non-album) tracks. The "singleton" path
format can be used to customize where these tracks are stored. While
importing, you can choose the "as Tracks" (T) option to add
singletons to your library.
singletons to your library. The query "singleton:true" matches only
singleton tracks; "singleton:false" matches only album tracks.
* The "distance" number, which quantifies how different an album's
current and proposed metadata are, is now displayed as "similarity"
instead. This should be less noisy and confusing; you'll now see

View file

@ -279,6 +279,13 @@ def _sanitize_for_path(value, pathmod, key=None):
value = str(value)
return value
def _bool(value):
"""Returns a boolean reflecting a human-entered string."""
if value.lower() in ('yes', '1', 'true', 't', 'y'):
return True
else:
return False
# Library items (songs).
@ -453,7 +460,9 @@ class Query(object):
ResultIterator.
"""
c = library.conn.cursor()
c.execute(*self.statement())
stmt, subs = self.statement()
log.debug('Executing query: %s' % stmt)
c.execute(stmt, subs)
return ResultIterator(c, library)
class FieldQuery(Query):
@ -489,6 +498,20 @@ class SubstringQuery(FieldQuery):
def match(self, item):
return self.pattern.lower() in getattr(item, self.field).lower()
class SingletonQuery(Query):
"""Matches either singleton or non-singleton items."""
def __init__(self, sense):
self.sense = sense
def clause(self):
if self.sense:
return "album_id ISNULL", ()
else:
return "NOT album_id ISNULL", ()
def match(self, item):
return (not item.album_id) == self.sense
class CollectionQuery(Query):
"""An abstract query class that aggregates other queries. Can be
indexed like a list to access the sub-queries.
@ -515,16 +538,6 @@ class CollectionQuery(Query):
clause = (' ' + joiner + ' ').join(clause_parts)
return clause, subvals
@classmethod
def from_dict(cls, matches):
"""Construct a query from a dictionary, matches, whose keys are
item field names and whose values are substring patterns.
"""
subqueries = []
for key, pattern in matches.iteritems():
subqueries.append(SubstringQuery(key, pattern))
return cls(subqueries)
# regular expression for _parse_query, below
_pq_regex = re.compile(r'(?:^|(?<=\s))' # zero-width match for whitespace
# or beginning of string
@ -569,6 +582,8 @@ class CollectionQuery(Query):
subqueries.append(AnySubstringQuery(pattern, default_fields))
elif key.lower() in ITEM_KEYS: # ignore unrecognized keys
subqueries.append(SubstringQuery(key.lower(), pattern))
elif key.lower() == 'singleton':
subqueries.append(SingletonQuery(_bool(pattern)))
if not subqueries: # no terms in query
subqueries = [TrueQuery()]
return cls(subqueries)
@ -1094,6 +1109,7 @@ class Library(BaseLibrary):
sql = "SELECT * FROM items " + \
"WHERE " + where + \
" ORDER BY artist, album, disc, track"
log.debug('Getting items with SQL: %s' % sql)
c = self.conn.execute(sql, subvals)
return ResultIterator(c, self)

View file

@ -168,6 +168,29 @@ class GetTest(unittest.TestCase, AssertsMixin):
self.assert_matched(results, 'Boracay')
self.assert_done(results)
class MemoryGetTest(unittest.TestCase, AssertsMixin):
def setUp(self):
self.album_item = _common.item()
self.album_item.title = 'album item'
self.single_item = _common.item()
self.single_item.title = 'singleton item'
self.lib = beets.library.Library(':memory:')
self.lib.add(self.single_item)
self.lib.add_album([self.album_item])
def test_singleton_true(self):
q = 'singleton:true'
results = self.lib.get(q)
self.assert_matched(results, 'singleton item')
self.assert_done(results)
def test_singleton_false(self):
q = 'singleton:false'
results = self.lib.get(q)
self.assert_matched(results, 'album item')
self.assert_done(results)
class BrowseTest(unittest.TestCase, AssertsMixin):
def setUp(self):
self.lib = beets.library.Library('rsrc' + os.sep + 'test.blb')