beginnings of explicit album management

This commit is contained in:
Adrian Sampson 2010-07-14 12:40:25 -07:00
parent 722e0ea2b7
commit 3006f9953c
3 changed files with 78 additions and 116 deletions

View file

@ -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))

Binary file not shown.

View file

@ -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__)