diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index f5210f41c..cda357be7 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -15,6 +15,11 @@ """Representation of type information for DBCore model fields. """ from . import query +from beets.util import str2bool + + + +# Abstract base. class Type(object): @@ -22,62 +27,116 @@ class Type(object): information about how to store the value in the database, query, format, and parse a given field. """ - def __init__(self, sql, query, format_func=None): + def __init__(self, sql, query): """Create a type. `sql` is the SQLite column type for the value. `query` is the `Query` subclass to be used when querying the - field. `format_func` is a function that transforms values of - this type to a human-readable Unicode string. If `format_func` - is not provided, the subclass must override `format` to provide - the functionality. + field. """ self.sql = sql self.query = query - self.format_func = format_func def format(self, value): """Given a value of this type, produce a Unicode string representing the value. This is used in template evaluation. """ - return self.format_func(value) + raise NotImplementedError() - -# Common singleton types. - -ID_TYPE = Type('INTEGER PRIMARY KEY', query.NumericQuery, unicode) -INT_TYPE = Type('INTEGER', query.NumericQuery, - lambda n: unicode(n or 0)) -FLOAT_TYPE = Type('REAL', query.NumericQuery, - lambda n: u'{0:.1f}'.format(n or 0.0)) -STRING_TYPE = Type('TEXT', query.SubstringQuery, - lambda s: unicode(s) if s else u'') -BOOL_TYPE = Type('INTEGER', query.BooleanQuery, - lambda b: unicode(bool(b))) + def parse(self, string): + """Parse a (possibly human-written) string and return the + indicated value of this type. + """ + raise NotImplementedError() -# Parameterized types. +# Reusable types. -class PaddedInt(Type): +class Integer(Type): + """A basic integer type. + """ + def __init__(self, sql='INTEGER', query=query.NumericQuery): + super(Integer, self).__init__(sql, query) + + def format(self, value): + return unicode(value or 0) + + def parse(self, string): + try: + return int(string) + except ValueError: + return 0 + + +class PaddedInt(Integer): """An integer field that is formatted with a given number of digits, padded with zeroes. """ def __init__(self, digits): + super(PaddedInt, self).__init__() self.digits = digits - super(PaddedInt, self).__init__('INTEGER', query.NumericQuery) def format(self, value): return u'{0:0{1}d}'.format(value or 0, self.digits) -class ScaledInt(Type): +class ScaledInt(Integer): """An integer whose formatting operation scales the number by a constant and adds a suffix. Good for units with large magnitudes. """ def __init__(self, unit, suffix=u''): + super(ScaledInt, self).__init__() self.unit = unit self.suffix = suffix - super(ScaledInt, self).__init__('INTEGER', query.NumericQuery) def format(self, value): return u'{0}{1}'.format((value or 0) // self.unit, self.suffix) + + +class Id(Integer): + """An integer used as the row key for a SQLite table. + """ + def __init__(self): + super(Id, self).__init__('INTEGER PRIMARY KEY') + + +class Float(Type): + """A basic floating-point type. + """ + def __init__(self, sql='REAL', query=query.NumericQuery): + super(Float, self).__init__(sql, query) + + def format(self, value): + return u'{0:.1f}'.format(value or 0.0) + + def parse(self, string): + try: + return float(string) + except ValueError: + return 0.0 + + +class String(Type): + """A Unicode string type. + """ + def __init__(self, sql='TEXT', query=query.SubstringQuery): + super(String, self).__init__(sql, query) + + def format(self, value): + return unicode(value) if value else u'' + + def parse(self, string): + return string + + +class Boolean(Type): + """A boolean type. + """ + def __init__(self, sql='INTEGER', query=query.BooleanQuery): + return super(Boolean, self).__init__(sql, query) + + def format(self, value): + return unicode(bool(value)) + + def parse(self, string): + return str2bool(string) diff --git a/beets/library.py b/beets/library.py index 6324e1b35..12ba5b198 100644 --- a/beets/library.py +++ b/beets/library.py @@ -74,16 +74,35 @@ class SingletonQuery(dbcore.Query): +# Library-specific field types. + + +class DateType(types.Type): + def __init__(self): + super(DateType, self).__init__('REAL', dbcore.query.NumericQuery) + + def format(self, value): + return time.strftime(beets.config['time_format'].get(unicode), + time.localtime(value or 0)) + + def parse(self, string): + raise NotImplementedError() # FIXME + + +class PathType(types.Type): + def __init__(self): + super(PathType, self).__init__('BLOB', PathQuery) + + def format(self, value): + return util.displayable_path(value) + + def parse(self, string): + return normpath(bytestring_path(string)) + + + # Model field lists. -# Common types used in field definitions. -DATE_TYPE = types.Type( - 'REAL', - dbcore.query.NumericQuery, - lambda n: time.strftime(beets.config['time_format'].get(unicode), - time.localtime(n or 0)) -) -PATH_TYPE = types.Type('BLOB', PathQuery, util.displayable_path) # Fields in the "items" database table; all the metadata available for # items in the library. These are used directly in SQL; they are @@ -94,21 +113,21 @@ PATH_TYPE = types.Type('BLOB', PathQuery, util.displayable_path) # - Is the field writable? # - Does the field reflect an attribute of a MediaFile? ITEM_FIELDS = [ - ('id', types.ID_TYPE, False, False), - ('path', PATH_TYPE, False, False), - ('album_id', types.INT_TYPE, False, False), + ('id', types.Id(), False, False), + ('path', PathType(), False, False), + ('album_id', types.Integer(), False, False), - ('title', types.STRING_TYPE, True, True), - ('artist', types.STRING_TYPE, True, True), - ('artist_sort', types.STRING_TYPE, True, True), - ('artist_credit', types.STRING_TYPE, True, True), - ('album', types.STRING_TYPE, True, True), - ('albumartist', types.STRING_TYPE, True, True), - ('albumartist_sort', types.STRING_TYPE, True, True), - ('albumartist_credit', types.STRING_TYPE, True, True), - ('genre', types.STRING_TYPE, True, True), - ('composer', types.STRING_TYPE, True, True), - ('grouping', types.STRING_TYPE, True, True), + ('title', types.String(), True, True), + ('artist', types.String(), True, True), + ('artist_sort', types.String(), True, True), + ('artist_credit', types.String(), True, True), + ('album', types.String(), True, True), + ('albumartist', types.String(), True, True), + ('albumartist_sort', types.String(), True, True), + ('albumartist_credit', types.String(), True, True), + ('genre', types.String(), True, True), + ('composer', types.String(), True, True), + ('grouping', types.String(), True, True), ('year', types.PaddedInt(4), True, True), ('month', types.PaddedInt(2), True, True), ('day', types.PaddedInt(2), True, True), @@ -116,45 +135,45 @@ ITEM_FIELDS = [ ('tracktotal', types.PaddedInt(2), True, True), ('disc', types.PaddedInt(2), True, True), ('disctotal', types.PaddedInt(2), True, True), - ('lyrics', types.STRING_TYPE, True, True), - ('comments', types.STRING_TYPE, True, True), - ('bpm', types.INT_TYPE, True, True), - ('comp', types.BOOL_TYPE, True, True), - ('mb_trackid', types.STRING_TYPE, True, True), - ('mb_albumid', types.STRING_TYPE, True, True), - ('mb_artistid', types.STRING_TYPE, True, True), - ('mb_albumartistid', types.STRING_TYPE, True, True), - ('albumtype', types.STRING_TYPE, True, True), - ('label', types.STRING_TYPE, True, True), - ('acoustid_fingerprint', types.STRING_TYPE, True, True), - ('acoustid_id', types.STRING_TYPE, True, True), - ('mb_releasegroupid', types.STRING_TYPE, True, True), - ('asin', types.STRING_TYPE, True, True), - ('catalognum', types.STRING_TYPE, True, True), - ('script', types.STRING_TYPE, True, True), - ('language', types.STRING_TYPE, True, True), - ('country', types.STRING_TYPE, True, True), - ('albumstatus', types.STRING_TYPE, True, True), - ('media', types.STRING_TYPE, True, True), - ('albumdisambig', types.STRING_TYPE, True, True), - ('disctitle', types.STRING_TYPE, True, True), - ('encoder', types.STRING_TYPE, True, True), - ('rg_track_gain', types.FLOAT_TYPE, True, True), - ('rg_track_peak', types.FLOAT_TYPE, True, True), - ('rg_album_gain', types.FLOAT_TYPE, True, True), - ('rg_album_peak', types.FLOAT_TYPE, True, True), + ('lyrics', types.String(), True, True), + ('comments', types.String(), True, True), + ('bpm', types.Integer(), True, True), + ('comp', types.Boolean(), True, True), + ('mb_trackid', types.String(), True, True), + ('mb_albumid', types.String(), True, True), + ('mb_artistid', types.String(), True, True), + ('mb_albumartistid', types.String(), True, True), + ('albumtype', types.String(), True, True), + ('label', types.String(), True, True), + ('acoustid_fingerprint', types.String(), True, True), + ('acoustid_id', types.String(), True, True), + ('mb_releasegroupid', types.String(), True, True), + ('asin', types.String(), True, True), + ('catalognum', types.String(), True, True), + ('script', types.String(), True, True), + ('language', types.String(), True, True), + ('country', types.String(), True, True), + ('albumstatus', types.String(), True, True), + ('media', types.String(), True, True), + ('albumdisambig', types.String(), True, True), + ('disctitle', types.String(), True, True), + ('encoder', types.String(), 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.PaddedInt(4), True, True), ('original_month', types.PaddedInt(2), True, True), ('original_day', types.PaddedInt(2), True, True), - ('length', types.FLOAT_TYPE, False, True), + ('length', types.Float(), False, True), ('bitrate', types.ScaledInt(1000, u'kbps'), False, True), - ('format', types.STRING_TYPE, False, True), + ('format', types.String(), False, True), ('samplerate', types.ScaledInt(1000, u'kHz'), False, True), - ('bitdepth', types.INT_TYPE, False, True), - ('channels', types.INT_TYPE, False, True), - ('mtime', DATE_TYPE, False, False), - ('added', DATE_TYPE, False, False), + ('bitdepth', types.Integer(), False, True), + ('channels', types.Integer(), False, True), + ('mtime', DateType(), False, False), + ('added', DateType(), 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]] @@ -164,36 +183,36 @@ 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', types.ID_TYPE, False), - ('artpath', PATH_TYPE, False), - ('added', DATE_TYPE, True), + ('id', types.Id(), False), + ('artpath', PathType(), False), + ('added', DateType(), True), - ('albumartist', types.STRING_TYPE, True), - ('albumartist_sort', types.STRING_TYPE, True), - ('albumartist_credit', types.STRING_TYPE, True), - ('album', types.STRING_TYPE, True), - ('genre', types.STRING_TYPE, True), + ('albumartist', types.String(), True), + ('albumartist_sort', types.String(), True), + ('albumartist_credit', types.String(), True), + ('album', types.String(), True), + ('genre', types.String(), True), ('year', types.PaddedInt(4), True), ('month', types.PaddedInt(2), True), ('day', types.PaddedInt(2), True), ('tracktotal', types.PaddedInt(2), True), ('disctotal', types.PaddedInt(2), True), - ('comp', types.BOOL_TYPE, True), - ('mb_albumid', types.STRING_TYPE, True), - ('mb_albumartistid', types.STRING_TYPE, True), - ('albumtype', types.STRING_TYPE, True), - ('label', types.STRING_TYPE, True), - ('mb_releasegroupid', types.STRING_TYPE, True), - ('asin', types.STRING_TYPE, True), - ('catalognum', types.STRING_TYPE, True), - ('script', types.STRING_TYPE, True), - ('language', types.STRING_TYPE, True), - ('country', types.STRING_TYPE, True), - ('albumstatus', types.STRING_TYPE, True), - ('media', types.STRING_TYPE, True), - ('albumdisambig', types.STRING_TYPE, True), - ('rg_album_gain', types.FLOAT_TYPE, True), - ('rg_album_peak', types.FLOAT_TYPE, True), + ('comp', types.Boolean(), True), + ('mb_albumid', types.String(), True), + ('mb_albumartistid', types.String(), True), + ('albumtype', types.String(), True), + ('label', types.String(), True), + ('mb_releasegroupid', types.String(), True), + ('asin', types.String(), True), + ('catalognum', types.String(), True), + ('script', types.String(), True), + ('language', types.String(), True), + ('country', types.String(), True), + ('albumstatus', types.String(), True), + ('media', types.String(), True), + ('albumdisambig', types.String(), True), + ('rg_album_gain', types.Float(), True), + ('rg_album_peak', types.Float(), True), ('original_year', types.PaddedInt(4), True), ('original_month', types.PaddedInt(2), True), ('original_day', types.PaddedInt(2), True), diff --git a/test/test_dbcore.py b/test/test_dbcore.py index e5e5f457e..f173ff08b 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -25,17 +25,12 @@ 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, - unicode) -INT_TYPE = dbcore.Type('INTEGER', dbcore.query.NumericQuery, - unicode) - class TestModel1(dbcore.Model): _table = 'test' _flex_table = 'testflex' _fields = { - 'id': ID_TYPE, - 'field_one': INT_TYPE, + 'id': dbcore.types.Id(), + 'field_one': dbcore.types.Integer(), } @classmethod @@ -51,9 +46,9 @@ class TestDatabase1(dbcore.Database): class TestModel2(TestModel1): _fields = { - 'id': ID_TYPE, - 'field_one': INT_TYPE, - 'field_two': INT_TYPE, + 'id': dbcore.types.Id(), + 'field_one': dbcore.types.Integer(), + 'field_two': dbcore.types.Integer(), } class TestDatabase2(dbcore.Database): @@ -62,10 +57,10 @@ class TestDatabase2(dbcore.Database): class TestModel3(TestModel1): _fields = { - 'id': ID_TYPE, - 'field_one': INT_TYPE, - 'field_two': INT_TYPE, - 'field_three': INT_TYPE, + 'id': dbcore.types.Id(), + 'field_one': dbcore.types.Integer(), + 'field_two': dbcore.types.Integer(), + 'field_three': dbcore.types.Integer(), } class TestDatabase3(dbcore.Database): @@ -74,11 +69,11 @@ class TestDatabase3(dbcore.Database): class TestModel4(TestModel1): _fields = { - 'id': ID_TYPE, - 'field_one': INT_TYPE, - 'field_two': INT_TYPE, - 'field_three': INT_TYPE, - 'field_four': INT_TYPE, + 'id': dbcore.types.Id(), + 'field_one': dbcore.types.Integer(), + 'field_two': dbcore.types.Integer(), + 'field_three': dbcore.types.Integer(), + 'field_four': dbcore.types.Integer(), } class TestDatabase4(dbcore.Database): @@ -89,8 +84,8 @@ class AnotherTestModel(TestModel1): _table = 'another' _flex_table = 'anotherflex' _fields = { - 'id': ID_TYPE, - 'foo': INT_TYPE, + 'id': dbcore.types.Id(), + 'foo': dbcore.types.Integer(), } class TestDatabaseTwoModels(dbcore.Database):