mirror of
https://github.com/beetbox/beets.git
synced 2025-12-28 03:22:39 +01:00
Merge pull request #1749 from diego-plan9/humanlength
Format length as M:SS by default
This commit is contained in:
commit
d1adaa9cb7
7 changed files with 123 additions and 2 deletions
|
|
@ -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+
|
||||
|
|
|
|||
|
|
@ -653,6 +653,33 @@ 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:
|
||||
return util.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):
|
||||
|
|
|
|||
|
|
@ -195,6 +195,28 @@ 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):
|
||||
if not beets.config['format_raw_length'].get(bool):
|
||||
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 util.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):
|
||||
|
|
@ -426,7 +448,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'),
|
||||
|
|
|
|||
|
|
@ -843,3 +843,16 @@ def case_sensitive(path):
|
|||
lower = _windows_long_path_name(path.lower())
|
||||
upper = _windows_long_path_name(path.upper())
|
||||
return lower != upper
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ New:
|
|||
singles compilation, "1." See :ref:`not_query`. :bug:`819` :bug:`1728`
|
||||
* :doc:`/plugins/info`: The plugin now accepts the ``-f/--format`` option for
|
||||
customizing how items are displayed. :bug:`1737`
|
||||
* Track length is now displayed as ``M:SS`` by default, instead of displaying
|
||||
the raw number of seconds. Queries on track length also accept this format:
|
||||
for example, ``beet list length:5:30..`` will find all your tracks that have
|
||||
a duration over 5 minutes and 30 seconds. You can toggle this setting off
|
||||
via the ``format_raw_length`` configuration option. :bug:`1749`
|
||||
|
||||
For developers:
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class InfoTest(unittest.TestCase, TestHelper):
|
|||
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():
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import shutil
|
|||
import re
|
||||
import unicodedata
|
||||
import sys
|
||||
import time
|
||||
|
||||
from test import _common
|
||||
from test._common import unittest
|
||||
|
|
@ -1127,6 +1128,58 @@ class ParseQueryTest(unittest.TestCase):
|
|||
beets.library.parse_query_string(b"query", None)
|
||||
|
||||
|
||||
class LibraryFieldTypesTest(unittest.TestCase):
|
||||
"""Test format() and parse() for library-specific field types"""
|
||||
def test_datetype(self):
|
||||
t = beets.library.DateType()
|
||||
|
||||
# format
|
||||
time_local = time.strftime(beets.config['time_format'].get(unicode),
|
||||
time.localtime(123456789))
|
||||
self.assertEqual(time_local, t.format(123456789))
|
||||
# parse
|
||||
self.assertEqual(123456789.0, t.parse(time_local))
|
||||
self.assertEqual(123456789.0, t.parse('123456789.0'))
|
||||
self.assertEqual(t.null, t.parse('not123456789.0'))
|
||||
self.assertEqual(t.null, t.parse('1973-11-29'))
|
||||
|
||||
def test_pathtype(self):
|
||||
t = beets.library.PathType()
|
||||
|
||||
# format
|
||||
self.assertEqual('/tmp', t.format('/tmp'))
|
||||
self.assertEqual(u'/tmp/\xe4lbum', t.format(u'/tmp/\u00e4lbum'))
|
||||
# parse
|
||||
self.assertEqual(b'/tmp', t.parse('/tmp'))
|
||||
self.assertEqual(b'/tmp/\xc3\xa4lbum', t.parse(u'/tmp/\u00e4lbum/'))
|
||||
|
||||
def test_musicalkey(self):
|
||||
t = beets.library.MusicalKey()
|
||||
|
||||
# parse
|
||||
self.assertEqual('C#m', t.parse('c#m'))
|
||||
self.assertEqual('Gm', t.parse('g minor'))
|
||||
self.assertEqual('Not c#m', t.parse('not C#m'))
|
||||
|
||||
def test_durationtype(self):
|
||||
t = beets.library.DurationType()
|
||||
|
||||
# format
|
||||
self.assertEqual('1:01', t.format(61.23))
|
||||
self.assertEqual('60:01', t.format(3601.23))
|
||||
self.assertEqual('0:00', t.format(None))
|
||||
# parse
|
||||
self.assertEqual(61.0, t.parse('1:01'))
|
||||
self.assertEqual(61.23, t.parse('61.23'))
|
||||
self.assertEqual(3601.0, t.parse('60:01'))
|
||||
self.assertEqual(t.null, t.parse('1:00:01'))
|
||||
self.assertEqual(t.null, t.parse('not61.23'))
|
||||
# config format_raw_length
|
||||
beets.config['format_raw_length'] = True
|
||||
self.assertEqual(61.23, t.format(61.23))
|
||||
self.assertEqual(3601.23, t.format(3601.23))
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue