From be3bcbafe6cd09a21dfd88b76330fb2fc8c638e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stig=20Inge=20Lea=20Bj=C3=B8rnsen?= Date: Sun, 2 Feb 2014 17:17:11 +0100 Subject: [PATCH 1/3] Add a plugin for querying date fields against date intervals or instants. The interval syntax is similar to that of NumericQuery. Example: beet ls 'added:T2008..2010' --- beetsplug/datequery.py | 157 +++++++++++++++++++++++++++++++++++++ docs/plugins/datequery.rst | 29 +++++++ docs/plugins/index.rst | 2 + test/test_datequery.py | 60 ++++++++++++++ 4 files changed, 248 insertions(+) create mode 100644 beetsplug/datequery.py create mode 100644 docs/plugins/datequery.rst create mode 100644 test/test_datequery.py diff --git a/beetsplug/datequery.py b/beetsplug/datequery.py new file mode 100644 index 000000000..8bdcfe8a2 --- /dev/null +++ b/beetsplug/datequery.py @@ -0,0 +1,157 @@ +"""Matches date fields stored as seconds since Unix epoch time. + +Dates can be specified as year-month-day where only year is mandatory. + +The value of a date field can be matched against a date interval by using an +ellipses interval syntax similar to that of NumericQuery. +""" + +from __future__ import unicode_literals, absolute_import, print_function +from beets.plugins import BeetsPlugin +from beets.dbcore import FieldQuery +from datetime import datetime, timedelta +from beets.library import ITEM_FIELDS, DateType + +_DATE_FIELDS = [fieldname for (fieldname, typedef, _, _) + in ITEM_FIELDS if isinstance(typedef, DateType) ] + +def _queryable(fieldname): + """Determine whether a field can by queried as a date. + """ + return fieldname in _DATE_FIELDS + +def _to_epoch_time(date): + epoch = datetime.utcfromtimestamp(0) + return int((date - epoch).total_seconds()) + +def _parse_periods(pattern): + """Parse two Periods separated by '..' + """ + parts = pattern.split('..', 1) + if len(parts) == 1: + instant = Period.parse(parts[0]) + return (instant, instant) + else: + start = Period.parse(parts[0]) + end = Period.parse(parts[1]) + return (start, end) + +class Period(object): + """A period of time given by a date, time and precision. + + Example: + 2014-01-01 10:50:30 with precision 'month' represent all instants of time + during January 2014. + """ + + precisions = ('year', 'month', 'day') + date_formats = ('%Y', '%Y-%m', '%Y-%m-%d') + + def __init__(self, date, precision): + if precision not in Period.precisions: + raise ValueError('Invalid precision ' + str(precision)) + self.date = date + self.precision = precision + + @classmethod + def parse(cls, string): + """Parse a date into a period. + """ + if not string: return None + ordinal = string.count('-') + if ordinal >= len(cls.date_formats): + raise ValueError('Date is not in one of the formats ' + + ', '.join(cls.date_formats)) + date_format = cls.date_formats[ordinal] + date = datetime.strptime(string, date_format) + precision = cls.precisions[ordinal] + return cls(date, precision) + + def open_right_endpoint(self): + """Based on the precision, convert the period to a precise datetime + for use as a right endpoint in a right-open interval. + """ + precision = self.precision + date = self.date + if 'year' == self.precision: + return date.replace(year=date.year + 1, month=1) + elif 'month' == precision: + if (date.month < 12): + return date.replace(month=date.month + 1) + else: + return date.replace(year=date.year + 1, month=1) + elif 'day' == precision: + return date + timedelta(days=1) + else: + raise ValueError('Unhandled precision ' + str(precision)) + +class DateInterval(object): + """A closed-open interval of dates. + + A left endpoint of None means since the beginning of time. + A right endpoint of None means towards infinity. + """ + + def __init__(self, start, end): + if start is not None and end is not None and not start < end: + raise ValueError("Start date {} is not before end date {}" + .format(start, end)) + self.start = start + self.end = end + + @classmethod + def from_periods(cls, start, end): + """Create an interval with two Periods as the endpoints. + """ + end_date = end.open_right_endpoint() if end is not None else None + start_date = start.date if start is not None else None + return cls(start_date, end_date) + + def contains(self, date): + if self.start is not None and date < self.start: + return False + if self.end is not None and date >= self.end: + return False + return True + + def __str__(self): + return'[{}, {})'.format(self.start, self.end) + +class DateQuery(FieldQuery): + def __init__(self, field, pattern, fast=True): + super(DateQuery, self).__init__(field, pattern, fast) + if not _queryable(field): + raise ValueError('Field {} cannot be queried as a date'.format(field)) + + (start, end) = _parse_periods(pattern) + self.interval = DateInterval.from_periods(start, end) + + def match(self, item): + timestamp = float(item[self.field]) + date = datetime.utcfromtimestamp(timestamp) + return self.interval.contains(date) + + def col_clause(self): + if self.interval.start is not None and self.interval.end is not None: + start_epoch_time = _to_epoch_time(self.interval.start) + end_epoch_time = _to_epoch_time(self.interval.end) + template = ("date({}, 'unixepoch') >= date(?, 'unixepoch')" + " AND date({}, 'unixepoch') < date(?, 'unixepoch')") + clause = template.format(self.field, self.field) + return (clause, (start_epoch_time, end_epoch_time)) + elif self.interval.start is not None: + epoch_time = _to_epoch_time(self.interval.start) + template = "date({}, 'unixepoch') >= date(?, 'unixepoch')" + clause = template.format(self.field) + return clause.format(self.field), (epoch_time,) + elif self.interval.end is not None: + epoch_time = _to_epoch_time(self.interval.end) + template = "date({}, 'unixepoch') < date(?, 'unixepoch')" + clause = template.format(self.field) + return clause.format(self.field), (epoch_time,) + else: + return '1 = ?', (1,) # match any date + +class DateQueryPlugin(BeetsPlugin): + def queries(self): + return {'T': DateQuery} diff --git a/docs/plugins/datequery.rst b/docs/plugins/datequery.rst new file mode 100644 index 000000000..bb04228f2 --- /dev/null +++ b/docs/plugins/datequery.rst @@ -0,0 +1,29 @@ +DateQuery Plugin +================ + +The ``datequery`` plugin enables date fields to be queried against date +instants or date intervals. + +Dates can be specified as year-month-day where only year is mandatory. + +Date intervals must have at least a start or an end. The endpoints are +separated by two dots. + +A field can be queried as a date by prefixing the date criteria by ``T``. + +Example command line queries:: + + # All albums added in the year 2008: + beet ls -a 'added:T2008' + + # All items added in the years 2008, 2009 and 2010 + beet ls 'added:T2008..2010' + + # All items added before the year 2010 + beet ls 'added:T..2009' + + # All items added in the interval [2008-12-01T00:00:00, 2009-10-12T00:00:00) + beet ls 'added:T2008-12..2009-10-11' + + # All items with a stored file modification time in the interval [2008-12-01T00:00:00, 2008-12-03T00:00:00) + beet ls 'mtime:T2008-12-01..2008-12-02' diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 1a527391a..6eed7ab32 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -56,6 +56,7 @@ by typing ``beet version``. beatport fromfilename ftintitle + datequery Autotagger Extensions --------------------- @@ -121,6 +122,7 @@ Miscellaneous * :doc:`info`: Print music files' tags to the console. * :doc:`missing`: List missing tracks. * :doc:`duplicates`: List duplicate tracks or albums. +* :doc:`datequery`: Query date fields against date intervals or instants. .. _MPD: http://mpd.wikia.com/ .. _MPD clients: http://mpd.wikia.com/wiki/Clients diff --git a/test/test_datequery.py b/test/test_datequery.py new file mode 100644 index 000000000..906c822a0 --- /dev/null +++ b/test/test_datequery.py @@ -0,0 +1,60 @@ +import unittest +from datetime import datetime +from beetsplug.datequery import _parse_periods, DateInterval + +def _date(string): + return datetime.strptime(string, '%Y-%m-%dT%H:%M:%S') + +class TestDateQuery(unittest.TestCase): + + def test_year_precision_intervals(self): + self.assertContains('2000..2001', '2000-01-01T00:00:00') + self.assertContains('2000..2001', '2001-06-20T14:15:16') + self.assertContains('2000..2001', '2001-12-31T23:59:59') + self.assertExcludes('2000..2001', '1999-12-31T23:59:59') + self.assertExcludes('2000..2001', '2002-01-01T00:00:00') + + self.assertContains('2000..', '2000-01-01T00:00:00') + self.assertContains('2000..', '2099-10-11T00:00:00') + self.assertExcludes('2000..', '1999-12-31T23:59:59') + + self.assertContains('..2001', '2001-12-31T23:59:59') + self.assertExcludes('..2001', '2002-01-01T00:00:00') + + def test_day_precision_intervals(self): + self.assertContains('2000-06-20..2000-06-20', '2000-06-20T00:00:00') + self.assertContains('2000-06-20..2000-06-20', '2000-06-20T10:20:30') + self.assertContains('2000-06-20..2000-06-20', '2000-06-20T23:59:59') + self.assertExcludes('2000-06-20..2000-06-20', '2000-06-19T23:59:59') + self.assertExcludes('2000-06-20..2000-06-20', '2000-06-21T00:00:00') + + def test_month_precision_intervals(self): + self.assertContains('1999-12..2000-02', '1999-12-01T00:00:00') + self.assertContains('1999-12..2000-02', '2000-02-15T05:06:07') + self.assertContains('1999-12..2000-02', '2000-02-29T23:59:59') + self.assertExcludes('1999-12..2000-02', '1999-11-30T23:59:59') + self.assertExcludes('1999-12..2000-02', '2000-03-01T00:00:00') + + def test_unbounded_endpoints(self): + self.assertContains('..', date=datetime.max) + self.assertContains('..', date=datetime.min) + self.assertContains('..', '1000-01-01T00:00:00') + + def assertContains(self, interval_pattern, date_pattern=None, date=None): + if date is None: + date = _date(date_pattern) + (start, end) = _parse_periods(interval_pattern) + interval = DateInterval.from_periods(start, end) + self.assertTrue(interval.contains(date)) + + def assertExcludes(self, interval_pattern, date_pattern): + date = _date(date_pattern) + (start, end) = _parse_periods(interval_pattern) + interval = DateInterval.from_periods(start, end) + self.assertFalse(interval.contains(date)) + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == '__main__': + unittest.main(defaultTest='suite') From a27d83a4bf998d3d1614492b3e3b61b185d0f7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stig=20Inge=20Lea=20Bj=C3=B8rnsen?= Date: Mon, 3 Feb 2014 18:21:23 +0100 Subject: [PATCH 2/3] Refactor the date query from being a plugin to being part of Beets core. Fields of the type DateType will now automatically be queried by DateQuery. --- beets/dbcore/query.py | 137 +++++++++++++++++++++++++++++++++++ beets/library.py | 2 +- beetsplug/datequery.py | 157 ----------------------------------------- test/test_datequery.py | 2 +- 4 files changed, 139 insertions(+), 159 deletions(-) delete mode 100644 beetsplug/datequery.py diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 74f6cb903..e95ae14ed 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -16,6 +16,7 @@ """ import re from beets import util +from datetime import datetime, timedelta class Query(object): @@ -322,3 +323,139 @@ class FalseQuery(Query): def match(self, item): return False + +def _to_epoch_time(date): + epoch = datetime.utcfromtimestamp(0) + return int((date - epoch).total_seconds()) + +def _parse_periods(pattern): + """Parse two Periods separated by '..' + """ + parts = pattern.split('..', 1) + if len(parts) == 1: + instant = Period.parse(parts[0]) + return (instant, instant) + else: + start = Period.parse(parts[0]) + end = Period.parse(parts[1]) + return (start, end) + +class Period(object): + """A period of time given by a date, time and precision. + + Example: + 2014-01-01 10:50:30 with precision 'month' represent all instants of time + during January 2014. + """ + + precisions = ('year', 'month', 'day') + date_formats = ('%Y', '%Y-%m', '%Y-%m-%d') + + def __init__(self, date, precision): + if precision not in Period.precisions: + raise ValueError('Invalid precision ' + str(precision)) + self.date = date + self.precision = precision + + @classmethod + def parse(cls, string): + """Parse a date into a period. + """ + if not string: return None + ordinal = string.count('-') + if ordinal >= len(cls.date_formats): + raise ValueError('Date is not in one of the formats ' + + ', '.join(cls.date_formats)) + date_format = cls.date_formats[ordinal] + date = datetime.strptime(string, date_format) + precision = cls.precisions[ordinal] + return cls(date, precision) + + def open_right_endpoint(self): + """Based on the precision, convert the period to a precise datetime + for use as a right endpoint in a right-open interval. + """ + precision = self.precision + date = self.date + if 'year' == self.precision: + return date.replace(year=date.year + 1, month=1) + elif 'month' == precision: + if (date.month < 12): + return date.replace(month=date.month + 1) + else: + return date.replace(year=date.year + 1, month=1) + elif 'day' == precision: + return date + timedelta(days=1) + else: + raise ValueError('Unhandled precision ' + str(precision)) + +class DateInterval(object): + """A closed-open interval of dates. + + A left endpoint of None means since the beginning of time. + A right endpoint of None means towards infinity. + """ + + def __init__(self, start, end): + if start is not None and end is not None and not start < end: + raise ValueError("Start date {} is not before end date {}" + .format(start, end)) + self.start = start + self.end = end + + @classmethod + def from_periods(cls, start, end): + """Create an interval with two Periods as the endpoints. + """ + end_date = end.open_right_endpoint() if end is not None else None + start_date = start.date if start is not None else None + return cls(start_date, end_date) + + def contains(self, date): + if self.start is not None and date < self.start: + return False + if self.end is not None and date >= self.end: + return False + return True + + def __str__(self): + return'[{}, {})'.format(self.start, self.end) + +class DateQuery(FieldQuery): + """Matches date fields stored as seconds since Unix epoch time. + + Dates can be specified as year-month-day where only year is mandatory. + + The value of a date field can be matched against a date interval by using + an ellipses interval syntax similar to that of NumericQuery. + """ + def __init__(self, field, pattern, fast=True): + super(DateQuery, self).__init__(field, pattern, fast) + (start, end) = _parse_periods(pattern) + self.interval = DateInterval.from_periods(start, end) + + def match(self, item): + timestamp = float(item[self.field]) + date = datetime.utcfromtimestamp(timestamp) + return self.interval.contains(date) + + def col_clause(self): + if self.interval.start is not None and self.interval.end is not None: + start_epoch_time = _to_epoch_time(self.interval.start) + end_epoch_time = _to_epoch_time(self.interval.end) + template = ("date({}, 'unixepoch') >= date(?, 'unixepoch')" + " AND date({}, 'unixepoch') < date(?, 'unixepoch')") + clause = template.format(self.field, self.field) + return (clause, (start_epoch_time, end_epoch_time)) + elif self.interval.start is not None: + epoch_time = _to_epoch_time(self.interval.start) + template = "date({}, 'unixepoch') >= date(?, 'unixepoch')" + clause = template.format(self.field) + return clause.format(self.field), (epoch_time,) + elif self.interval.end is not None: + epoch_time = _to_epoch_time(self.interval.end) + template = "date({}, 'unixepoch') < date(?, 'unixepoch')" + clause = template.format(self.field) + return clause.format(self.field), (epoch_time,) + else: + return '1 = ?', (1,) # match any date diff --git a/beets/library.py b/beets/library.py index a66e1df78..d5b07c3e7 100644 --- a/beets/library.py +++ b/beets/library.py @@ -79,7 +79,7 @@ class SingletonQuery(dbcore.Query): class DateType(types.Type): sql = u'REAL' - query = dbcore.query.NumericQuery + query = dbcore.query.DateQuery def format(self, value): return time.strftime(beets.config['time_format'].get(unicode), diff --git a/beetsplug/datequery.py b/beetsplug/datequery.py deleted file mode 100644 index 8bdcfe8a2..000000000 --- a/beetsplug/datequery.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Matches date fields stored as seconds since Unix epoch time. - -Dates can be specified as year-month-day where only year is mandatory. - -The value of a date field can be matched against a date interval by using an -ellipses interval syntax similar to that of NumericQuery. -""" - -from __future__ import unicode_literals, absolute_import, print_function -from beets.plugins import BeetsPlugin -from beets.dbcore import FieldQuery -from datetime import datetime, timedelta -from beets.library import ITEM_FIELDS, DateType - -_DATE_FIELDS = [fieldname for (fieldname, typedef, _, _) - in ITEM_FIELDS if isinstance(typedef, DateType) ] - -def _queryable(fieldname): - """Determine whether a field can by queried as a date. - """ - return fieldname in _DATE_FIELDS - -def _to_epoch_time(date): - epoch = datetime.utcfromtimestamp(0) - return int((date - epoch).total_seconds()) - -def _parse_periods(pattern): - """Parse two Periods separated by '..' - """ - parts = pattern.split('..', 1) - if len(parts) == 1: - instant = Period.parse(parts[0]) - return (instant, instant) - else: - start = Period.parse(parts[0]) - end = Period.parse(parts[1]) - return (start, end) - -class Period(object): - """A period of time given by a date, time and precision. - - Example: - 2014-01-01 10:50:30 with precision 'month' represent all instants of time - during January 2014. - """ - - precisions = ('year', 'month', 'day') - date_formats = ('%Y', '%Y-%m', '%Y-%m-%d') - - def __init__(self, date, precision): - if precision not in Period.precisions: - raise ValueError('Invalid precision ' + str(precision)) - self.date = date - self.precision = precision - - @classmethod - def parse(cls, string): - """Parse a date into a period. - """ - if not string: return None - ordinal = string.count('-') - if ordinal >= len(cls.date_formats): - raise ValueError('Date is not in one of the formats ' - + ', '.join(cls.date_formats)) - date_format = cls.date_formats[ordinal] - date = datetime.strptime(string, date_format) - precision = cls.precisions[ordinal] - return cls(date, precision) - - def open_right_endpoint(self): - """Based on the precision, convert the period to a precise datetime - for use as a right endpoint in a right-open interval. - """ - precision = self.precision - date = self.date - if 'year' == self.precision: - return date.replace(year=date.year + 1, month=1) - elif 'month' == precision: - if (date.month < 12): - return date.replace(month=date.month + 1) - else: - return date.replace(year=date.year + 1, month=1) - elif 'day' == precision: - return date + timedelta(days=1) - else: - raise ValueError('Unhandled precision ' + str(precision)) - -class DateInterval(object): - """A closed-open interval of dates. - - A left endpoint of None means since the beginning of time. - A right endpoint of None means towards infinity. - """ - - def __init__(self, start, end): - if start is not None and end is not None and not start < end: - raise ValueError("Start date {} is not before end date {}" - .format(start, end)) - self.start = start - self.end = end - - @classmethod - def from_periods(cls, start, end): - """Create an interval with two Periods as the endpoints. - """ - end_date = end.open_right_endpoint() if end is not None else None - start_date = start.date if start is not None else None - return cls(start_date, end_date) - - def contains(self, date): - if self.start is not None and date < self.start: - return False - if self.end is not None and date >= self.end: - return False - return True - - def __str__(self): - return'[{}, {})'.format(self.start, self.end) - -class DateQuery(FieldQuery): - def __init__(self, field, pattern, fast=True): - super(DateQuery, self).__init__(field, pattern, fast) - if not _queryable(field): - raise ValueError('Field {} cannot be queried as a date'.format(field)) - - (start, end) = _parse_periods(pattern) - self.interval = DateInterval.from_periods(start, end) - - def match(self, item): - timestamp = float(item[self.field]) - date = datetime.utcfromtimestamp(timestamp) - return self.interval.contains(date) - - def col_clause(self): - if self.interval.start is not None and self.interval.end is not None: - start_epoch_time = _to_epoch_time(self.interval.start) - end_epoch_time = _to_epoch_time(self.interval.end) - template = ("date({}, 'unixepoch') >= date(?, 'unixepoch')" - " AND date({}, 'unixepoch') < date(?, 'unixepoch')") - clause = template.format(self.field, self.field) - return (clause, (start_epoch_time, end_epoch_time)) - elif self.interval.start is not None: - epoch_time = _to_epoch_time(self.interval.start) - template = "date({}, 'unixepoch') >= date(?, 'unixepoch')" - clause = template.format(self.field) - return clause.format(self.field), (epoch_time,) - elif self.interval.end is not None: - epoch_time = _to_epoch_time(self.interval.end) - template = "date({}, 'unixepoch') < date(?, 'unixepoch')" - clause = template.format(self.field) - return clause.format(self.field), (epoch_time,) - else: - return '1 = ?', (1,) # match any date - -class DateQueryPlugin(BeetsPlugin): - def queries(self): - return {'T': DateQuery} diff --git a/test/test_datequery.py b/test/test_datequery.py index 906c822a0..b23fae7c5 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -1,6 +1,6 @@ import unittest from datetime import datetime -from beetsplug.datequery import _parse_periods, DateInterval +from beets.dbcore.query import _parse_periods, DateInterval def _date(string): return datetime.strptime(string, '%Y-%m-%dT%H:%M:%S') From 732daddf53a6fd6755f2265f53f437c94a29be8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stig=20Inge=20Lea=20Bj=C3=B8rnsen?= Date: Tue, 4 Feb 2014 21:48:33 +0100 Subject: [PATCH 3/3] Move the date query documentation into the query reference documentation. --- docs/plugins/datequery.rst | 29 ----------------------------- docs/plugins/index.rst | 1 - docs/reference/query.rst | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 30 deletions(-) delete mode 100644 docs/plugins/datequery.rst diff --git a/docs/plugins/datequery.rst b/docs/plugins/datequery.rst deleted file mode 100644 index bb04228f2..000000000 --- a/docs/plugins/datequery.rst +++ /dev/null @@ -1,29 +0,0 @@ -DateQuery Plugin -================ - -The ``datequery`` plugin enables date fields to be queried against date -instants or date intervals. - -Dates can be specified as year-month-day where only year is mandatory. - -Date intervals must have at least a start or an end. The endpoints are -separated by two dots. - -A field can be queried as a date by prefixing the date criteria by ``T``. - -Example command line queries:: - - # All albums added in the year 2008: - beet ls -a 'added:T2008' - - # All items added in the years 2008, 2009 and 2010 - beet ls 'added:T2008..2010' - - # All items added before the year 2010 - beet ls 'added:T..2009' - - # All items added in the interval [2008-12-01T00:00:00, 2009-10-12T00:00:00) - beet ls 'added:T2008-12..2009-10-11' - - # All items with a stored file modification time in the interval [2008-12-01T00:00:00, 2008-12-03T00:00:00) - beet ls 'mtime:T2008-12-01..2008-12-02' diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 6eed7ab32..1637fafa4 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -122,7 +122,6 @@ Miscellaneous * :doc:`info`: Print music files' tags to the console. * :doc:`missing`: List missing tracks. * :doc:`duplicates`: List duplicate tracks or albums. -* :doc:`datequery`: Query date fields against date intervals or instants. .. _MPD: http://mpd.wikia.com/ .. _MPD clients: http://mpd.wikia.com/wiki/Clients diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 578611269..8bacf824b 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -128,6 +128,41 @@ and this command finds MP3 files with bitrates of 128k or lower:: $ beet list format:MP3 bitrate:..128000 +Date and Date Range Queries +--------------------------- + +Date fields such as added and mtime can be queried against a date or a one- or +two-sided date interval. + +A date is be specified as ``year-month-day`` where only year is mandatory. + +Date intervals should have at least a start or an end. The endpoints are +separated by two dots (``..``). + +An example query for finding all albums added in the year 2008:: + + $ beet ls -a 'added:2008' + +Find all items added in the years 2008, 2009 and 2010:: + + $ beet ls 'added:2008..2010' + +Find all items added before the year 2010:: + + $ beet ls 'added:..2009' + +Find all items added in the interval +[2008-12-01T00:00:00, 2009-10-12T00:00:00):: + + $ beet ls 'added:2008-12..2009-10-11' + +Find all items with a stored file modification time in the interval +[2008-12-01T00:00:00, 2008-12-03T00:00:00):: + + $ beet ls 'mtime:2008-12-01..2008-12-02' + +Note that the interval ``..`` is also valid, but it simply means the interval +of all possible dates. Path Queries ------------