diff --git a/beets/library.py b/beets/library.py index 640becdec..9613cb690 100644 --- a/beets/library.py +++ b/beets/library.py @@ -385,7 +385,7 @@ class Item(object): # Dealing with files themselves. - def move(self, library, copy=False): + def move(self, library, copy=False, in_album=False): """Move the item to its designated location within the library directory (provided by destination()). Subdirectories are created as needed. If the operation succeeds, the item's path @@ -393,6 +393,11 @@ class Item(object): If copy is True, moving the file is copied rather than moved. + If in_album is True, then the track is treated as part of an + album even if it does not yet have an album_id associated with + it. (This allows items to be moved before they are added to the + database, a performance optimization.) + Passes on appropriate exceptions if directories cannot be created or moving/copying fails. @@ -400,7 +405,7 @@ class Item(object): library.save() after this method in order to keep on-disk data consistent. """ - dest = library.destination(self) + dest = library.destination(self, in_album=in_album) # Create necessary ancestry for the move. _mkdirall(dest) @@ -908,14 +913,15 @@ class Library(BaseLibrary): self.conn.executescript(setup_sql) self.conn.commit() - def destination(self, item, pathmod=None): + def destination(self, item, pathmod=None, in_album=False): """Returns the path in the library directory designated for item - item (i.e., where the file ought to be). + item (i.e., where the file ought to be). in_album forces the + item to be treated as part of an album. """ pathmod = pathmod or os.path # Use a path format based on the album type, if available. - if not item.album_id: + if not item.album_id and not in_album: # Singleton track. Never use the "album" formats. if 'singleton' in self.path_formats: path_format = self.path_formats['singleton'] diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 3b6f93c7c..e66fdeafc 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -563,7 +563,7 @@ def apply_choices(lib, copy, write, art, delete, progress): for item in task.items] for item in task.items: if copy: - item.move(lib, True) + item.move(lib, True, task.choice_flag != CHOICE_TRACKS) if write and task.choice_flag == CHOICE_ALBUM: item.write() @@ -621,7 +621,7 @@ def simple_import(lib, paths, copy, delete, resume): if delete: old_paths = [os.path.realpath(item.path) for item in task.items] for item in task.items: - item.move(lib, True) + item.move(lib, True, True) album = lib.add_album(task.items, True) lib.save() diff --git a/test/_common.py b/test/_common.py index c424bbfee..604999c37 100644 --- a/test/_common.py +++ b/test/_common.py @@ -1,6 +1,7 @@ """Some common functionality for beets' test cases.""" import time import sys +import os # Mangle the search path to include the beets sources. sys.path.insert(0, '..') @@ -126,3 +127,15 @@ class DummyIO(object): def restore(self): sys.stdin = sys.__stdin__ sys.stdout = sys.__stdout__ + + +# Mixin for additional assertions. + +class ExtraAsserts(object): + def assertExists(self, path): + self.assertTrue(os.path.exists(path), + 'file does not exist: %s' % path) + + def assertNotExists(self, path): + self.assertFalse(os.path.exists(path), + 'file exists: %s' % path) diff --git a/test/test_ui.py b/test/test_ui.py index bdf5d58a5..176fc252e 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -130,15 +130,21 @@ class ImportTest(unittest.TestCase): paths = self._run_import(['sometrack'], delete=True) self.assertFalse(os.path.exists(paths[0])) -class ImportApplyTest(unittest.TestCase): +class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): def setUp(self): self.libdir = os.path.join('rsrc', 'testlibdir') os.mkdir(self.libdir) self.lib = library.Library(':memory:', self.libdir) + self.lib.path_formats = { + 'default': 'one', + 'comp': 'two', + 'singleton': 'three', + } self.srcpath = os.path.join(self.libdir, 'srcfile.mp3') shutil.copy(os.path.join('rsrc', 'full.mp3'), self.srcpath) self.i = library.Item.from_path(self.srcpath) + self.i.comp = False trackinfo = {'title': 'one', 'artist': 'some artist', 'track': 1, 'length': 1, 'id': 'trackid'} @@ -155,24 +161,56 @@ class ImportApplyTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.libdir) - def call_apply(self, coro, items, info): + def _call_apply(self, coro, items, info): task = commands.ImportTask(None, None, None) task.set_choice((info, items)) coro.send(task) + def _call_apply_choice(self, coro, items, choice): + task = commands.ImportTask(None, None, items) + task.set_choice(choice) + coro.send(task) + def test_apply_no_delete(self): coro = commands.apply_choices(self.lib, True, False, False, False, False) coro.next() # Prime coroutine. - self.call_apply(coro, [self.i], self.info) - self.assertTrue(os.path.exists(self.srcpath)) + self._call_apply(coro, [self.i], self.info) + self.assertExists(self.srcpath) def test_apply_with_delete(self): coro = commands.apply_choices(self.lib, True, False, False, True, False) coro.next() # Prime coroutine. - self.call_apply(coro, [self.i], self.info) - self.assertFalse(os.path.exists(self.srcpath)) + self._call_apply(coro, [self.i], self.info) + self.assertNotExists(self.srcpath) + + def test_apply_asis_uses_album_path(self): + coro = commands.apply_choices(self.lib, True, False, False, + False, False) + coro.next() # Prime coroutine. + self._call_apply_choice(coro, [self.i], commands.CHOICE_ASIS) + self.assertExists( + os.path.join(self.libdir, self.lib.path_formats['default']+'.mp3') + ) + + def test_apply_match_uses_album_path(self): + coro = commands.apply_choices(self.lib, True, False, False, + False, False) + coro.next() # Prime coroutine. + self._call_apply(coro, [self.i], self.info) + self.assertExists( + os.path.join(self.libdir, self.lib.path_formats['default']+'.mp3') + ) + + def test_apply_as_tracks_uses_singleton_path(self): + coro = commands.apply_choices(self.lib, True, False, False, + False, False) + coro.next() # Prime coroutine. + self._call_apply_choice(coro, [self.i], commands.CHOICE_TRACKS) + self.assertExists( + os.path.join(self.libdir, self.lib.path_formats['singleton']+'.mp3') + ) class DuplicateCheckTest(unittest.TestCase): def setUp(self):