From dd0c70e01ca3cd81152a270685aacc2f829be096 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 13 Jul 2010 21:14:11 -0700 Subject: [PATCH 1/9] add triggers for dropping album entries consistently with items --- beets/library.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/beets/library.py b/beets/library.py index 819645493..6b2c85ca7 100644 --- a/beets/library.py +++ b/beets/library.py @@ -716,6 +716,8 @@ class Library(BaseLibrary): self._make_table('items', item_fields) self._make_table('albums', album_fields) + + self._make_triggers() def _make_table(self, table, fields): """Set up the schema of the library file. fields is a list of @@ -753,6 +755,31 @@ class Library(BaseLibrary): self.conn.executescript(setup_sql) self.conn.commit() + def _make_triggers(self): + """Setup triggers for the database to keep the tables + consistent. + """ + # Set up triggers for dropping album info rows when no longer + # needed. + trigger_sql = """ + WHEN + ((SELECT id FROM items WHERE album=OLD.album AND artist=OLD.artist) + IS NULL) + BEGIN + DELETE FROM albums WHERE + album=OLD.album AND artist=OLD.artist; + END; + """ + self.conn.execute(""" + CREATE TRIGGER IF NOT EXISTS delete_album + AFTER DELETE ON items + """ + trigger_sql) + self.conn.execute(""" + CREATE TRIGGER IF NOT EXISTS change_album + AFTER UPDATE OF album, artist ON items + """ + trigger_sql) + self.conn.commit() + def destination(self, item): """Returns the path in the library directory designated for item item (i.e., where the file ought to be). From f1870b79412c8b694920f44ee0502d8d71ca1229 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 13 Jul 2010 21:22:55 -0700 Subject: [PATCH 2/9] AlbumInfo implementation now uses opaque "ident" field (concretely: row id) --- beets/library.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/beets/library.py b/beets/library.py index 6b2c85ca7..cfbf6b7ad 100644 --- a/beets/library.py +++ b/beets/library.py @@ -533,22 +533,21 @@ class ResultIterator(object): class AlbumInfo(object): """Provides access to information about albums stored in a library. """ - def __init__(self, library, album, artist): + def __init__(self, library, ident): self._library = library - self._album = album - self._artist = artist + self._ident = ident def __getattr__(self, key): """Get an album field's value.""" if key in ALBUM_KEYS: - return self._library._album_get(self._album, self._artist, key) + 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._album, self._artist, key, value) + self._library._album_set(self._ident, key, value) else: super(AlbumInfo, self).__setattr__(key, value) @@ -685,14 +684,14 @@ class BaseLibrary(object): """Given an artist and album name, return an AlbumInfo proxy object for the given item's album. """ - return AlbumInfo(self, item.artist, item.album) + return AlbumInfo(self, (item.artist, item.album)) - def _album_get(self, artist, album, key): + def _album_get(self, ident, key): """For the album specified, returns the value associated with the key.""" raise NotImplementedError() - def _album_set(self, artist, album, key, value): + def _album_set(self, ident, key, value): """Sets the indicated album's value for key.""" raise NotImplementedError() @@ -939,16 +938,19 @@ class Library(BaseLibrary): sql = 'SELECT id FROM albums WHERE artist=? AND album=?' c = self.conn.execute(sql, (item.artist, item.album)) row = c.fetchone() - if not row: + if row: + album_id = row[0] + else: sql = 'INSERT INTO albums (artist, album) VALUES (?, ?)' - self.conn.execute(sql, (item.artist, item.album)) - return super(Library, self).albuminfo(item) + c = self.conn.execute(sql, (item.artist, item.album)) + album_id = c.lastrowid + return AlbumInfo(self, album_id) - def _album_get(self, artist, album, key): - sql = 'SELECT %s FROM albums WHERE artist=? AND album=?' % key - c = self.conn.execute(sql, (artist, album)) + 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 _album_set(self, artist, album, key, value): - sql = 'UPDATE albums SET %s=? WHERE artist=? AND album=?' % key - self.conn.execute(sql, (value, artist, album)) + def _album_set(self, album_id, key, value): + sql = 'UPDATE albums SET %s=? WHERE id=?' % key + self.conn.execute(sql, (value, album_id)) From 5bb064a8604f09fff08b2bc58eb94fbdc71a3fdb Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 13 Jul 2010 22:00:42 -0700 Subject: [PATCH 3/9] configurable album art path construction --- beets/library.py | 15 +++++++++++++-- beets/ui/__init__.py | 6 +++++- test/rsrc/test.blb | Bin 5120 -> 7168 bytes test/test_db.py | 16 +++++++++++++++- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/beets/library.py b/beets/library.py index cfbf6b7ad..9448d234c 100644 --- a/beets/library.py +++ b/beets/library.py @@ -703,11 +703,13 @@ class Library(BaseLibrary): def __init__(self, path='library.blb', directory='~/Music', path_format='$artist/$album/$track $title', + art_filename='cover', item_fields=ITEM_FIELDS, album_fields=ALBUM_FIELDS): self.path = path self.directory = directory self.path_format = path_format + self.art_filename = art_filename self.conn = sqlite3.connect(self.path) self.conn.row_factory = sqlite3.Row @@ -786,8 +788,8 @@ class Library(BaseLibrary): libpath = self.directory subpath_tmpl = Template(self.path_format) - # build the mapping for substitution in the path template, beginning - # with the values from the database + # Build the mapping for substitution in the path template, + # beginning with the values from the database. mapping = {} for key in ITEM_KEYS_META: value = getattr(item, key) @@ -814,6 +816,15 @@ class Library(BaseLibrary): return _normpath(os.path.join(libpath, subpath)) + def art_path(self, item, image): + """Returns a path to the destination for the album art image + for the item's album. `image` is the path of the image that + will be moved there (used for its extension). + """ + item_dir = os.path.dirname(self.destination(item)) + _, ext = os.path.splitext(image) + dest = os.path.join(item_dir, self.art_filename + ext) + return dest # Main interface. diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index e504d187f..0f53595d4 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -31,6 +31,7 @@ CONFIG_FILE = os.path.expanduser('~/.beetsconfig') DEFAULT_LIBRARY = '~/.beetsmusic.blb' DEFAULT_DIRECTORY = '~/Music' DEFAULT_PATH_FORMAT = '$artist/$album/$track $title' +DEFAULT_ART_FILENAME = 'cover' # UI exception. Commands should throw this in order to display @@ -369,9 +370,12 @@ def main(): config_val(config, 'beets', 'directory', DEFAULT_DIRECTORY) path_format = options.path_format or \ config_val(config, 'beets', 'path_format', DEFAULT_PATH_FORMAT) + art_filename = \ + config_val(config, 'beets', 'art_filename', DEFAULT_ART_FILENAME) lib = library.Library(os.path.expanduser(libpath), directory, - path_format) + path_format, + art_filename) # Invoke the subcommand. try: diff --git a/test/rsrc/test.blb b/test/rsrc/test.blb index bdbf362bccfd1fca00947db1a85f5dd49d5cadb7..1559b449b130b979af4b5023b2369384de1e1ec8 100644 GIT binary patch delta 764 zcmZqBXt0MjoYHl%ubC9cJh^s>;zkX9~V6^OTp34MIo`M zB(t~#A_!(`A{plytl$^w Date: Tue, 13 Jul 2010 22:34:52 -0700 Subject: [PATCH 4/9] autotagger -r switch now fetches and places album art --- beets/library.py | 3 ++- beets/ui/commands.py | 28 ++++++++++++++++++++++------ test/test_db.py | 3 ++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/beets/library.py b/beets/library.py index 9448d234c..1a72508d2 100644 --- a/beets/library.py +++ b/beets/library.py @@ -821,11 +821,12 @@ class Library(BaseLibrary): for the item's album. `image` is the path of the image that will be moved there (used for its extension). """ - item_dir = os.path.dirname(self.destination(item)) + item_dir = os.path.dirname(item.path) _, ext = os.path.splitext(image) dest = os.path.join(item_dir, self.art_filename + ext) return dest + # Main interface. def add(self, item, copy=False): diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 2c434c6e0..ae29f4ebe 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -18,12 +18,15 @@ interface. import os import logging +import shutil from beets import ui from beets.ui import print_ from beets import autotag from beets import library from beets.mediafile import UnreadableFileError, FileTypeError +import beets.autotag.art +autotag.art = beets.autotag.art # Global logger. log = logging.getLogger('beets') @@ -147,12 +150,12 @@ def tag_log(logfile, status, items): path = os.path.commonprefix([item.path for item in items]) print >>logfile, status, os.path.dirname(path) -def tag_album(items, lib, copy=True, write=True, logfile=None): +def tag_album(items, lib, copy=True, write=True, logfile=None, art=False): """Import items into lib, tagging them as an album. If copy, then items are copied into the destination directory. If write, then new metadata is written back to the files' tags. If logfile is provided, then a log message will be added there if the album is - untaggable. + untaggable. If art, then try to download album art for the album. """ # Try to get candidate metadata. search_artist, search_album = None, None @@ -228,14 +231,25 @@ def tag_album(items, lib, copy=True, write=True, logfile=None): # locking while we do the copying and tag updates. for item in items: lib.add(item) + + # Get album art. + if art: + artpath = autotag.art.art_for_album(info) + if artpath: + artdest = lib.art_path(items[0], artpath) + #fixme -- move if possible? + shutil.copy(artpath, artdest) + lib.albuminfo(items[0]).artpath = artdest -def import_files(lib, paths, copy=True, write=True, autot=True, logpath=None): +def import_files(lib, paths, copy=True, write=True, autot=True, + logpath=None, art=False): """Import the files in the given list of paths, tagging each leaf directory as an album. If copy, then the files are copied into the library folder. If write, then new metadata is written to the files themselves. If not autot, then just import the files without attempting to tag. If logpath is provided, then untaggable - albums will be logged there. + albums will be logged there. If art, then try to download album art + for each album. """ if logpath: logfile = open(logpath, 'w') @@ -257,7 +271,7 @@ def import_files(lib, paths, copy=True, write=True, autot=True, logpath=None): first = False # Infer tags. - tag_album(album, lib, copy, write, logfile) + tag_album(album, lib, copy, write, logfile, art) # Write the database after each album. lib.save() @@ -316,6 +330,8 @@ import_cmd.parser.add_option('-A', '--noautotag', action='store_false', help="don't infer tags for imported files (opposite of -a)") import_cmd.parser.add_option('-l', '--log', dest='logpath', help='file to log untaggable albums for later review') +import_cmd.parser.add_option('-r', '--art', action='store_true', + default=None, help="try to download album art") def import_func(lib, config, opts, args): copy = opts.copy if opts.copy is not None else \ ui.config_val(config, 'beets', 'import_copy', @@ -324,7 +340,7 @@ def import_func(lib, config, opts, args): ui.config_val(config, 'beets', 'import_write', DEFAULT_IMPORT_WRITE, bool) autot = opts.autotag if opts.autotag is not None else DEFAULT_IMPORT_AUTOT - import_files(lib, args, copy, write, autot, opts.logpath) + import_files(lib, args, copy, write, autot, opts.logpath, opts.art) import_cmd.func = import_func default_commands.append(import_cmd) diff --git a/test/test_db.py b/test/test_db.py index d353ba425..7f2d72ae7 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -251,6 +251,7 @@ class ArtDestinationTest(unittest.TestCase): def setUp(self): self.lib = beets.library.Library(':memory:') self.i = item() + self.i.path = '/some/music/file.mp3' self.lib.art_filename = 'artimage' def test_art_filename_respects_setting(self): @@ -259,7 +260,7 @@ class ArtDestinationTest(unittest.TestCase): def test_art_path_in_item_dir(self): art = self.lib.art_path(self.i, 'something.jpg') - track = self.lib.destination(self.i) + track = self.i.path self.assertEqual(os.path.dirname(art), os.path.dirname(track)) class MigrationTest(unittest.TestCase): From e7e7ee64b0d8345182e64f8d1393fbb40344aac5 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 13 Jul 2010 23:43:49 -0700 Subject: [PATCH 5/9] albuminfo() can now take an album id as well as an item --- beets/library.py | 24 +++++++++++++++--------- test/test_db.py | 5 +++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/beets/library.py b/beets/library.py index 1a72508d2..4cc66e6b0 100644 --- a/beets/library.py +++ b/beets/library.py @@ -945,17 +945,23 @@ class Library(BaseLibrary): # Album information. 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] + """Get an album info proxy object given either an item or an + album id. + """ + if isinstance(item, int): + album_id = item else: - sql = 'INSERT INTO albums (artist, album) VALUES (?, ?)' + # 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)) - album_id = c.lastrowid + row = c.fetchone() + if row: + album_id = row[0] + 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) def _album_get(self, album_id, key): diff --git a/test/test_db.py b/test/test_db.py index 7f2d72ae7..0d275cf3d 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -409,6 +409,11 @@ class AlbumInfoTest(unittest.TestCase): # Cursor should only return one row. self.assertNotEqual(c.fetchone(), None) self.assertEqual(c.fetchone(), None) + + def test_albuminfo_by_albumid(self): + ai = self.lib.albuminfo(self.i) + ai = self.lib.albuminfo(ai.id) + self.assertEqual(ai.artist, self.i.artist) def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From 54df8a7b430634e3831efa79e782562bb22ce2ce Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 13 Jul 2010 23:54:47 -0700 Subject: [PATCH 6/9] config option and negative flag for getting art (defaults to true) --- beets/ui/commands.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index ae29f4ebe..62b63d448 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -38,9 +38,10 @@ default_commands = [] # import: Autotagger and importer. -DEFAULT_IMPORT_COPY = True +DEFAULT_IMPORT_COPY = True DEFAULT_IMPORT_WRITE = True DEFAULT_IMPORT_AUTOT = True +DEFAULT_IMPORT_ART = True def show_change(cur_artist, cur_album, items, info, dist): """Print out a representation of the changes that will be made if @@ -328,10 +329,12 @@ import_cmd.parser.add_option('-a', '--autotag', action='store_true', import_cmd.parser.add_option('-A', '--noautotag', action='store_false', dest='autotag', help="don't infer tags for imported files (opposite of -a)") -import_cmd.parser.add_option('-l', '--log', dest='logpath', - help='file to log untaggable albums for later review') import_cmd.parser.add_option('-r', '--art', action='store_true', default=None, help="try to download album art") +import_cmd.parser.add_option('-R', '--noart', action='store_false', + dest='art', help="don't album art (opposite of -r)") +import_cmd.parser.add_option('-l', '--log', dest='logpath', + help='file to log untaggable albums for later review') def import_func(lib, config, opts, args): copy = opts.copy if opts.copy is not None else \ ui.config_val(config, 'beets', 'import_copy', @@ -340,7 +343,10 @@ def import_func(lib, config, opts, args): ui.config_val(config, 'beets', 'import_write', DEFAULT_IMPORT_WRITE, bool) autot = opts.autotag if opts.autotag is not None else DEFAULT_IMPORT_AUTOT - import_files(lib, args, copy, write, autot, opts.logpath, opts.art) + art = opts.art if opts.art is not None else \ + ui.config_val(config, 'beets', 'import_art', + DEFAULT_IMPORT_ART, bool) + import_files(lib, args, copy, write, autot, opts.logpath, art) import_cmd.func = import_func default_commands.append(import_cmd) From 5335bc6b8a94c31a2485a1be0d2b262e3d797ea3 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 14 Jul 2010 00:01:54 -0700 Subject: [PATCH 7/9] log a message when no art is found --- beets/ui/commands.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 62b63d448..71164bd5a 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -234,13 +234,17 @@ def tag_album(items, lib, copy=True, write=True, logfile=None, art=False): lib.add(item) # Get album art. - if art: - artpath = autotag.art.art_for_album(info) - if artpath: - artdest = lib.art_path(items[0], artpath) - #fixme -- move if possible? - shutil.copy(artpath, artdest) - lib.albuminfo(items[0]).artpath = artdest + if info is not CHOICE_ASIS: + if art: + artpath = autotag.art.art_for_album(info) + if artpath: + artdest = lib.art_path(items[0], artpath) + #fixme -- move if possible? + shutil.copy(artpath, artdest) + lib.albuminfo(items[0]).artpath = artdest + else: + log.info('no album art found for %s - %s' % + (info['artist'], info['album'])) def import_files(lib, paths, copy=True, write=True, autot=True, logpath=None, art=False): From fe892cc2680b2d7a903a2eed5b81a0288a784ce2 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 14 Jul 2010 11:55:19 -0700 Subject: [PATCH 8/9] attempt at managing album info table in Python instead of with triggers I think this is the end of the road for the design that treats albums as a lightweight hanger-on to items. That is, we attempted to keep the interface strictly item-focused; album information was created and deleted on the fly in response to creation and deletion of items. I now believe that this was ultimately a bad idea and can only lead to unexpected behavior and complex implementation. It's time to start over. --- beets/library.py | 93 +++++++++++++++++++++++++++----------------- beets/ui/commands.py | 4 +- test/test_db.py | 40 +++++++++++++++++++ test/test_files.py | 1 + 4 files changed, 100 insertions(+), 38 deletions(-) diff --git a/beets/library.py b/beets/library.py index 4cc66e6b0..90f85889a 100644 --- a/beets/library.py +++ b/beets/library.py @@ -115,6 +115,13 @@ def _ancestry(path): out.insert(0, path) return out +def _mkdirall(path): + """Like mkdir -p, make directories for the entire ancestry of path. + """ + for ancestor in _ancestry(path): + if not os.path.isdir(ancestor): + os.mkdir(ancestor) + def _components(path): """Return a list of the path components in path. For instance: >>> _components('/a/b/c') @@ -273,12 +280,9 @@ class Item(object): """ dest = library.destination(self) - # Create necessary ancestry for the move. Like os.renames but only - # halfway. - for ancestor in _ancestry(dest): - if not os.path.isdir(ancestor): - os.mkdir(ancestor) - + # Create necessary ancestry for the move. + _mkdirall(dest) + if copy: shutil.copy(self.path, dest) else: @@ -717,8 +721,6 @@ class Library(BaseLibrary): self._make_table('items', item_fields) self._make_table('albums', album_fields) - - self._make_triggers() def _make_table(self, table, fields): """Set up the schema of the library file. fields is a list of @@ -756,31 +758,6 @@ class Library(BaseLibrary): self.conn.executescript(setup_sql) self.conn.commit() - def _make_triggers(self): - """Setup triggers for the database to keep the tables - consistent. - """ - # Set up triggers for dropping album info rows when no longer - # needed. - trigger_sql = """ - WHEN - ((SELECT id FROM items WHERE album=OLD.album AND artist=OLD.artist) - IS NULL) - BEGIN - DELETE FROM albums WHERE - album=OLD.album AND artist=OLD.artist; - END; - """ - self.conn.execute(""" - CREATE TRIGGER IF NOT EXISTS delete_album - AFTER DELETE ON items - """ + trigger_sql) - self.conn.execute(""" - CREATE TRIGGER IF NOT EXISTS change_album - AFTER UPDATE OF album, artist ON items - """ + trigger_sql) - self.conn.commit() - def destination(self, item): """Returns the path in the library directory designated for item item (i.e., where the file ought to be). @@ -879,10 +856,13 @@ class Library(BaseLibrary): # build assignments for query assignments = '' subvars = [] + album_changed = False for key in ITEM_KEYS: if (key != 'id') and (item.dirty[key] or store_all): assignments += key + '=?,' subvars.append(getattr(item, key)) + if key in ('artist', 'album'): + album_changed = True if not assignments: # nothing to store (i.e., nothing was dirty) @@ -890,6 +870,12 @@ class Library(BaseLibrary): assignments = assignments[:-1] # knock off last , + # Get old artist and album for cleanup. + old_artist, old_album = self.conn.execute( + 'SELECT artist, album FROM items WHERE id=?', + (item.id,) + ).fetchone() + # finish the query query = 'UPDATE items SET ' + assignments + ' WHERE id=?' subvars.append(item.id) @@ -897,9 +883,45 @@ class Library(BaseLibrary): self.conn.execute(query, subvars) item._clear_dirty() - def remove(self, item): - self.conn.execute('DELETE FROM items WHERE id=?', (item.id,)) + # Clean up album. + album_row = self._cleanup_album(old_artist, old_album) + def remove(self, item, delete=False): + # Get album and artist so we can clean up the album entry. + artist, album = self.conn.execute( + 'SELECT artist, album FROM items WHERE id=?', + (item.id,) + ).fetchone() + + self.conn.execute('DELETE FROM items WHERE id=?', (item.id,)) + if delete: + os.unlink(item.path) + + # Clean up album. + album_row = self._cleanup_album(artist, album) + if delete and album_row and album_row['artpath']: + # When deleting items, delete their art as well. + os.unlink(album_row['artpath']) + + def _cleanup_album(self, artist, album): + """If there are no items with the album specified, then removes + the corresponding album entry and returns it. Otherwise, returns + None. + """ + c = self.conn.execute( + 'SELECT id FROM items WHERE artist=? AND album=?', + (artist, album) + ) + if c.fetchone() is None: + album_row = self.conn.execute( + 'SELECT * FROM albums WHERE artist=? AND album=?', + (artist, album) + ).fetchone() + self.conn.execute( + 'DELETE FROM albums WHERE artist=? AND album=?', + (artist, album) + ) + return album_row # Browsing. @@ -972,3 +994,4 @@ class Library(BaseLibrary): 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/beets/ui/commands.py b/beets/ui/commands.py index 71164bd5a..b54e64b71 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -411,9 +411,7 @@ def remove_items(lib, query, album, delete=False): # Remove and delete. for item in items: - lib.remove(item) - if delete: - os.unlink(item.path) + lib.remove(item, delete) lib.save() remove_cmd = ui.Subcommand('remove', diff --git a/test/test_db.py b/test/test_db.py index 0d275cf3d..593d7aa9a 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -19,6 +19,7 @@ import unittest import sys import os import sqlite3 +import shutil sys.path.append('..') import beets.library @@ -263,6 +264,45 @@ class ArtDestinationTest(unittest.TestCase): track = self.i.path self.assertEqual(os.path.dirname(art), os.path.dirname(track)) +class ArtFileTest(unittest.TestCase): + def _touch(self, path): + # Create file if it doesn't exist. + open(path, 'a').close() + + def setUp(self): + # Make library and item. + self.lib = beets.library.Library(':memory:') + self.libdir = os.path.join('rsrc', 'testlibdir') + self.lib.directory = self.libdir + self.i = item() + self.i.path = self.lib.destination(self.i) + # Make a file. + beets.library._mkdirall(self.i.path) + self._touch(self.i.path) + self.lib.add(self.i) + # Make an art file too. + self.art = self.lib.art_path(self.i, 'something.jpg') + self._touch(self.art) + self.lib.albuminfo(self.i).artpath = self.art + def tearDown(self): + if os.path.exists(self.libdir): + shutil.rmtree(self.libdir) + + def test_art_deleted_when_items_deleted(self): + self.assertTrue(os.path.exists(self.art)) + self.lib.remove(self.i, True) + self.assertFalse(os.path.exists(self.art)) + + def test_art_moves_with_last_album(self): + self.assertTrue(os.path.exists(self.art)) + oldpath = self.i.path + self.i.artist = 'newArtist' + self.i.move(self.lib) + self.assertNotEqual(self.i.path, oldpath) + self.assertFalse(os.path.exists(self.art)) + newart = self.lib.art_path(self.i) + self.assertTrue(os.path.exists(newart)) + class MigrationTest(unittest.TestCase): """Tests the ability to change the database schema between versions. diff --git a/test/test_files.py b/test/test_files.py index 8be524141..138d9ce5e 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -101,3 +101,4 @@ def suite(): if __name__ == '__main__': unittest.main(defaultTest='suite') + From f181835e2a91435cda31163d6b4a133828e11ec7 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 14 Jul 2010 23:36:35 -0700 Subject: [PATCH 9/9] abandon attempt at making albums implicit from items --HG-- branch : implalbum extra : close : 1