diff --git a/beets/importer.py b/beets/importer.py index 487173fb0..b2fff0399 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -688,14 +688,14 @@ def apply_choices(config): # Find existing item entries that these are replacing (for # re-imports). Old album structures are automatically cleaned up # when the last item is removed. - replaced_items = defaultdict(list) + task.replaced_items = defaultdict(list) for item in items: dup_items = lib.items(library.MatchQuery('path', item.path)) for dup_item in dup_items: - replaced_items[item].append(dup_item) + task.replaced_items[item].append(dup_item) log.debug('replacing item %i: %s' % (dup_item.id, displayable_path(item.path))) - log.debug('%i of %i items replaced' % (len(replaced_items), + log.debug('%i of %i items replaced' % (len(task.replaced_items), len(items))) # Find old items that should be replaced as part of a duplicate @@ -725,7 +725,7 @@ def apply_choices(config): # are in place before calls to destination(). with lib.transaction(): # Remove old items. - for replaced in replaced_items.itervalues(): + for replaced in task.replaced_items.itervalues(): for item in replaced: lib.remove(item) for item in duplicate_items: @@ -741,7 +741,19 @@ def apply_choices(config): for item in items: lib.add(item) +def manipulate_files(config): + """A coroutine (pipeline stage) that performs necessary file + manipulations *after* items have been added to the library. + """ + lib = _reopen_lib(config.lib) + task = None + while True: + task = yield task + if task.should_skip(): + continue + # Move/copy files. + items = task.all_items() task.old_paths = [item.path for item in items] # For deletion. for item in items: if config.move: @@ -756,7 +768,7 @@ def apply_choices(config): # out-of-library files. Otherwise, copy and keep track # of the old path. old_path = item.path - if replaced_items[item]: + if task.replaced_items[item]: # This is a reimport. Move in-library files and copy # out-of-library files. if lib.directory in util.ancestry(old_path): @@ -928,7 +940,7 @@ def run_import(**kwargs): else: # When not autotagging, just display progress. stages += [show_progress(config)] - stages += [apply_choices(config)] + stages += [apply_choices(config), manipulate_files(config)] if config.art: stages += [fetch_art(config)] stages += [finalize(config)] diff --git a/test/test_importer.py b/test/test_importer.py index 955a0fcc5..2bd6039c8 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -168,24 +168,31 @@ class NonAutotaggedImportTest(unittest.TestCase): paths = self._run_import(['sometrack'], singletons=True) self.assertTrue(os.path.exists(paths[0])) -# Utilities for invoking the apply_choices coroutine. -def _call_apply(coros, items, info, toppath=None): - task = importer.ImportTask(None, None, None) +# Utilities for invoking the apply_choices, manipulate_files, and finalize +# coroutines. +def _call_stages(config, items, choice_or_info, + stages=[importer.apply_choices, + importer.manipulate_files, + importer.finalize], + album=True, toppath=None): + # Set up the import task. + task = importer.ImportTask(None, None, items) task.is_album = True task.toppath = toppath - task.set_choice((info, items)) - if not isinstance(coros, list): - coros = [coros] - for coro in coros: - task = coro.send(task) - return task -def _call_apply_choice(coro, items, choice, album=True): - task = importer.ImportTask(None, None, items) - task.is_album = album if not album: task.item = items[0] - task.set_choice(choice) - coro.send(task) + if isinstance(choice_or_info, importer.action): + task.set_choice(choice_or_info) + else: + task.set_choice((choice_or_info, items)) + + # Call the coroutines. + for stage in stages: + coro = stage(config) + coro.next() + coro.send(task) + + return task class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): def setUp(self): @@ -228,51 +235,41 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): def test_finalize_no_delete(self): config = _common.iconfig(self.lib, delete=False) - applyc = importer.apply_choices(config) - applyc.next() - finalize = importer.finalize(config) - finalize.next() - _call_apply([applyc, finalize], [self.i], self.info) + _call_stages(config, [self.i], self.info) self.assertExists(self.srcpath) def test_finalize_with_delete(self): config = _common.iconfig(self.lib, delete=True) - applyc = importer.apply_choices(config) - applyc.next() - finalize = importer.finalize(config) - finalize.next() - _call_apply([applyc, finalize], [self.i], self.info) + _call_stages(config, [self.i], self.info) self.assertNotExists(self.srcpath) def test_finalize_with_delete_prunes_directory_empty(self): config = _common.iconfig(self.lib, delete=True) - applyc = importer.apply_choices(config) - applyc.next() - finalize = importer.finalize(config) - finalize.next() - _call_apply([applyc, finalize], [self.i], self.info, - self.srcdir) + _call_stages(config, [self.i], self.info, + toppath=self.srcdir) self.assertNotExists(os.path.dirname(self.srcpath)) def test_apply_asis_uses_album_path(self): - coro = importer.apply_choices(_common.iconfig(self.lib)) - coro.next() # Prime coroutine. - _call_apply_choice(coro, [self.i], importer.action.ASIS) + config = _common.iconfig(self.lib) + _call_stages(config, [self.i], importer.action.ASIS) self.assertExists(os.path.join(self.libdir, 'one.mp3')) def test_apply_match_uses_album_path(self): - coro = importer.apply_choices(_common.iconfig(self.lib)) - coro.next() # Prime coroutine. - _call_apply(coro, [self.i], self.info) + config = _common.iconfig(self.lib) + _call_stages(config, [self.i], self.info) self.assertExists(os.path.join(self.libdir, 'one.mp3')) def test_apply_tracks_uses_singleton_path(self): - coro = importer.apply_choices(_common.iconfig(self.lib)) - coro.next() # Prime coroutine. + config = _common.iconfig(self.lib) + apply_coro = importer.apply_choices(config) + apply_coro.next() + manip_coro = importer.manipulate_files(config) + manip_coro.next() task = importer.ImportTask.item_task(self.i) task.set_choice(self.info.tracks[0]) - coro.send(task) + apply_coro.send(task) + manip_coro.send(task) self.assertExists( os.path.join(self.libdir, 'three.mp3') @@ -285,9 +282,8 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): # Just test no exception for now. def test_apply_populates_old_paths(self): - coro = importer.apply_choices(_common.iconfig(self.lib)) - coro.next() - task = _call_apply(coro, [self.i], self.info) + config = _common.iconfig(self.lib) + task = _call_stages(config, [self.i], self.info) self.assertEqual(task.old_paths, [self.srcpath]) def test_reimport_inside_file_moves_and_does_not_add_to_old_paths(self): @@ -305,9 +301,8 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): self.i.comp = False # Then, re-import the same file. - coro = importer.apply_choices(_common.iconfig(self.lib)) - coro.next() - task = _call_apply(coro, [self.i], self.info) + config =_common.iconfig(self.lib) + task = _call_stages(config, [self.i], self.info) # Old file should be gone. self.assertNotExists(internal_srcpath) @@ -327,9 +322,8 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): self.lib.conn.commit() # Then, re-import the same file. - coro = importer.apply_choices(_common.iconfig(self.lib)) - coro.next() - task = _call_apply(coro, [self.i], self.info) + config = _common.iconfig(self.lib) + task = _call_stages(config, [self.i], self.info) # Old file should still exist. self.assertExists(self.srcpath) @@ -341,21 +335,13 @@ class ImportApplyTest(unittest.TestCase, _common.ExtraAsserts): def test_apply_with_move(self): config = _common.iconfig(self.lib, move=True) - applyc = importer.apply_choices(config) - applyc.next() - finalize = importer.finalize(config) - finalize.next() - _call_apply([applyc], [self.i], self.info) + _call_stages(config, [self.i], self.info) self.assertExists(list(self.lib.items())[0].path) self.assertNotExists(self.srcpath) def test_apply_with_move_prunes_empty_directory(self): config = _common.iconfig(self.lib, move=True) - applyc = importer.apply_choices(config) - applyc.next() - finalize = importer.finalize(config) - finalize.next() - _call_apply([applyc], [self.i], self.info, self.srcdir) + _call_stages(config, [self.i], self.info, toppath=self.srcdir) self.assertNotExists(os.path.dirname(self.srcpath)) class AsIsApplyTest(unittest.TestCase): @@ -380,11 +366,9 @@ class AsIsApplyTest(unittest.TestCase): os.remove(self.dbpath) def _apply_result(self): - """Run the "apply" coroutine and get the resulting Album.""" - coro = importer.apply_choices(self.config) - coro.next() - _call_apply_choice(coro, self.items, importer.action.ASIS) - + """Run the "apply" coroutines and get the resulting Album.""" + _call_stages(self.config, self.items, importer.action.ASIS, + stages=[importer.apply_choices]) return self.lib.albums()[0] def test_asis_homogenous_va_not_set(self): @@ -428,9 +412,9 @@ class ApplyExistingItemsTest(unittest.TestCase, _common.ExtraAsserts): def _apply_asis(self, items, album=True): """Run the "apply" coroutine.""" - coro = importer.apply_choices(self.config) - coro.next() - _call_apply_choice(coro, items, importer.action.ASIS, album) + _call_stages(self.config, items, importer.action.ASIS, album=album, + stages=[importer.apply_choices, + importer.manipulate_files]) def test_apply_existing_album_does_not_duplicate_item(self): # First, import an item to add it to the library.