diff --git a/beets/importer.py b/beets/importer.py index e7d8b40bc..b9c00c8c7 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -443,6 +443,7 @@ class ImportTask(BaseImportTask): self.candidates = [] self.rec = None self.should_remove_duplicates = False + self.should_merge_duplicates = False self.is_album = True self.search_ids = [] # user-supplied candidate IDs. @@ -1037,6 +1038,10 @@ class ArchiveImportTask(SentinelImportTask): self.extracted = True self.toppath = extract_to +class MergedImportTask(ImportTask): + def __init__(self, toppath, paths, items): + super(MergedImportTask, self).__init__(toppath, paths, items) + class ImportTaskFactory(object): """Generate album and singleton import tasks for all media files @@ -1352,7 +1357,27 @@ def user_query(session, task): ]) return pipeline.multiple(ipl.pull()) - resolve_duplicates(session, task) + if type(task) != MergedImportTask: + resolve_duplicates(session, task) + if task.should_merge_duplicates: + duplicate_items = task.duplicate_items(session.lib) + for item in duplicate_items: + item.id = None + item.album_id = None + + duplicate_paths = [item.path for item in duplicate_items] + + merged_task = MergedImportTask(None, + task.paths + duplicate_paths, + task.items + duplicate_items) + + ipl = pipeline.Pipeline([ + iter([merged_task]), + lookup_candidates(session), + user_query(session) + ]) + return pipeline.multiple(ipl.pull()) + apply_choice(session, task) return task @@ -1373,6 +1398,7 @@ def resolve_duplicates(session, task): u'skip': u's', u'keep': u'k', u'remove': u'r', + u'merge': u'm', u'ask': u'a', }) log.debug(u'default action for duplicates: {0}', duplicate_action) @@ -1386,6 +1412,9 @@ def resolve_duplicates(session, task): elif duplicate_action == u'r': # Remove old. task.should_remove_duplicates = True + elif duplicate_action == u'm': + # Merge duplicates together + task.should_merge_duplicates = True else: # No default action set; ask the session. session.resolve_duplicate(task, found_duplicates) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index a4b433925..c8beb11e2 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -791,7 +791,7 @@ class TerminalImportSession(importer.ImportSession): )) sel = ui.input_options( - (u'Skip new', u'Keep both', u'Remove old') + (u'Skip new', u'Keep both', u'Remove old', u'Merge all') ) if sel == u's': @@ -803,6 +803,8 @@ class TerminalImportSession(importer.ImportSession): elif sel == u'r': # Remove old. task.should_remove_duplicates = True + elif sel == u'm': + task.should_merge_duplicates = True else: assert False diff --git a/test/helper.py b/test/helper.py index ebf039fca..5b3bec8b5 100644 --- a/test/helper.py +++ b/test/helper.py @@ -535,7 +535,7 @@ class TestImportSession(importer.ImportSession): choose_item = choose_match - Resolution = Enum('Resolution', 'REMOVE SKIP KEEPBOTH') + Resolution = Enum('Resolution', 'REMOVE SKIP KEEPBOTH MERGE') default_resolution = 'REMOVE' @@ -553,6 +553,8 @@ class TestImportSession(importer.ImportSession): task.set_choice(importer.action.SKIP) elif res == self.Resolution.REMOVE: task.should_remove_duplicates = True + elif res == self.Resolution.MERGE: + task.should_merge_duplicates = True def generate_album_info(album_id, track_ids): diff --git a/test/test_importer.py b/test/test_importer.py index 1e0dd1fb9..c6b021f33 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1248,6 +1248,12 @@ class ImportDuplicateAlbumTest(unittest.TestCase, TestHelper, item = self.lib.items().get() self.assertEqual(item.title, u't\xeftle 0') + def test_merge_duplicate_album(self): + self.importer.default_resolution = self.importer.Resolution.MERGE + self.importer.run() + + self.assertEqual(len(self.lib.albums()), 1) + def test_twice_in_import_dir(self): self.skipTest('write me')