split apply_choice coroutine

This essential import pipeline stage is now two: one that applies metadata
changes and one that manipulates the filesystem. This will eventually allow
lastgenere to apply its changes before destinations are calculated.
This commit is contained in:
Adrian Sampson 2012-05-22 23:22:34 -07:00
parent 66e75c398d
commit 86f513d4ab
2 changed files with 68 additions and 72 deletions

View file

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

View file

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