diff --git a/beets/library.py b/beets/library.py index fddedc0e2..a7d39699c 100644 --- a/beets/library.py +++ b/beets/library.py @@ -35,6 +35,8 @@ MAX_FILENAME_LENGTH = 200 ITEM_FIELDS = [ ('id', 'integer primary key', False, False), ('path', 'text', False, False), + ('album_id', 'int', False, False), + ('title', 'text', True, True), ('artist', 'text', True, True), ('album', 'text', True, True), @@ -55,6 +57,7 @@ ITEM_FIELDS = [ ('mb_trackid', 'text', True, True), ('mb_albumid', 'text', True, True), ('mb_artistid', 'text', True, True), + ('length', 'real', False, True), ('bitrate', 'int', False, True), ] @@ -182,7 +185,10 @@ class Item(object): def from_path(cls, path): """Creates a new item from the media file at the specified path. """ - i = cls({}) + # Initiate with values that aren't read from files. + i = cls({ + 'album_id': None, + }) i.read(_unicode_path(path)) return i @@ -528,30 +534,6 @@ class ResultIterator(object): return Item(row) -# Album information proxy objects. - -class AlbumInfo(object): - """Provides access to information about albums stored in a library. - """ - def __init__(self, library, ident): - self._library = library - self._ident = ident - - def __getattr__(self, key): - """Get an album field's value.""" - if key in ALBUM_KEYS: - return self._library._album_get(self._ident, key) - else: - return getattr(self, key) - - def __setattr__(self, key, value): - """Set an album field.""" - if key in ALBUM_KEYS: - self._library._album_set(self._ident, key, value) - else: - super(AlbumInfo, self).__setattr__(key, value) - - # An abstract library. class BaseLibrary(object): @@ -675,27 +657,6 @@ class BaseLibrary(object): return sorted(out, compare) - # Album information. - # Provides access to information about items at an album - # granularity. AlbumInfo proxy objects are used to access fields; - # they invoke _album_get and _album_set. - - def albuminfo(self, item): - """Given an artist and album name, return an AlbumInfo proxy - object for the given item's album. - """ - return AlbumInfo(self, (item.artist, item.album)) - - def _album_get(self, ident, key): - """For the album specified, returns the value associated with - the key.""" - raise NotImplementedError() - - def _album_set(self, ident, key, value): - """Sets the indicated album's value for key.""" - raise NotImplementedError() - - # Concrete DB-backed library. class Library(BaseLibrary): @@ -903,27 +864,59 @@ class Library(BaseLibrary): return ResultIterator(c, self) - # Album information. + # Album-level data. - def albuminfo(self, item): - # Lazily create a row in the albums table if one doesn't - # exist. - sql = 'SELECT id FROM albums WHERE artist=? AND album=?' - c = self.conn.execute(sql, (item.artist, item.album)) - row = c.fetchone() - if row: - album_id = row[0] + class _AlbumInfo(object): + """Provides access to information about albums stored in a + library. + """ + def __init__(self, library, album_id): + self._library = library + self._id = album_id + + def __getattr__(self, key): + if key == 'id': + return self._id + elif key in ALBUM_KEYS: + sql = 'SELECT %s FROM albums WHERE id=?' % key + c = self._library.conn.execute(sql, (self._id,)) + return c.fetchone()[0] + else: + return getattr(self, key) + + def __setattr__(self, key, value): + if key in ALBUM_KEYS: + sql = 'UPDATE albums SET %s=? WHERE id=?' % key + self._library.conn.execute(sql, (value, self._id)) + else: + object.__setattr__(self, key, value) + + def get_album(self, item_or_id): + """Given an album ID or an item associated with an album, + return an _AlbumInfo object for the album. + """ + if isinstance(item_or_id, int): + album_id = item_or_id else: - sql = 'INSERT INTO albums (artist, album) VALUES (?, ?)' - c = self.conn.execute(sql, (item.artist, item.album)) - album_id = c.lastrowid - return AlbumInfo(self, album_id) + album_id = item_or_id.album_id + if album_id is None: + return None + return self._AlbumInfo(self, album_id) - def _album_get(self, album_id, key): - sql = 'SELECT %s FROM albums WHERE id=?' % key - c = self.conn.execute(sql, (album_id,)) - return c.fetchone()[0] + def add_album(self, artist, album, items=()): + """Create a new album in the database with the given metadata. + If items are provided, they are also added and associated with + the album. Returns an _AlbumInfo object. + """ + c = self.conn.execute( + 'INSERT INTO albums (artist, album) VALUES (?, ?)', + (artist, album) + ) + albuminfo = self._AlbumInfo(self, c.lastrowid) + + for item in items: + item.album_id = albuminfo.id + self.add(item) + + return albuminfo - def _album_set(self, album_id, key, value): - sql = 'UPDATE albums SET %s=? WHERE id=?' % key - self.conn.execute(sql, (value, album_id)) diff --git a/test/rsrc/test.blb b/test/rsrc/test.blb index bdbf362bc..7963086c0 100644 Binary files a/test/rsrc/test.blb and b/test/rsrc/test.blb differ diff --git a/test/test_db.py b/test/test_db.py index 3cd386827..ded7296e4 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -49,6 +49,7 @@ def item(): return beets.library.Item({ 'mb_trackid': 'someID-1', 'mb_albumid': 'someID-2', 'mb_artistid': 'someID-3', + 'album_id': None, }) np = beets.library._normpath @@ -325,68 +326,24 @@ class AlbumInfoTest(unittest.TestCase): def setUp(self): self.lib = beets.library.Library(':memory:') self.i = item() - self.lib.add(self.i) + self.lib.add_album(self.i.artist, self.i.album, (self.i,)) def test_albuminfo_reflects_metadata(self): - ai = self.lib.albuminfo(self.i) + ai = self.lib.get_album(self.i) self.assertEqual(ai.artist, self.i.artist) self.assertEqual(ai.album, self.i.album) def test_albuminfo_stores_art(self): - ai = self.lib.albuminfo(self.i) + ai = self.lib.get_album(self.i) ai.artpath = '/my/great/art' - new_ai = self.lib.albuminfo(self.i) + new_ai = self.lib.get_album(self.i) self.assertEqual(new_ai.artpath, '/my/great/art') - def test_albuminfo_removed_when_last_item_removed(self): - self.lib.albuminfo(self.i) - c = self.lib.conn.cursor() - c.execute('select * from albums where album=?', (self.i.album,)) - self.assertNotEqual(c.fetchone(), None) - - self.lib.remove(self.i) - - c = self.lib.conn.cursor() - c.execute('select * from albums where album=?', (self.i.album,)) - self.assertEqual(c.fetchone(), None) - - def test_albuminfo_changes_when_item_field_changes(self): - self.lib.albuminfo(self.i) - self.i.album = 'anotherAlbum' - self.lib.store(self.i) - - ai = self.lib.albuminfo(self.i) - self.assertEqual(ai.album, 'anotherAlbum') - - def test_old_albuminfo_removed_when_last_item_changes(self): - oldalbum = self.i.album - self.lib.albuminfo(self.i) - self.i.album = 'anotherAlbum' - self.lib.store(self.i) - - c = self.lib.conn.cursor() - c.execute('select * from albums where album=?', (oldalbum,)) - self.assertEqual(c.fetchone(), None) - - def test_splitting_album_leaves_albuminfo_for_both(self): - i2 = item() - self.lib.add(i2) - self.lib.albuminfo(self.i) - self.lib.albuminfo(i2) - - i2.artist = 'anotherArtist' - self.lib.store(i2) - - ai = self.lib.albuminfo(self.i) - self.assertEqual(ai.artist, self.i.artist) - ai = self.lib.albuminfo(i2) - self.assertEqual(ai.artist, 'anotherArtist') - def test_albuminfo_for_two_items_doesnt_duplicate_row(self): i2 = item() self.lib.add(i2) - self.lib.albuminfo(self.i) - self.lib.albuminfo(i2) + self.lib.get_album(self.i) + self.lib.get_album(i2) c = self.lib.conn.cursor() c.execute('select * from albums where album=?', (self.i.album,)) @@ -394,6 +351,18 @@ class AlbumInfoTest(unittest.TestCase): self.assertNotEqual(c.fetchone(), None) self.assertEqual(c.fetchone(), None) + def test_individual_tracks_have_no_albuminfo(self): + i2 = item() + i2.album = 'aTotallyDifferentAlbum' + self.lib.add(i2) + ai = self.lib.get_album(i2) + self.assertEqual(ai, None) + + def test_get_album_by_id(self): + ai = self.lib.get_album(self.i) + ai = self.lib.get_album(self.i.id) + self.assertNotEqual(ai, None) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__)