diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index beaf4341c..feeefbf28 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -29,7 +29,7 @@ from .hooks import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch from .match import Proposal, Recommendation, tag_album, tag_item if TYPE_CHECKING: - from collections.abc import Mapping, Sequence + from collections.abc import Sequence from beets.library import Album, Item, LibModel @@ -204,11 +204,11 @@ def apply_album_metadata(album_info: AlbumInfo, album: Album): correct_list_fields(album) -def apply_metadata(album_info: AlbumInfo, mapping: Mapping[Item, TrackInfo]): - """Set the items' metadata to match an AlbumInfo object using a - mapping from Items to TrackInfo objects. - """ - for item, track_info in mapping.items(): +def apply_metadata( + album_info: AlbumInfo, item_info_pairs: list[tuple[Item, TrackInfo]] +): + """Set items metadata to match corresponding tagged info.""" + for item, track_info in item_info_pairs: # Artist or artist credit. if config["artist_credit"]: item.artist = ( diff --git a/beets/autotag/distance.py b/beets/autotag/distance.py index 37c6f84f4..5e3f630e3 100644 --- a/beets/autotag/distance.py +++ b/beets/autotag/distance.py @@ -422,7 +422,7 @@ def track_distance( def distance( items: Sequence[Item], album_info: AlbumInfo, - mapping: dict[Item, TrackInfo], + item_info_pairs: list[tuple[Item, TrackInfo]], ) -> Distance: """Determines how "significant" an album metadata change would be. Returns a Distance object. `album_info` is an AlbumInfo object @@ -518,16 +518,16 @@ def distance( # Tracks. dist.tracks = {} - for item, track in mapping.items(): + for item, track in item_info_pairs: dist.tracks[track] = track_distance(item, track, album_info.va) dist.add("tracks", dist.tracks[track].distance) # Missing tracks. - for _ in range(len(album_info.tracks) - len(mapping)): + for _ in range(len(album_info.tracks) - len(item_info_pairs)): dist.add("missing_tracks", 1.0) # Unmatched tracks. - for _ in range(len(items) - len(mapping)): + for _ in range(len(items) - len(item_info_pairs)): dist.add("unmatched_tracks", 1.0) dist.add_data_source(likelies["data_source"], album_info.data_source) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index b809609ea..b5d3e866c 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -223,6 +223,14 @@ class AlbumMatch(NamedTuple): extra_items: list[Item] extra_tracks: list[TrackInfo] + @property + def item_info_pairs(self) -> list[tuple[Item, TrackInfo]]: + return list(self.mapping.items()) + + @property + def items(self) -> list[Item]: + return [i for i, _ in self.item_info_pairs] + class TrackMatch(NamedTuple): distance: Distance diff --git a/beets/autotag/match.py b/beets/autotag/match.py index d0f3fd134..acbcca5ac 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -69,7 +69,7 @@ class Proposal(NamedTuple): def assign_items( items: Sequence[Item], tracks: Sequence[TrackInfo], -) -> tuple[dict[Item, TrackInfo], list[Item], list[TrackInfo]]: +) -> tuple[list[tuple[Item, TrackInfo]], list[Item], list[TrackInfo]]: """Given a list of Items and a list of TrackInfo objects, find the best mapping between them. Returns a mapping from Items to TrackInfo objects, a set of extra Items, and a set of extra TrackInfo @@ -95,7 +95,7 @@ def assign_items( extra_items.sort(key=lambda i: (i.disc, i.track, i.title)) extra_tracks = list(set(tracks) - set(mapping.values())) extra_tracks.sort(key=lambda t: (t.index, t.title)) - return mapping, extra_items, extra_tracks + return list(mapping.items()), extra_items, extra_tracks def match_by_id(items: Iterable[Item]) -> AlbumInfo | None: @@ -217,10 +217,12 @@ def _add_candidate( return # Find mapping between the items and the track info. - mapping, extra_items, extra_tracks = assign_items(items, info.tracks) + item_info_pairs, extra_items, extra_tracks = assign_items( + items, info.tracks + ) # Get the change distance. - dist = distance(items, info, mapping) + dist = distance(items, info, item_info_pairs) # Skip matches with ignored penalties. penalties = [key for key, _ in dist] @@ -232,7 +234,7 @@ def _add_candidate( log.debug("Success. Distance: {}", dist) results[info.album_id] = hooks.AlbumMatch( - dist, info, mapping, extra_items, extra_tracks + dist, info, dict(item_info_pairs), extra_items, extra_tracks ) diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py index 9f60d7619..3a9c044b2 100644 --- a/beets/importer/tasks.py +++ b/beets/importer/tasks.py @@ -245,21 +245,21 @@ class ImportTask(BaseImportTask): matched items. """ if self.choice_flag in (Action.ASIS, Action.RETAG): - return list(self.items) + return self.items elif self.choice_flag == Action.APPLY and isinstance( self.match, autotag.AlbumMatch ): - return list(self.match.mapping.keys()) + return self.match.items else: assert False def apply_metadata(self): """Copy metadata from match info to the items.""" if config["import"]["from_scratch"]: - for item in self.match.mapping: + for item in self.match.items: item.clear() - autotag.apply_metadata(self.match.info, self.match.mapping) + autotag.apply_metadata(self.match.info, self.match.item_info_pairs) def duplicate_items(self, lib: library.Library): duplicate_items = [] diff --git a/beets/ui/commands/import_/display.py b/beets/ui/commands/import_/display.py index a12f1f8d3..fd6758b54 100644 --- a/beets/ui/commands/import_/display.py +++ b/beets/ui/commands/import_/display.py @@ -373,8 +373,9 @@ class AlbumChange(ChangeRepresentation): # Tracks. # match is an AlbumMatch NamedTuple, mapping is a dict # Sort the pairs by the track_info index (at index 1 of the NamedTuple) - pairs = list(self.match.mapping.items()) - pairs.sort(key=lambda item_and_track_info: item_and_track_info[1].index) + pairs = sorted( + self.match.item_info_pairs, key=lambda pair: pair[1].index + ) # Build up LHS and RHS for track difference display. The `lines` list # contains `(left, right)` tuples. lines = [] diff --git a/beetsplug/bpsync.py b/beetsplug/bpsync.py index 9ae6d47d5..fbdf8cc70 100644 --- a/beetsplug/bpsync.py +++ b/beetsplug/bpsync.py @@ -149,14 +149,14 @@ class BPSyncPlugin(BeetsPlugin): library_trackid_to_item = { int(item.mb_trackid): item for item in items } - item_to_trackinfo = { - item: beatport_trackid_to_trackinfo[track_id] + item_info_pairs = [ + (item, beatport_trackid_to_trackinfo[track_id]) for track_id, item in library_trackid_to_item.items() - } + ] self._log.info("applying changes to {}", album) with lib.transaction(): - autotag.apply_metadata(albuminfo, item_to_trackinfo) + autotag.apply_metadata(albuminfo, item_info_pairs) changed = False # Find any changed item to apply Beatport changes to album. any_changed_item = items[0] diff --git a/beetsplug/mbpseudo.py b/beetsplug/mbpseudo.py index 94b6f09a0..b61af2cc7 100644 --- a/beetsplug/mbpseudo.py +++ b/beetsplug/mbpseudo.py @@ -264,11 +264,8 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin): album_info.album_id, ) album_info.use_pseudo_as_ref() - mapping = match.mapping - new_mappings, _, _ = assign_items( - list(mapping.keys()), album_info.tracks - ) - mapping.update(new_mappings) + new_pairs, *_ = assign_items(match.items, album_info.tracks) + album_info.mapping = dict(new_pairs) if album_info.data_source == self.data_source: album_info.data_source = "MusicBrainz" diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 3f7daec6c..5b74b67c9 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -121,18 +121,20 @@ class MBSyncPlugin(BeetsPlugin): # Construct a track mapping according to MBIDs (release track MBIDs # first, if available, and recording MBIDs otherwise). This should # work for albums that have missing or extra tracks. - mapping = {} + item_info_pairs = [] items = list(album.items()) for item in items: if ( item.mb_releasetrackid and item.mb_releasetrackid in releasetrack_index ): - mapping[item] = releasetrack_index[item.mb_releasetrackid] + item_info_pairs.append( + (item, releasetrack_index[item.mb_releasetrackid]) + ) else: candidates = track_index[item.mb_trackid] if len(candidates) == 1: - mapping[item] = candidates[0] + item_info_pairs.append((item, candidates[0])) else: # If there are multiple copies of a recording, they are # disambiguated using their disc and track number. @@ -141,13 +143,13 @@ class MBSyncPlugin(BeetsPlugin): c.medium_index == item.track and c.medium == item.disc ): - mapping[item] = c + item_info_pairs.append((item, c)) break # Apply. self._log.debug("applying changes to {}", album) with lib.transaction(): - autotag.apply_metadata(album_info, mapping) + autotag.apply_metadata(album_info, item_info_pairs) changed = False # Find any changed item to apply changes to album. any_changed_item = items[0] diff --git a/test/autotag/test_distance.py b/test/autotag/test_distance.py index 213d32956..9a658f5e1 100644 --- a/test/autotag/test_distance.py +++ b/test/autotag/test_distance.py @@ -182,7 +182,7 @@ class TestAlbumDistance: @pytest.fixture def get_dist(self, items): def inner(info: AlbumInfo): - return distance(items, info, dict(zip(items, info.tracks))) + return distance(items, info, list(zip(items, info.tracks))) return inner diff --git a/test/test_autotag.py b/test/test_autotag.py index 8d467e5ed..48ae09ccb 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -55,10 +55,12 @@ class TestAssignment(ConfigMixin): items = [Item(title=title) for title in item_titles] tracks = [TrackInfo(title=title) for title in track_titles] - mapping, extra_items, extra_tracks = match.assign_items(items, tracks) + item_info_pairs, extra_items, extra_tracks = match.assign_items( + items, tracks + ) assert ( - {i.title: t.title for i, t in mapping.items()}, + {i.title: t.title for i, t in item_info_pairs}, [i.title for i in extra_items], [t.title for t in extra_tracks], ) == (expected_mapping, expected_extra_items, expected_extra_tracks) @@ -105,7 +107,7 @@ class TestAssignment(ConfigMixin): trackinfo.append(info(11, "Beloved One", 243.733)) trackinfo.append(info(12, "In the Lord's Arms", 186.13300000000001)) - expected = dict(zip(items, trackinfo)), [], [] + expected = list(zip(items, trackinfo)), [], [] assert match.assign_items(items, trackinfo) == expected @@ -113,12 +115,10 @@ class TestAssignment(ConfigMixin): class ApplyTestUtil: def _apply(self, info=None, per_disc_numbering=False, artist_credit=False): info = info or self.info - mapping = {} - for i, t in zip(self.items, info.tracks): - mapping[i] = t + item_info_pairs = list(zip(self.items, info.tracks)) config["per_disc_numbering"] = per_disc_numbering config["artist_credit"] = artist_credit - autotag.apply_metadata(info, mapping) + autotag.apply_metadata(info, item_info_pairs) class ApplyTest(BeetsTestCase, ApplyTestUtil): diff --git a/test/ui/commands/test_import.py b/test/ui/commands/test_import.py index d74d2d816..6e96c3bf3 100644 --- a/test/ui/commands/test_import.py +++ b/test/ui/commands/test_import.py @@ -87,15 +87,17 @@ class ShowChangeTest(IOMixin, unittest.TestCase): """Return an unicode string representing the changes""" items = items or self.items info = info or self.info - mapping = dict(zip(items, info.tracks)) + item_info_pairs = list(zip(items, info.tracks)) config["ui"]["color"] = color config["import"]["detail"] = True - change_dist = distance(items, info, mapping) + change_dist = distance(items, info, item_info_pairs) change_dist._penalties = {"album": [dist], "artist": [dist]} show_change( cur_artist, cur_album, - autotag.AlbumMatch(change_dist, info, mapping, set(), set()), + autotag.AlbumMatch( + change_dist, info, dict(item_info_pairs), set(), set() + ), ) return self.io.getoutput().lower()