diff --git a/NEWS b/NEWS index 87971790f..61a453d40 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/beets/library.py b/beets/library.py index 37f56873e..640becdec 100644 --- a/beets/library.py +++ b/beets/library.py @@ -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) diff --git a/test/test_query.py b/test/test_query.py index 46f8a440b..1020753d4 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -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')