mirror of
https://github.com/beetbox/beets.git
synced 2025-12-15 21:14:19 +01:00
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:
parent
66e75c398d
commit
86f513d4ab
2 changed files with 68 additions and 72 deletions
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue