diff --git a/beets/config_default.yaml b/beets/config_default.yaml index b59ce3297..8599d7091 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -27,7 +27,9 @@ import: group_albums: no pretend: false search_ids: [] - duplicate_keys: albumartist album + duplicate_keys: + album: albumartist album + single: artist title duplicate_action: ask bell: no set_fields: {} diff --git a/beets/importer.py b/beets/importer.py index a671a9d8e..02a9cc43d 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -676,7 +676,7 @@ class ImportTask(BaseImportTask): duplicates = [] task_paths = {i.path for i in self.items if i} - keys = config['import']['duplicate_keys'].as_str_seq() + keys = config['import']['duplicate_keys']['album'].as_str_seq() info['albumartist'] = info['artist'] # Create an Album object so that flexible attributes can be used. tmp_album = library.Album(lib, **info) @@ -893,12 +893,17 @@ class SingletonImportTask(ImportTask): self.is_album = False self.paths = [item.path] - def chosen_ident(self): - assert self.choice_flag in (action.ASIS, action.APPLY, action.RETAG) + def chosen_info(self): + """Return a dictionary of metadata about the current choice. + May only be called when the choice flag is ASIS or RETAG + (in which case the data comes from the files' current metadata) + or APPLY (in which case the data comes from the choice). + """ + assert self.choice_flag in (action.ASIS, action.RETAG, action.APPLY) if self.choice_flag in (action.ASIS, action.RETAG): - return (self.item.artist, self.item.title) + return dict(self.item) elif self.choice_flag is action.APPLY: - return (self.match.info.artist, self.match.info.title) + return self.match.info.copy() def imported_items(self): return [self.item] @@ -919,14 +924,14 @@ class SingletonImportTask(ImportTask): """Return a list of items from `lib` that have the same artist and title as the task. """ - artist, title = self.chosen_ident() + info = self.chosen_info() found_items = [] - query = dbcore.AndQuery(( - dbcore.MatchQuery('artist', artist), - dbcore.MatchQuery('title', title), - )) - for other_item in lib.items(query): + keys = config['import']['duplicate_keys']['single'].as_str_seq() + # Create an Item object so that flexible attributes can be used. + tmp_item = library.Item(lib, **info) + + for other_item in tmp_item.duplicates(*keys): # Existing items not considered duplicates. if other_item.path != self.item.path: found_items.append(other_item) diff --git a/beets/library.py b/beets/library.py index 9f4e5252a..12ea705b6 100644 --- a/beets/library.py +++ b/beets/library.py @@ -607,6 +607,20 @@ class Item(LibModel): i.mtime = i.current_mtime() # Initial mtime. return i + @classmethod + def construct_match_queries(cls, **info): + subqueries = [] + for (key, value) in info.items(): + # Use slow queries for flexible attributes. + fast = key in cls._fields + subqueries.append(dbcore.MatchQuery(key, value, fast)) + return subqueries + + def duplicates(self, *keys): + info = {key: self.get(key) for key in keys} + subqueries = self.construct_match_queries(**info) + return self._db.items(dbcore.AndQuery(subqueries)) + def __setitem__(self, key, value): """Set the item's value for a standard field or a flexattr.""" # Encode unicode paths and read buffers. diff --git a/test/test_importer.py b/test/test_importer.py index 0e8fe7a61..52cf43bd6 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1242,7 +1242,7 @@ class ImportDuplicateAlbumTest(unittest.TestCase, TestHelper, # Create import session self.importer = self.create_importer() config['import']['autotag'] = True - config['import']['duplicate_keys'] = 'albumartist album' + config['import']['duplicate_keys']['album'] = 'albumartist album' def tearDown(self): self.teardown_beets() @@ -1313,7 +1313,7 @@ class ImportDuplicateAlbumTest(unittest.TestCase, TestHelper, self.skipTest('write me') def test_keep_when_extra_key_is_different(self): - config['import']['duplicate_keys'] = 'albumartist album flex' + config['import']['duplicate_keys']['album'] = 'albumartist album flex' item = self.lib.items().get() import_file = MediaFile(os.path.join( @@ -1359,6 +1359,7 @@ class ImportDuplicateSingletonTest(unittest.TestCase, TestHelper, self.importer = self.create_importer() config['import']['autotag'] = True config['import']['singletons'] = True + config['import']['duplicate_keys']['single'] = 'artist title' def tearDown(self): self.teardown_beets() @@ -1395,6 +1396,18 @@ class ImportDuplicateSingletonTest(unittest.TestCase, TestHelper, item = self.lib.items().get() self.assertEqual(item.mb_trackid, 'old trackid') + def test_keep_when_extra_key_is_different(self): + config['import']['duplicate_keys']['single'] = 'artist title flex' + item = self.lib.items().get() + item.flex = 'different' + item.store() + self.assertEqual(len(self.lib.items()), 1) + + self.importer.default_resolution = self.importer.Resolution.SKIP + self.importer.run() + + self.assertEqual(len(self.lib.items()), 2) + def test_twice_in_import_dir(self): self.skipTest('write me')