mirror of
https://github.com/beetbox/beets.git
synced 2025-12-15 21:14:19 +01:00
beginnings of explicit album management
This commit is contained in:
parent
722e0ea2b7
commit
3006f9953c
3 changed files with 78 additions and 116 deletions
125
beets/library.py
125
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))
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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__)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue