From b5d8ced9d9cbb2358f139331e137a1fb12076b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20Wikstr=C3=B6m?= Date: Sat, 10 Jan 2026 20:03:50 +0200 Subject: [PATCH 1/2] Enable duplicate detection for as-is imports When importing with autotag=no, duplicate detection was skipped entirely because the import_asis stage called _apply_choice() directly without first calling _resolve_duplicates(). This meant the duplicate_keys and duplicate_action config options were ignored for as-is imports. This was a known limitation documented by a FIXME comment added in commit 79d1203541 (Sep 2014): "We should also resolve duplicates when not autotagging." The FIXME was later removed during a comment cleanup (f145e3b18) but the issue was never addressed. This commit adds the _resolve_duplicates() call to import_asis, ensuring duplicate detection works consistently regardless of the autotag setting. This applies to both album imports and singleton imports. Test changes: - Renamed test_no_autotag_keeps_duplicate_album to test_no_autotag_removes_duplicate_album to verify the corrected behavior - Added test_no_autotag_removes_duplicate_singleton to verify singleton duplicate detection also works with autotag=no Co-Authored-By: Claude Opus 4.5 --- beets/importer/stages.py | 1 + docs/changelog.rst | 3 +++ test/test_importer.py | 49 ++++++++++++++++++++++++++++++++-------- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/beets/importer/stages.py b/beets/importer/stages.py index 0f8cf922b..64d305aa8 100644 --- a/beets/importer/stages.py +++ b/beets/importer/stages.py @@ -230,6 +230,7 @@ def import_asis(session: ImportSession, task: ImportTask): log.info("{}", displayable_path(task.paths)) task.set_choice(Action.ASIS) + _resolve_duplicates(session, task) _apply_choice(session, task) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6a0fa5d7d..5c18803bb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -220,6 +220,9 @@ Bug fixes name (like "feat.", "+", or "&") prevent it. Using the albumartists list field and fetching a genre for each artist separately improves the chance of receiving valid results in that stage. +- Duplicate detection now works for as-is imports (when ``autotag`` is + disabled). Previously, ``duplicate_keys`` and ``duplicate_action`` config + options were silently ignored for as-is imports. - :doc:`/plugins/ftintitle`: Fixed artist name splitting to prioritize explicit featuring tokens (feat, ft, featuring) over generic separators (&, and), preventing incorrect splits when both are present. diff --git a/test/test_importer.py b/test/test_importer.py index 56327af06..fe37072fe 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -954,29 +954,34 @@ class ImportDuplicateAlbumTest(PluginMixin, ImportTestCase): item = self.lib.items().get() assert item.title == "new title" - def test_no_autotag_keeps_duplicate_album(self): + def test_no_autotag_removes_duplicate_album(self): config["import"]["autotag"] = False + album = self.lib.albums().get() item = self.lib.items().get() assert item.title == "t\xeftle 0" assert item.filepath.exists() - # Imported item has the same artist and album as the one in the - # library. + # Imported item has the same albumartist and album as the one in the + # library album. We use album metadata (not item metadata) since + # duplicate detection uses album-level fields. import_file = os.path.join( self.importer.paths[0], b"album", b"track_1.mp3" ) import_file = MediaFile(import_file) - import_file.artist = item["artist"] - import_file.albumartist = item["artist"] - import_file.album = item["album"] + import_file.artist = album.albumartist + import_file.albumartist = album.albumartist + import_file.album = album.album import_file.title = "new title" + import_file.save() self.importer.default_resolution = self.importer.Resolution.REMOVE self.importer.run() - assert item.filepath.exists() - assert len(self.lib.albums()) == 2 - assert len(self.lib.items()) == 2 + # Old duplicate should be removed, new one imported + assert len(self.lib.albums()) == 1 + assert len(self.lib.items()) == 1 + # The new item should be in the library + assert self.lib.items().get().title == "new title" def test_keep_duplicate_album(self): self.importer.default_resolution = self.importer.Resolution.KEEPBOTH @@ -1105,6 +1110,32 @@ class ImportDuplicateSingletonTest(ImportTestCase): assert len(self.lib.items()) == 2 + def test_no_autotag_removes_duplicate_singleton(self): + config["import"]["autotag"] = False + item = self.lib.items().get() + assert item.mb_trackid == "old trackid" + assert item.filepath.exists() + + # Imported item has the same artist and title as the one in the + # library. We use item metadata since duplicate detection uses + # item-level fields for singletons. + import_file = os.path.join( + self.importer.paths[0], b"album", b"track_1.mp3" + ) + import_file = MediaFile(import_file) + import_file.artist = item.artist + import_file.title = item.title + import_file.mb_trackid = "new trackid" + import_file.save() + + self.importer.default_resolution = self.importer.Resolution.REMOVE + self.importer.run() + + # Old duplicate should be removed, new one imported + assert len(self.lib.items()) == 1 + # The new item should be in the library + assert self.lib.items().get().mb_trackid == "new trackid" + def test_twice_in_import_dir(self): self.skipTest("write me") From bf7997d45fbdcd7c8618af0db39acda5d53ce079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Wed, 4 Mar 2026 14:32:44 +0000 Subject: [PATCH 2/2] Move changelog note under Unreleased section --- docs/changelog.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5c18803bb..b7a284587 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -44,15 +44,18 @@ Bug fixes - :doc:`plugins/zero`: When the ``omit_single_disc`` option is set, ``disctotal`` is zeroed alongside ``disc``. - :doc:`plugins/fetchart`: Prevent deletion of configured fallback cover art -- In autotagging, initialise empty multi-valued fields with ``None`` instead of - empty list, which caused beets to overwrite existing metadata with empty list - values instead of leaving them unchanged. :bug:`6403` +- :ref:`import-cmd` When autotagging, initialise empty multi-valued fields with + ``None`` instead of empty list, which caused beets to overwrite existing + metadata with empty list values instead of leaving them unchanged. :bug:`6403` - :doc:`plugins/fuzzy`: Improve fuzzy matching when the query is shorter than the field value so substring-style searches produce more useful results. :bug:`2043` - :doc:`plugins/fuzzy`: Force slow query evaluation whenever the fuzzy prefix is used (for example ``~foo`` or ``%%foo``), so fuzzy matching is applied consistently. :bug:`5638` +- :ref:`import-cmd` Duplicate detection now works for as-is imports (when + ``autotag`` is disabled). Previously, ``duplicate_keys`` and + ``duplicate_action`` config options were silently ignored for as-is imports. For plugin developers ~~~~~~~~~~~~~~~~~~~~~ @@ -220,9 +223,6 @@ Bug fixes name (like "feat.", "+", or "&") prevent it. Using the albumartists list field and fetching a genre for each artist separately improves the chance of receiving valid results in that stage. -- Duplicate detection now works for as-is imports (when ``autotag`` is - disabled). Previously, ``duplicate_keys`` and ``duplicate_action`` config - options were silently ignored for as-is imports. - :doc:`/plugins/ftintitle`: Fixed artist name splitting to prioritize explicit featuring tokens (feat, ft, featuring) over generic separators (&, and), preventing incorrect splits when both are present.