diff --git a/beets/library.py b/beets/library.py index 65c27a0c9..2cd114855 100644 --- a/beets/library.py +++ b/beets/library.py @@ -549,6 +549,31 @@ class BooleanQuery(MatchQuery): self.pattern = util.str2bool(pattern) self.pattern = int(self.pattern) +class YearQuery(FieldQuery): + """Matches a year or years against a year field. + """ + @classmethod + def applies_to(cls, field): + return field in ['year', 'original_year'] + + @classmethod + def value_match(cls, pattern, value): + """Determine whether the value matches the pattern. Both + arguments are strings. + """ + return value in cls._expanded_years(pattern) + + def clause(self): + years = YearQuery._expanded_years(self.pattern) + return self.field + " IN (" + ",".join(years) + ")", () + + @classmethod + def _expanded_years(self, pattern): + ranges = [[int(y) for y in se.split('-')] for se in pattern.split(',')] + years = [range(r[0], r[1] + 1) if len(r) > 1 else [r[0]] for r in ranges] + return [str(y) for yrs in years for y in yrs] + + class SingletonQuery(Query): """Matches either singleton or non-singleton items.""" def __init__(self, sense): @@ -750,6 +775,8 @@ def parse_query_part(part): for pre, query_class in prefixes.items(): if term.startswith(pre): return key, term[len(pre):], query_class + if YearQuery.applies_to(key): + return key, term, YearQuery return key, term, SubstringQuery # The default query type. def construct_query_part(query_part, default_fields, all_keys): diff --git a/test/test_query.py b/test/test_query.py index 54b30cf1e..0254fbad5 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -60,6 +60,22 @@ class QueryParseTest(unittest.TestCase): r = (None, 'test:regexp', beets.library.RegexpQuery) self.assertEqual(pqp(q), r) + def test_single_year(self): + q = 'year:1999' + r = ('year', '1999', beets.library.YearQuery) + self.assertEqual(pqp(q), r) + + def test_multiple_years(self): + q = 'year:1999,2002,2010' + r = ('year', '1999,2002,2010', beets.library.YearQuery) + self.assertEqual(pqp(q), r) + + def test_year_range(self): + q = 'year:1999-2001' + r = ('year', '1999-2001', beets.library.YearQuery) + self.assertEqual(pqp(q), r) + + class AnyFieldQueryTest(unittest.TestCase): def setUp(self): self.lib = beets.library.Library(':memory:') @@ -219,6 +235,29 @@ class GetTest(unittest.TestCase, AssertsMixin): self.assert_matched(results, 'Littlest Things') self.assert_done(results) + def test_single_year(self): + q = 'year:2006' + results = self.lib.items(q) + self.assert_matched(results, 'Littlest Things') + self.assert_matched(results, 'Lovers Who Uncover') + self.assert_done(results) + + def test_year_range(self): + q = 'year:2000-2010' + results = self.lib.items(q) + self.assert_matched(results, 'Littlest Things') + self.assert_matched(results, 'Take Pills') + self.assert_matched(results, 'Lovers Who Uncover') + self.assert_done(results) + + def test_multiple_years(self): + q = 'year:1987,2004-2006' + results = self.lib.items(q) + self.assert_matched(results, 'Littlest Things') + self.assert_matched(results, 'Lovers Who Uncover') + self.assert_matched(results, 'Boracay') + self.assert_done(results) + class MemoryGetTest(unittest.TestCase, AssertsMixin): def setUp(self): self.album_item = _common.item() @@ -312,6 +351,14 @@ class MatchTest(unittest.TestCase): q = beets.library.SubstringQuery('disc', '6') self.assertTrue(q.match(self.item)) + def test_year_match_positive(self): + q = beets.library.YearQuery('year', '1') + self.assertTrue(q.match(self.item)) + + def test_year_match_negative(self): + q = beets.library.YearQuery('year', '10') + self.assertFalse(q.match(self.item)) + class PathQueryTest(unittest.TestCase, AssertsMixin): def setUp(self): self.lib = beets.library.Library(':memory:')