From 67eb0ed54c60d2cab69616238aa802e98838cdcf Mon Sep 17 00:00:00 2001 From: Diego Moreda Date: Fri, 4 Dec 2015 20:51:25 +0100 Subject: [PATCH] Format track duration as H:MM instead of seconds * Modify library.Item in order to have length formatted as H:MM instead of the raw number of seconds by using a types.Float subclass (DurationType). * Add library.DurationType, with custom format() and parse() methods that handle the conversion. * Add dbcore.query.DurationQuery as a NumericQuery subclass that _convert()s the ranges specified by the user to floats, delegating the rest of the functionality in the parent NumericQuery class. * Add ui.raw_seconds_short() as the reverse of human_seconds_short(). This function uses a regular expression in order to allow any number of minutes, and always required SS to have two digits. --- beets/dbcore/query.py | 29 +++++++++++++++++++++++++++++ beets/library.py | 21 ++++++++++++++++++++- beets/ui/__init__.py | 13 +++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index f0adac665..9308ba0b3 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -653,6 +653,35 @@ class DateQuery(FieldQuery): return clause, subvals +class DurationQuery(NumericQuery): + """NumericQuery that allow human-friendly (M:SS) time interval formats. + + Converts the range(s) to a float value, and delegates on NumericQuery. + + Raises InvalidQueryError when the pattern does not represent an int, float + or M:SS time interval. + """ + def _convert(self, s): + """Convert a M:SS or numeric string to a float. + + Return None if `s` is empty. + Raise an InvalidQueryError if the string cannot be converted. + """ + if not s: + return None + try: + # TODO: tidy up circular import + from beets.ui import raw_seconds_short + return raw_seconds_short(s) + except ValueError: + try: + return float(s) + except ValueError: + raise InvalidQueryArgumentTypeError( + s, + "a M:SS string or a float") + + # Sorting. class Sort(object): diff --git a/beets/library.py b/beets/library.py index 870c46856..13b0b92fa 100644 --- a/beets/library.py +++ b/beets/library.py @@ -195,6 +195,25 @@ class MusicalKey(types.String): return self.parse(key) +class DurationType(types.Float): + """Human-friendly (M:SS) representation of a time interval.""" + query = dbcore.query.DurationQuery + + def format(self, value): + return beets.ui.human_seconds_short(value or 0.0) + + def parse(self, string): + try: + # Try to format back hh:ss to seconds. + return beets.ui.raw_seconds_short(value) + except ValueError: + # Fall back to a plain float.. + try: + return float(string) + except ValueError: + return self.null + + # Library-specific sort types. class SmartArtistSort(dbcore.query.Sort): @@ -426,7 +445,7 @@ class Item(LibModel): 'original_day': types.PaddedInt(2), 'initial_key': MusicalKey(), - 'length': types.FLOAT, + 'length': DurationType(), 'bitrate': types.ScaledInt(1000, u'kbps'), 'format': types.STRING, 'samplerate': types.ScaledInt(1000, u'kHz'), diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index c51c3acb6..10266b537 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -416,6 +416,19 @@ def human_seconds_short(interval): return u'%i:%02i' % (interval // 60, interval % 60) +def raw_seconds_short(string): + """Formats a human-readable M:SS string as a float (number of seconds). + + Raises ValueError if the conversion cannot take place due to `string` not + being in the right format. + """ + match = re.match('^(\d+):([0-5]\d)$', string) + if not match: + raise ValueError('String not in M:SS format') + minutes, seconds = map(int, match.groups()) + return float(minutes*60 + seconds) + + # Colorization. # ANSI terminal colorization code heavily inspired by pygments: