Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format length as M:SS by default #1749

Merged
merged 10 commits into from
Dec 13, 2015
1 change: 1 addition & 0 deletions beets/config_default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ ui:
format_item: $artist - $album - $title
format_album: $albumartist - $album
time_format: '%Y-%m-%d %H:%M:%S'
format_raw_length: no

sort_album: albumartist+ album+
sort_item: artist+ album+ disc+ track+
Expand Down
29 changes: 29 additions & 0 deletions beets/dbcore/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,35 @@ def col_clause(self):
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point on the circular import—and we mostly try to make dbcore a separate component. Maybe the function should live in this module?

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):
Expand Down
25 changes: 24 additions & 1 deletion beets/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,29 @@ def normalize(self, key):
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):
# TODO: decide if documenting format_raw_length
if not beets.config['format_raw_length'].get(bool):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, .get(bool) is implied when using a configuration view as a condition.

return beets.ui.human_seconds_short(value or 0.0)
else:
return value

def parse(self, string):
try:
# Try to format back hh:ss to seconds.
return beets.ui.raw_seconds_short(string)
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):
Expand Down Expand Up @@ -426,7 +449,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'),
Expand Down
13 changes: 13 additions & 0 deletions beets/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A note about using a regexp: I have decided to go through this route to make it consistent with human_seconds_short (ie. format should be (int)M:(int)SS, where SS must be < 60). Using the datetime facilities would restrict M to be < 60.

It might be desirable to make it a bit more flexible (allow HH:MM:SS, or M:SS.zzzzz, etc), and it should not be too hard, but I've opted not to include those for keeping it as an "inverse" of sorts of raw_seconds_short.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect. This seems good for now—and hour-long tracks are certainly an edge case.

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:
Expand Down
2 changes: 1 addition & 1 deletion test/test_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def test_custom_format(self):
self.add_item_fixtures()
out = self.run_with_output('--library', '--format',
'$track. $title - $artist ($length)')
self.assertEqual(u'02. tïtle 0 - the artist (1.1)\n', out)
self.assertEqual(u'02. tïtle 0 - the artist (0:01)\n', out)


def suite():
Expand Down