Enable duplicate detection for as-is (autotag: no) imports (#6280)

> **Note**: This fix was developed with assistance from Claude Code
(AI). The problem was identified by me, and Claude helped investigate
the codebase, trace the git history to find the original FIXME,
implement the fix, and update the tests. All changes have been reviewed
and tested.

When importing with `autotag: no`, duplicate detection is completely
bypassed. The `import_asis` stage calls `_apply_choice()` directly
without first calling `_resolve_duplicates()`, meaning any configured
`duplicate_keys` and `duplicate_action` settings are ignored.

This was a known limitation. Commit 79d1203541 (Sep 2014) added a FIXME
comment:

  ```python
  # FIXME We should also resolve duplicates when not
  # autotagging. This is currently handled in `user_query`
  ```

The FIXME was removed during a comment cleanup in f145e3b18 (Jan 2015),
but the underlying issue was never fixed. A test
`test_no_autotag_keeps_duplicate_album` was added to document the
existing behavior at the time.

  ### The Fix
Add `_resolve_duplicates(session, task)` to the `import_asis` stage
before `_apply_choice()`, matching the behavior of the `user_query`
stage used when autotagging.

  ### Test Changes
- Renamed `test_no_autotag_keeps_duplicate_album` →
`test_no_autotag_removes_duplicate_album`
- Fixed the test to use album metadata instead of item metadata for
duplicate matching
  - Added missing `import_file.save()` call
This commit is contained in:
Šarūnas Nejus 2026-03-04 14:39:19 +00:00 committed by GitHub
commit 1ecd2c5bdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 47 additions and 12 deletions

View file

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

View file

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

View file

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