diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 4ba73617f..f37460423 100644 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -26,47 +26,11 @@ from beets.util.functemplate import Template from .query import MatchQuery -# Path element formatting for templating. -# FIXME remove this once we have type-based formatting. -def format_for_path(value, key=None): - """Sanitize the value for inclusion in a path: replace separators - with _, etc. Doesn't guarantee that the whole path will be valid; - you should still call `util.sanitize_path` on the complete path. - """ - if isinstance(value, basestring): - if isinstance(value, str): - value = value.decode('utf8', 'ignore') - elif key in ('track', 'tracktotal', 'disc', 'disctotal'): - # Pad indices with zeros. - value = u'%02i' % (value or 0) - elif key == 'year': - value = u'%04i' % (value or 0) - elif key in ('month', 'day'): - value = u'%02i' % (value or 0) - elif key == 'bitrate': - # Bitrate gets formatted as kbps. - value = u'%ikbps' % ((value or 0) // 1000) - elif key == 'samplerate': - # Sample rate formatted as kHz. - value = u'%ikHz' % ((value or 0) // 1000) - elif key in ('added', 'mtime'): - # Times are formatted to be human-readable. - value = time.strftime(beets.config['time_format'].get(unicode), - time.localtime(value)) - value = unicode(value) - elif value is None: - value = u'' - else: - value = unicode(value) - - return value - - # Abstract base for model classes and their field types. -Type = namedtuple('Type', 'sql query') +Type = namedtuple('Type', 'sql query format') class Model(object): @@ -364,9 +328,11 @@ class Model(object): """ value = self.get(key) - # FIXME this will get replaced with more sophisticated - # (type-based) formatting logic. - value = format_for_path(value, key) + # Format the value as a string according to its type, if any. + if key in self._fields: + value = self._fields[key].format(value) + elif not isinstance(value, unicode): + value = unicode(value) if for_path: sep_repl = beets.config['path_sep_replace'].get(unicode) diff --git a/beets/library.py b/beets/library.py index 48af99f8d..3c4b6893a 100644 --- a/beets/library.py +++ b/beets/library.py @@ -78,14 +78,27 @@ class SingletonQuery(dbcore.Query): # Model field lists. # Common types used in field definitions. -TYPES = { - int: Type('INTEGER', dbcore.query.NumericQuery), - float: Type('REAL', dbcore.query.NumericQuery), - datetime: Type('REAL', dbcore.query.NumericQuery), - unicode: Type('TEXT', dbcore.query.SubstringQuery), - bool: Type('INTEGER', dbcore.query.BooleanQuery), -} -PATH_TYPE = Type('BLOB', PathQuery) +ID_TYPE = Type('INTEGER PRIMARY KEY', dbcore.query.NumericQuery, unicode) +INT_TYPE = Type('INTEGER', dbcore.query.NumericQuery, unicode) +FLOAT_TYPE = Type('REAL', dbcore.query.NumericQuery, + lambda n: u'{0:.1f}'.format(n)) +DATE_TYPE = Type( + 'REAL', + dbcore.query.NumericQuery, + lambda d: time.strftime(beets.config['time_format'].get(unicode), + time.localtime(d)) +) +STRING_TYPE = Type('TEXT', dbcore.query.SubstringQuery, unicode) +BOOL_TYPE = Type('INTEGER', dbcore.query.BooleanQuery, unicode) +PATH_TYPE = Type('BLOB', PathQuery, util.displayable_path) + +def _padded_int(digits): + return Type('INTEGER', dbcore.query.NumericQuery, + lambda n: u'{0:0{1}d}'.format(n, digits)) + +def _scaled_int(suffix=u'', unit=1000): + return Type('INTEGER', dbcore.query.NumericQuery, + lambda n: u'{0}{1}'.format(n // unit, suffix)) # Fields in the "items" database table; all the metadata available for # items in the library. These are used directly in SQL; they are @@ -96,68 +109,67 @@ PATH_TYPE = Type('BLOB', PathQuery) # - Is the field writable? # - Does the field reflect an attribute of a MediaFile? ITEM_FIELDS = [ - ('id', Type('INTEGER PRIMARY KEY', dbcore.query.NumericQuery), - False, False), - ('path', PATH_TYPE, False, False), - ('album_id', TYPES[int], False, False), + ('id', ID_TYPE, False, False), + ('path', PATH_TYPE, False, False), + ('album_id', INT_TYPE, False, False), - ('title', TYPES[unicode], True, True), - ('artist', TYPES[unicode], True, True), - ('artist_sort', TYPES[unicode], True, True), - ('artist_credit', TYPES[unicode], True, True), - ('album', TYPES[unicode], True, True), - ('albumartist', TYPES[unicode], True, True), - ('albumartist_sort', TYPES[unicode], True, True), - ('albumartist_credit', TYPES[unicode], True, True), - ('genre', TYPES[unicode], True, True), - ('composer', TYPES[unicode], True, True), - ('grouping', TYPES[unicode], True, True), - ('year', TYPES[int], True, True), - ('month', TYPES[int], True, True), - ('day', TYPES[int], True, True), - ('track', TYPES[int], True, True), - ('tracktotal', TYPES[int], True, True), - ('disc', TYPES[int], True, True), - ('disctotal', TYPES[int], True, True), - ('lyrics', TYPES[unicode], True, True), - ('comments', TYPES[unicode], True, True), - ('bpm', TYPES[int], True, True), - ('comp', TYPES[bool], True, True), - ('mb_trackid', TYPES[unicode], True, True), - ('mb_albumid', TYPES[unicode], True, True), - ('mb_artistid', TYPES[unicode], True, True), - ('mb_albumartistid', TYPES[unicode], True, True), - ('albumtype', TYPES[unicode], True, True), - ('label', TYPES[unicode], True, True), - ('acoustid_fingerprint', TYPES[unicode], True, True), - ('acoustid_id', TYPES[unicode], True, True), - ('mb_releasegroupid', TYPES[unicode], True, True), - ('asin', TYPES[unicode], True, True), - ('catalognum', TYPES[unicode], True, True), - ('script', TYPES[unicode], True, True), - ('language', TYPES[unicode], True, True), - ('country', TYPES[unicode], True, True), - ('albumstatus', TYPES[unicode], True, True), - ('media', TYPES[unicode], True, True), - ('albumdisambig', TYPES[unicode], True, True), - ('disctitle', TYPES[unicode], True, True), - ('encoder', TYPES[unicode], True, True), - ('rg_track_gain', TYPES[float], True, True), - ('rg_track_peak', TYPES[float], True, True), - ('rg_album_gain', TYPES[float], True, True), - ('rg_album_peak', TYPES[float], True, True), - ('original_year', TYPES[int], True, True), - ('original_month', TYPES[int], True, True), - ('original_day', TYPES[int], True, True), + ('title', STRING_TYPE, True, True), + ('artist', STRING_TYPE, True, True), + ('artist_sort', STRING_TYPE, True, True), + ('artist_credit', STRING_TYPE, True, True), + ('album', STRING_TYPE, True, True), + ('albumartist', STRING_TYPE, True, True), + ('albumartist_sort', STRING_TYPE, True, True), + ('albumartist_credit', STRING_TYPE, True, True), + ('genre', STRING_TYPE, True, True), + ('composer', STRING_TYPE, True, True), + ('grouping', STRING_TYPE, True, True), + ('year', _padded_int(4), True, True), + ('month', _padded_int(2), True, True), + ('day', _padded_int(2), True, True), + ('track', _padded_int(2), True, True), + ('tracktotal', _padded_int(2), True, True), + ('disc', _padded_int(2), True, True), + ('disctotal', _padded_int(2), True, True), + ('lyrics', STRING_TYPE, True, True), + ('comments', STRING_TYPE, True, True), + ('bpm', INT_TYPE, True, True), + ('comp', BOOL_TYPE, True, True), + ('mb_trackid', STRING_TYPE, True, True), + ('mb_albumid', STRING_TYPE, True, True), + ('mb_artistid', STRING_TYPE, True, True), + ('mb_albumartistid', STRING_TYPE, True, True), + ('albumtype', STRING_TYPE, True, True), + ('label', STRING_TYPE, True, True), + ('acoustid_fingerprint', STRING_TYPE, True, True), + ('acoustid_id', STRING_TYPE, True, True), + ('mb_releasegroupid', STRING_TYPE, True, True), + ('asin', STRING_TYPE, True, True), + ('catalognum', STRING_TYPE, True, True), + ('script', STRING_TYPE, True, True), + ('language', STRING_TYPE, True, True), + ('country', STRING_TYPE, True, True), + ('albumstatus', STRING_TYPE, True, True), + ('media', STRING_TYPE, True, True), + ('albumdisambig', STRING_TYPE, True, True), + ('disctitle', STRING_TYPE, True, True), + ('encoder', STRING_TYPE, True, True), + ('rg_track_gain', FLOAT_TYPE, True, True), + ('rg_track_peak', FLOAT_TYPE, True, True), + ('rg_album_gain', FLOAT_TYPE, True, True), + ('rg_album_peak', FLOAT_TYPE, True, True), + ('original_year', _padded_int(4), True, True), + ('original_month', _padded_int(2), True, True), + ('original_day', _padded_int(2), True, True), - ('length', TYPES[float], False, True), - ('bitrate', TYPES[int], False, True), - ('format', TYPES[unicode], False, True), - ('samplerate', TYPES[int], False, True), - ('bitdepth', TYPES[int], False, True), - ('channels', TYPES[int], False, True), - ('mtime', TYPES[int], False, False), - ('added', TYPES[datetime], False, False), + ('length', FLOAT_TYPE, False, True), + ('bitrate', _scaled_int(u'kbps'), False, True), + ('format', STRING_TYPE, False, True), + ('samplerate', _scaled_int(u'kHz'), False, True), + ('bitdepth', INT_TYPE, False, True), + ('channels', INT_TYPE, False, True), + ('mtime', DATE_TYPE, False, False), + ('added', DATE_TYPE, False, False), ] ITEM_KEYS_WRITABLE = [f[0] for f in ITEM_FIELDS if f[3] and f[2]] ITEM_KEYS_META = [f[0] for f in ITEM_FIELDS if f[3]] @@ -167,39 +179,39 @@ ITEM_KEYS = [f[0] for f in ITEM_FIELDS] # The third entry in each tuple indicates whether the field reflects an # identically-named field in the items table. ALBUM_FIELDS = [ - ('id', Type('INTEGER PRIMARY KEY', dbcore.query.NumericQuery), False), - ('artpath', PATH_TYPE, False), - ('added', TYPES[datetime], True), + ('id', ID_TYPE, False), + ('artpath', PATH_TYPE, False), + ('added', DATE_TYPE, True), - ('albumartist', TYPES[unicode], True), - ('albumartist_sort', TYPES[unicode], True), - ('albumartist_credit', TYPES[unicode], True), - ('album', TYPES[unicode], True), - ('genre', TYPES[unicode], True), - ('year', TYPES[int], True), - ('month', TYPES[int], True), - ('day', TYPES[int], True), - ('tracktotal', TYPES[int], True), - ('disctotal', TYPES[int], True), - ('comp', TYPES[bool], True), - ('mb_albumid', TYPES[unicode], True), - ('mb_albumartistid', TYPES[unicode], True), - ('albumtype', TYPES[unicode], True), - ('label', TYPES[unicode], True), - ('mb_releasegroupid', TYPES[unicode], True), - ('asin', TYPES[unicode], True), - ('catalognum', TYPES[unicode], True), - ('script', TYPES[unicode], True), - ('language', TYPES[unicode], True), - ('country', TYPES[unicode], True), - ('albumstatus', TYPES[unicode], True), - ('media', TYPES[unicode], True), - ('albumdisambig', TYPES[unicode], True), - ('rg_album_gain', TYPES[float], True), - ('rg_album_peak', TYPES[float], True), - ('original_year', TYPES[int], True), - ('original_month', TYPES[int], True), - ('original_day', TYPES[int], True), + ('albumartist', STRING_TYPE, True), + ('albumartist_sort', STRING_TYPE, True), + ('albumartist_credit', STRING_TYPE, True), + ('album', STRING_TYPE, True), + ('genre', STRING_TYPE, True), + ('year', _padded_int(4), True), + ('month', _padded_int(2), True), + ('day', _padded_int(2), True), + ('tracktotal', _padded_int(2), True), + ('disctotal', _padded_int(2), True), + ('comp', BOOL_TYPE, True), + ('mb_albumid', STRING_TYPE, True), + ('mb_albumartistid', STRING_TYPE, True), + ('albumtype', STRING_TYPE, True), + ('label', STRING_TYPE, True), + ('mb_releasegroupid', STRING_TYPE, True), + ('asin', STRING_TYPE, True), + ('catalognum', STRING_TYPE, True), + ('script', STRING_TYPE, True), + ('language', STRING_TYPE, True), + ('country', STRING_TYPE, True), + ('albumstatus', STRING_TYPE, True), + ('media', STRING_TYPE, True), + ('albumdisambig', STRING_TYPE, True), + ('rg_album_gain', FLOAT_TYPE, True), + ('rg_album_peak', FLOAT_TYPE, True), + ('original_year', _padded_int(4), True), + ('original_month', _padded_int(2), True), + ('original_day', _padded_int(2), True), ] ALBUM_KEYS = [f[0] for f in ALBUM_FIELDS] ALBUM_KEYS_ITEM = [f[0] for f in ALBUM_FIELDS if f[2]] diff --git a/test/test_dbcore.py b/test/test_dbcore.py index ff640800b..86578285a 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -25,8 +25,10 @@ from beets import dbcore # Fixture: concrete database and model classes. For migration tests, we # have multiple models with different numbers of fields. -ID_TYPE = dbcore.Type('INTEGER PRIMARY KEY', dbcore.query.NumericQuery) -INT_TYPE = dbcore.Type('INTEGER', dbcore.query.NumericQuery) +ID_TYPE = dbcore.Type('INTEGER PRIMARY KEY', dbcore.query.NumericQuery, + unicode) +INT_TYPE = dbcore.Type('INTEGER', dbcore.query.NumericQuery, + unicode) class TestModel1(dbcore.Model): _table = 'test'