From c53e5aab7db9c30ec1e8d5943b4b99fcedf7aed5 Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Sat, 8 Nov 2025 14:03:09 -0500 Subject: [PATCH 01/10] Add Media field for album and test case --- beets/config_default.yaml | 1 - beets/library/models.py | 3 +++ test/test_media_field.py | 11 +++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 test/test_media_field.py diff --git a/beets/config_default.yaml b/beets/config_default.yaml index c0bab8056..b0b495a22 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -7,7 +7,6 @@ statefile: state.pickle # --------------- Plugins --------------- plugins: [musicbrainz] - pluginpath: [] # --------------- Import --------------- diff --git a/beets/library/models.py b/beets/library/models.py index cbee2a411..f3cab6ae9 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -265,6 +265,7 @@ class Album(LibModel): "language": types.STRING, "country": types.STRING, "albumstatus": types.STRING, + "media": types.STRING, "albumdisambig": types.STRING, "releasegroupdisambig": types.STRING, "rg_album_gain": types.NULL_FLOAT, @@ -320,6 +321,7 @@ class Album(LibModel): "language", "country", "albumstatus", + "media", "albumdisambig", "releasegroupdisambig", "release_group_title", @@ -361,6 +363,7 @@ class Album(LibModel): getters = plugins.album_field_getters() getters["path"] = Album.item_dir getters["albumtotal"] = Album._albumtotal + return getters def items(self): diff --git a/test/test_media_field.py b/test/test_media_field.py new file mode 100644 index 000000000..3d405cc31 --- /dev/null +++ b/test/test_media_field.py @@ -0,0 +1,11 @@ +from beets.library import Item +from beets import library + +def test_album_media_field(tmp_path): + lib = library.Library(path=str(tmp_path / "library.db"), + directory=str(tmp_path / "music")) + + item = Item(title="Test Song", album="Test Album", media="Vinyl") + album = lib.add_album([item]) + + assert album.media == "Vinyl" \ No newline at end of file From 9f349b7498271764fd8ce7969bb66324fe8c8072 Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Sat, 8 Nov 2025 14:07:48 -0500 Subject: [PATCH 02/10] Add Media field for album and test case --- beets/library/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beets/library/models.py b/beets/library/models.py index f3cab6ae9..84368f197 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -363,7 +363,6 @@ class Album(LibModel): getters = plugins.album_field_getters() getters["path"] = Album.item_dir getters["albumtotal"] = Album._albumtotal - return getters def items(self): From 96633716595000965a8759df8848c83245d7eec4 Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Sat, 8 Nov 2025 14:16:54 -0500 Subject: [PATCH 03/10] Adding Documentation --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5ebf3f53e..08985fd72 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,7 @@ New features: - :doc:`plugins/mbpseudo`: Add a new `mbpseudo` plugin to proactively receive MusicBrainz pseudo-releases as recommendations during import. - Added support for Python 3.13. +-- Added album-level `$media` field derived from items’ media metadata. Bug fixes: From faa702fe755f3f6ee2ccaa59ef363426ff4560b4 Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Sat, 8 Nov 2025 14:25:02 -0500 Subject: [PATCH 04/10] Fixed Style --- docs/changelog.rst | 2 +- test/test_media_field.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 08985fd72..4345f0b39 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,7 +21,7 @@ New features: - :doc:`plugins/mbpseudo`: Add a new `mbpseudo` plugin to proactively receive MusicBrainz pseudo-releases as recommendations during import. - Added support for Python 3.13. --- Added album-level `$media` field derived from items’ media metadata. +- Added album-level `$media` field derived from items’ media metadata. Bug fixes: diff --git a/test/test_media_field.py b/test/test_media_field.py index 3d405cc31..91dd89d5a 100644 --- a/test/test_media_field.py +++ b/test/test_media_field.py @@ -1,11 +1,13 @@ -from beets.library import Item from beets import library +from beets.library import Item + def test_album_media_field(tmp_path): - lib = library.Library(path=str(tmp_path / "library.db"), - directory=str(tmp_path / "music")) + lib = library.Library( + path=str(tmp_path / "library.db"), directory=str(tmp_path / "music") + ) item = Item(title="Test Song", album="Test Album", media="Vinyl") - album = lib.add_album([item]) + album = lib.add_album([item]) - assert album.media == "Vinyl" \ No newline at end of file + assert album.media == "Vinyl" From 06850d872b11d4cfb08a5a5d77fc569e8544ef9b Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Sat, 22 Nov 2025 16:48:21 -0500 Subject: [PATCH 05/10] Media type as list --- beets/library/models.py | 12 +++++++++ test/test_media_field.py | 53 +++++++++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/beets/library/models.py b/beets/library/models.py index 84368f197..971a2b517 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -356,6 +356,17 @@ class Album(LibModel): """The path to album's cover picture as pathlib.Path.""" return Path(os.fsdecode(self.artpath)) if self.artpath else None + @property + def media(self): + """Return a list of distinct media types for the items in this album.""" + if not self.items(): + return [] + media_set = { + str(item.media) + for item in self.items() + if getattr(item, "media", None)} + return list(media_set) + @classmethod def _getters(cls): # In addition to plugin-provided computed fields, also expose @@ -363,6 +374,7 @@ class Album(LibModel): getters = plugins.album_field_getters() getters["path"] = Album.item_dir getters["albumtotal"] = Album._albumtotal + getters["media_types"] = lambda a: a.media_types return getters def items(self): diff --git a/test/test_media_field.py b/test/test_media_field.py index 91dd89d5a..aac6ae2c6 100644 --- a/test/test_media_field.py +++ b/test/test_media_field.py @@ -1,13 +1,48 @@ -from beets import library -from beets.library import Item +import unittest + +from beets.library import Item, Library -def test_album_media_field(tmp_path): - lib = library.Library( - path=str(tmp_path / "library.db"), directory=str(tmp_path / "music") - ) +class MediaFieldTest(unittest.TestCase): + def setUp(self): + self.lib = Library(':memory:') + self.lib.add_album = self.lib.add_album - item = Item(title="Test Song", album="Test Album", media="Vinyl") - album = lib.add_album([item]) + def add_album_with_items(self, items_data): + items = [] + for data in items_data: + item = Item(**data) + items.append(item) + album = self.lib.add_album(items) + return album - assert album.media == "Vinyl" + def test_album_media_field_multiple_types(self): + items_data = [ + {"title": "Track 1", "artist": "Artist A", "media": "CD"}, + {"title": "Track 2", "artist": "Artist A", "media": "Vinyl"}, + ] + album = self.add_album_with_items(items_data) + media = album.media + assert media == ["CD", "Vinyl"] + + def test_album_media_field_single_type(self): + items_data = [ + {"title": "Track 1", "artist": "Artist A", "media": "CD"}, + {"title": "Track 2", "artist": "Artist A", "media": "CD"}, + ] + album = self.add_album_with_items(items_data) + media = album.media + assert media == ["CD"] + + def test_album_with_no_media(self): + items_data = [ + {"title": "Track 1", "artist": "Artist A"}, + {"title": "Track 2", "artist": "Artist A"}, + ] + album = self.add_album_with_items(items_data) + media = album.media + assert media == [] + + +if __name__ == "__main__": + unittest.main() From 8fc30163c8191ce9edbee1f78c3ecfece68950c7 Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Sat, 22 Nov 2025 17:01:02 -0500 Subject: [PATCH 06/10] Style changes --- beets/library/models.py | 3 ++- test/test_media_field.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/beets/library/models.py b/beets/library/models.py index 971a2b517..69daeade3 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -364,7 +364,8 @@ class Album(LibModel): media_set = { str(item.media) for item in self.items() - if getattr(item, "media", None)} + if getattr(item, "media", None) + } return list(media_set) @classmethod diff --git a/test/test_media_field.py b/test/test_media_field.py index aac6ae2c6..e2410c6ad 100644 --- a/test/test_media_field.py +++ b/test/test_media_field.py @@ -5,7 +5,7 @@ from beets.library import Item, Library class MediaFieldTest(unittest.TestCase): def setUp(self): - self.lib = Library(':memory:') + self.lib = Library(":memory:") self.lib.add_album = self.lib.add_album def add_album_with_items(self, items_data): @@ -23,7 +23,7 @@ class MediaFieldTest(unittest.TestCase): ] album = self.add_album_with_items(items_data) media = album.media - assert media == ["CD", "Vinyl"] + assert sorted(media) == ["CD", "Vinyl"] def test_album_media_field_single_type(self): items_data = [ From a8818071e40b742cf960f6c5cb6195cf1574f3c2 Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Wed, 3 Dec 2025 11:04:22 -0500 Subject: [PATCH 07/10] Remove duplicate feature artist --- beets/library/models.py | 14 ------- beetsplug/discogs.py | 19 +++++++-- test/plugins/test_discogs.py | 24 +++++++++++ test/test_media_field.py | 78 ++++++++++++++++++------------------ 4 files changed, 78 insertions(+), 57 deletions(-) diff --git a/beets/library/models.py b/beets/library/models.py index 69daeade3..95da619e3 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -265,7 +265,6 @@ class Album(LibModel): "language": types.STRING, "country": types.STRING, "albumstatus": types.STRING, - "media": types.STRING, "albumdisambig": types.STRING, "releasegroupdisambig": types.STRING, "rg_album_gain": types.NULL_FLOAT, @@ -321,7 +320,6 @@ class Album(LibModel): "language", "country", "albumstatus", - "media", "albumdisambig", "releasegroupdisambig", "release_group_title", @@ -356,18 +354,6 @@ class Album(LibModel): """The path to album's cover picture as pathlib.Path.""" return Path(os.fsdecode(self.artpath)) if self.artpath else None - @property - def media(self): - """Return a list of distinct media types for the items in this album.""" - if not self.items(): - return [] - media_set = { - str(item.media) - for item in self.items() - if getattr(item, "media", None) - } - return list(media_set) - @classmethod def _getters(cls): # In addition to plugin-provided computed fields, also expose diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 29600a676..feaba4660 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -780,10 +780,21 @@ class DiscogsPlugin(MetadataSourcePlugin): featured_list, self.config["anv"]["artist_credit"] ) if featured: - artist += f" {self.config['featured_string']} {featured}" - artist_credit += ( - f" {self.config['featured_string']} {featured_credit}" - ) + featured_string = self.config["featured_string"].as_str() + token = f"{featured_string} {featured}".lower() + token_credit = f"{featured_string} {featured_credit}".lower() + + # Only append if this featured artist isn't already present + if token not in artist.lower(): + artist += f" {featured_string} {featured}" + + if token_credit not in artist_credit.lower(): + artist_credit += f" {featured_string} {featured_credit}" + # Previous code + # artist += f" {self.config['featured_string']} {featured}" + # artist_credit += ( + # f" {self.config['featured_string']} {featured_credit}" + # ) return IntermediateTrackInfo( title=title, track_id=track_id, diff --git a/test/plugins/test_discogs.py b/test/plugins/test_discogs.py index eb65bc588..2cf5e924d 100644 --- a/test/plugins/test_discogs.py +++ b/test/plugins/test_discogs.py @@ -601,6 +601,30 @@ def test_anv_album_artist(): }, "NEW ARTIST, VOCALIST Feat. SOLOIST, PERFORMER, MUSICIAN", ), + ( + { + "type_": "track", + "title": "Infinite Regression", + "position": "6", + "duration": "5:00", + "artists": [ + { + "name": "Filteria Feat. Ukiro", + "tracks": "", + "id": 11146, + "join": "", + } + ], + "extraartists": [ + { + "name": "Ukiro", + "id": 3, + "role": "Featuring", + }, + ], + }, + "Filteria Feat. Ukiro", + ), ], ) @patch("beetsplug.discogs.DiscogsPlugin.setup", Mock()) diff --git a/test/test_media_field.py b/test/test_media_field.py index e2410c6ad..8c2f1d846 100644 --- a/test/test_media_field.py +++ b/test/test_media_field.py @@ -1,48 +1,48 @@ -import unittest +# import unittest -from beets.library import Item, Library +# from beets.library import Item, Library -class MediaFieldTest(unittest.TestCase): - def setUp(self): - self.lib = Library(":memory:") - self.lib.add_album = self.lib.add_album +# class MediaFieldTest(unittest.TestCase): +# def setUp(self): +# self.lib = Library(":memory:") +# self.lib.add_album = self.lib.add_album - def add_album_with_items(self, items_data): - items = [] - for data in items_data: - item = Item(**data) - items.append(item) - album = self.lib.add_album(items) - return album +# def add_album_with_items(self, items_data): +# items = [] +# for data in items_data: +# item = Item(**data) +# items.append(item) +# album = self.lib.add_album(items) +# return album - def test_album_media_field_multiple_types(self): - items_data = [ - {"title": "Track 1", "artist": "Artist A", "media": "CD"}, - {"title": "Track 2", "artist": "Artist A", "media": "Vinyl"}, - ] - album = self.add_album_with_items(items_data) - media = album.media - assert sorted(media) == ["CD", "Vinyl"] +# def test_album_media_field_multiple_types(self): +# items_data = [ +# {"title": "Track 1", "artist": "Artist A", "media": "CD"}, +# {"title": "Track 2", "artist": "Artist A", "media": "Vinyl"}, +# ] +# album = self.add_album_with_items(items_data) +# media = album.media +# assert sorted(media) == ["CD", "Vinyl"] - def test_album_media_field_single_type(self): - items_data = [ - {"title": "Track 1", "artist": "Artist A", "media": "CD"}, - {"title": "Track 2", "artist": "Artist A", "media": "CD"}, - ] - album = self.add_album_with_items(items_data) - media = album.media - assert media == ["CD"] +# def test_album_media_field_single_type(self): +# items_data = [ +# {"title": "Track 1", "artist": "Artist A", "media": "CD"}, +# {"title": "Track 2", "artist": "Artist A", "media": "CD"}, +# ] +# album = self.add_album_with_items(items_data) +# media = album.media +# assert media == ["CD"] - def test_album_with_no_media(self): - items_data = [ - {"title": "Track 1", "artist": "Artist A"}, - {"title": "Track 2", "artist": "Artist A"}, - ] - album = self.add_album_with_items(items_data) - media = album.media - assert media == [] +# def test_album_with_no_media(self): +# items_data = [ +# {"title": "Track 1", "artist": "Artist A"}, +# {"title": "Track 2", "artist": "Artist A"}, +# ] +# album = self.add_album_with_items(items_data) +# media = album.media +# assert media == [] -if __name__ == "__main__": - unittest.main() +# if __name__ == "__main__": +# unittest.main() From 7362f22079d55f59d97881915712c32e1ce43513 Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Wed, 3 Dec 2025 11:41:17 -0500 Subject: [PATCH 08/10] Getting rid of Media file stuff --- beets/config_default.yaml | 1 + beets/library/models.py | 1 - test/plugins/test_plugin_mediafield.py | 127 ------------------------- test/test_media_field.py | 48 ---------- 4 files changed, 1 insertion(+), 176 deletions(-) delete mode 100644 test/plugins/test_plugin_mediafield.py delete mode 100644 test/test_media_field.py diff --git a/beets/config_default.yaml b/beets/config_default.yaml index b0b495a22..c0bab8056 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -7,6 +7,7 @@ statefile: state.pickle # --------------- Plugins --------------- plugins: [musicbrainz] + pluginpath: [] # --------------- Import --------------- diff --git a/beets/library/models.py b/beets/library/models.py index 95da619e3..cbee2a411 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -361,7 +361,6 @@ class Album(LibModel): getters = plugins.album_field_getters() getters["path"] = Album.item_dir getters["albumtotal"] = Album._albumtotal - getters["media_types"] = lambda a: a.media_types return getters def items(self): diff --git a/test/plugins/test_plugin_mediafield.py b/test/plugins/test_plugin_mediafield.py deleted file mode 100644 index 84565b47b..000000000 --- a/test/plugins/test_plugin_mediafield.py +++ /dev/null @@ -1,127 +0,0 @@ -# This file is part of beets. -# Copyright 2016, Adrian Sampson. -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. - -"""Tests the facility that lets plugins add custom field to MediaFile.""" - -import os -import shutil - -import mediafile -import pytest - -from beets.library import Item -from beets.plugins import BeetsPlugin -from beets.test import _common -from beets.test.helper import BeetsTestCase -from beets.util import bytestring_path, syspath - -field_extension = mediafile.MediaField( - mediafile.MP3DescStorageStyle("customtag"), - mediafile.MP4StorageStyle("----:com.apple.iTunes:customtag"), - mediafile.StorageStyle("customtag"), - mediafile.ASFStorageStyle("customtag"), -) - -list_field_extension = mediafile.ListMediaField( - mediafile.MP3ListDescStorageStyle("customlisttag"), - mediafile.MP4ListStorageStyle("----:com.apple.iTunes:customlisttag"), - mediafile.ListStorageStyle("customlisttag"), - mediafile.ASFStorageStyle("customlisttag"), -) - - -class ExtendedFieldTestMixin(BeetsTestCase): - def _mediafile_fixture(self, name, extension="mp3"): - name = bytestring_path(f"{name}.{extension}") - src = os.path.join(_common.RSRC, name) - target = os.path.join(self.temp_dir, name) - shutil.copy(syspath(src), syspath(target)) - return mediafile.MediaFile(target) - - def test_extended_field_write(self): - plugin = BeetsPlugin() - plugin.add_media_field("customtag", field_extension) - - try: - mf = self._mediafile_fixture("empty") - mf.customtag = "F#" - mf.save() - - mf = mediafile.MediaFile(mf.path) - assert mf.customtag == "F#" - - finally: - delattr(mediafile.MediaFile, "customtag") - Item._media_fields.remove("customtag") - - def test_extended_list_field_write(self): - plugin = BeetsPlugin() - plugin.add_media_field("customlisttag", list_field_extension) - - try: - mf = self._mediafile_fixture("empty") - mf.customlisttag = ["a", "b"] - mf.save() - - mf = mediafile.MediaFile(mf.path) - assert mf.customlisttag == ["a", "b"] - - finally: - delattr(mediafile.MediaFile, "customlisttag") - Item._media_fields.remove("customlisttag") - - def test_write_extended_tag_from_item(self): - plugin = BeetsPlugin() - plugin.add_media_field("customtag", field_extension) - - try: - mf = self._mediafile_fixture("empty") - assert mf.customtag is None - - item = Item(path=mf.path, customtag="Gb") - item.write() - mf = mediafile.MediaFile(mf.path) - assert mf.customtag == "Gb" - - finally: - delattr(mediafile.MediaFile, "customtag") - Item._media_fields.remove("customtag") - - def test_read_flexible_attribute_from_file(self): - plugin = BeetsPlugin() - plugin.add_media_field("customtag", field_extension) - - try: - mf = self._mediafile_fixture("empty") - mf.update({"customtag": "F#"}) - mf.save() - - item = Item.from_path(mf.path) - assert item["customtag"] == "F#" - - finally: - delattr(mediafile.MediaFile, "customtag") - Item._media_fields.remove("customtag") - - def test_invalid_descriptor(self): - with pytest.raises( - ValueError, match="must be an instance of MediaField" - ): - mediafile.MediaFile.add_field("somekey", True) - - def test_overwrite_property(self): - with pytest.raises( - ValueError, match='property "artist" already exists' - ): - mediafile.MediaFile.add_field("artist", mediafile.MediaField()) diff --git a/test/test_media_field.py b/test/test_media_field.py deleted file mode 100644 index 8c2f1d846..000000000 --- a/test/test_media_field.py +++ /dev/null @@ -1,48 +0,0 @@ -# import unittest - -# from beets.library import Item, Library - - -# class MediaFieldTest(unittest.TestCase): -# def setUp(self): -# self.lib = Library(":memory:") -# self.lib.add_album = self.lib.add_album - -# def add_album_with_items(self, items_data): -# items = [] -# for data in items_data: -# item = Item(**data) -# items.append(item) -# album = self.lib.add_album(items) -# return album - -# def test_album_media_field_multiple_types(self): -# items_data = [ -# {"title": "Track 1", "artist": "Artist A", "media": "CD"}, -# {"title": "Track 2", "artist": "Artist A", "media": "Vinyl"}, -# ] -# album = self.add_album_with_items(items_data) -# media = album.media -# assert sorted(media) == ["CD", "Vinyl"] - -# def test_album_media_field_single_type(self): -# items_data = [ -# {"title": "Track 1", "artist": "Artist A", "media": "CD"}, -# {"title": "Track 2", "artist": "Artist A", "media": "CD"}, -# ] -# album = self.add_album_with_items(items_data) -# media = album.media -# assert media == ["CD"] - -# def test_album_with_no_media(self): -# items_data = [ -# {"title": "Track 1", "artist": "Artist A"}, -# {"title": "Track 2", "artist": "Artist A"}, -# ] -# album = self.add_album_with_items(items_data) -# media = album.media -# assert media == [] - - -# if __name__ == "__main__": -# unittest.main() From 43888e6a1e7c333dccd1e1c6c5576e9abd99f0e4 Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Wed, 3 Dec 2025 21:26:32 -0500 Subject: [PATCH 09/10] Changing logic of removing duplicates --- beetsplug/discogs.py | 49 ++++++++++++++++++++++++++++++------ test/plugins/test_discogs.py | 23 +++++++++++++++++ 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index feaba4660..49574c123 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -732,6 +732,31 @@ class DiscogsPlugin(MetadataSourcePlugin): return text return DISAMBIGUATION_RE.sub("", text) + def _normalize_featured_name(self, name: str) -> str: + """Normalize a featured artist name for comparison.""" + # Reuse disambiguation stripping so "Artist (5)" and "Artist" match. + return self.strip_disambiguation(name).strip().lower() + + def _existing_featured_artists(self, artist: str) -> set[str]: + """Extract already-present featured artist names from an artist string. + + For example: + "Filteria Feat. Ukiro, Someone Else" + -> {"ukiro", "someone else"} + """ + feat_str = self.config["featured_string"].as_str() + if feat_str not in artist: + return set() + + # Split once: "Filteria Feat. Ukiro, Someone" -> ["Filteria ", " Ukiro, Someone"] + _, after_feat = artist.split(feat_str, 1) + raw_names = [n.strip() for n in after_feat.split(",")] + return { + self._normalize_featured_name(n) + for n in raw_names + if n + } + def get_track_info( self, track: Track, @@ -780,16 +805,24 @@ class DiscogsPlugin(MetadataSourcePlugin): featured_list, self.config["anv"]["artist_credit"] ) if featured: - featured_string = self.config["featured_string"].as_str() - token = f"{featured_string} {featured}".lower() - token_credit = f"{featured_string} {featured_credit}".lower() + feat_str = self.config["featured_string"].as_str() - # Only append if this featured artist isn't already present - if token not in artist.lower(): - artist += f" {featured_string} {featured}" + # What featured artists are *already* present in the string? + existing = self._existing_featured_artists(artist) - if token_credit not in artist_credit.lower(): - artist_credit += f" {featured_string} {featured_credit}" + # What are we trying to add now? + new = { + self._normalize_featured_name(n) + for n in featured.split(",") + if n.strip() + } + + # Only append if we'd actually introduce *new* featured names. + # This avoids "Filteria Feat. Ukiro Feat. Ukiro" and also + # fixes the ABCD/D example (ABCD feat. D + D again). + if not new.issubset(existing): + artist += f" {feat_str} {featured}" + artist_credit += f" {feat_str} {featured_credit}" # Previous code # artist += f" {self.config['featured_string']} {featured}" # artist_credit += ( diff --git a/test/plugins/test_discogs.py b/test/plugins/test_discogs.py index 2cf5e924d..92fc5b9fb 100644 --- a/test/plugins/test_discogs.py +++ b/test/plugins/test_discogs.py @@ -625,6 +625,29 @@ def test_anv_album_artist(): }, "Filteria Feat. Ukiro", ), + ( + { + "type_": "track", + "title": "track", + "position": "1", + "duration": "5:00", + "artists": [ + { + "name": "ABCD", + "tracks": "", + "id": 11146, + } + ], + "extraartists": [ + { + "name": "D", + "id": 3, + "role": "Featuring", + } + ], + }, + "ABCD Feat. D", + ), ], ) @patch("beetsplug.discogs.DiscogsPlugin.setup", Mock()) From 9614b541266c8664e9ab8a9d7158f4c64aad2274 Mon Sep 17 00:00:00 2001 From: Grace Coppola Date: Wed, 3 Dec 2025 21:30:50 -0500 Subject: [PATCH 10/10] Fixed style --- beetsplug/discogs.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 49574c123..d2f32fed3 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -748,14 +748,11 @@ class DiscogsPlugin(MetadataSourcePlugin): if feat_str not in artist: return set() - # Split once: "Filteria Feat. Ukiro, Someone" -> ["Filteria ", " Ukiro, Someone"] + # Split once: "Filteria Feat. Ukiro, Someone" -> + # ["Filteria ", " Ukiro, Someone"] _, after_feat = artist.split(feat_str, 1) raw_names = [n.strip() for n in after_feat.split(",")] - return { - self._normalize_featured_name(n) - for n in raw_names - if n - } + return {self._normalize_featured_name(n) for n in raw_names if n} def get_track_info( self,