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..82e685b7a 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -17,10 +17,14 @@ from __future__ import annotations from copy import deepcopy -from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar +from dataclasses import dataclass +from functools import cached_property +from typing import TYPE_CHECKING, Any, TypeVar from typing_extensions import Self +from beets.util import cached_classproperty + if TYPE_CHECKING: from beets.library import Item @@ -54,6 +58,10 @@ class AttrDict(dict[str, V]): class Info(AttrDict[Any]): """Container for metadata about a musical entity.""" + @cached_property + def name(self) -> str: + raise NotImplementedError + def __init__( self, album: str | None = None, @@ -95,6 +103,10 @@ class AlbumInfo(Info): user items, and later to drive tagging decisions once selected. """ + @cached_property + def name(self) -> str: + return self.album or "" + def __init__( self, tracks: list[TrackInfo], @@ -167,6 +179,10 @@ class TrackInfo(Info): stand alone for singleton matching. """ + @cached_property + def name(self) -> str: + return self.title or "" + def __init__( self, *, @@ -214,16 +230,32 @@ class TrackInfo(Info): # Structures that compose all the information for a candidate match. - - -class AlbumMatch(NamedTuple): +@dataclass +class Match: distance: Distance + info: Info + + @cached_classproperty + def type(cls) -> str: + return cls.__name__.removesuffix("Match") # type: ignore[attr-defined] + + +@dataclass +class AlbumMatch(Match): info: AlbumInfo mapping: dict[Item, TrackInfo] extra_items: list[Item] extra_tracks: list[TrackInfo] + @property + def item_info_pairs(self) -> list[tuple[Item, TrackInfo]]: + return list(self.mapping.items()) -class TrackMatch(NamedTuple): - distance: Distance + @property + def items(self) -> list[Item]: + return [i for i, _ in self.item_info_pairs] + + +@dataclass +class TrackMatch(Match): info: TrackInfo diff --git a/beets/autotag/match.py b/beets/autotag/match.py index d0f3fd134..b88acf3df 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 @@ -86,16 +86,17 @@ def assign_items( # Each item in `assigned_item_idxs` list corresponds to a track in the # `tracks` list. Each value is either an index into the assigned item in # `items` list, or -1 if that track has no match. - mapping = { - items[iidx]: t + item_info_pairs = [ + (items[iidx], t) for iidx, t in zip(assigned_item_idxs, tracks) if iidx != -1 - } - extra_items = list(set(items) - mapping.keys()) + ] + item_info_pairs.sort(key=lambda it: (it[0].disc, it[0].track, it[0].title)) + extra_items = list(set(items) - {i for i, _ in item_info_pairs}) extra_items.sort(key=lambda i: (i.disc, i.track, i.title)) - extra_tracks = list(set(tracks) - set(mapping.values())) + extra_tracks = list(set(tracks) - {t for _, t in item_info_pairs}) extra_tracks.sort(key=lambda t: (t.index, t.title)) - return mapping, extra_items, extra_tracks + return item_info_pairs, extra_items, extra_tracks def match_by_id(items: Iterable[Item]) -> AlbumInfo | None: @@ -217,10 +218,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,14 +235,14 @@ 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 ) def tag_album( items, search_artist: str | None = None, - search_album: str | None = None, + search_name: str | None = None, search_ids: list[str] = [], ) -> tuple[str, str, Proposal]: """Return a tuple of the current artist name, the current album @@ -300,10 +303,10 @@ def tag_album( ) # Search terms. - if not (search_artist and search_album): + if not (search_artist and search_name): # No explicit search terms -- use current metadata. - search_artist, search_album = cur_artist, cur_album - log.debug("Search terms: {} - {}", search_artist, search_album) + search_artist, search_name = cur_artist, cur_album + log.debug("Search terms: {} - {}", search_artist, search_name) # Is this album likely to be a "various artist" release? va_likely = ( @@ -315,7 +318,7 @@ def tag_album( # Get the results from the data sources. for matched_candidate in metadata_plugins.candidates( - items, search_artist, search_album, va_likely + items, search_artist, search_name, va_likely ): _add_candidate(items, candidates, matched_candidate) if opt_candidate := candidates.get(matched_candidate.album_id): @@ -331,7 +334,7 @@ def tag_album( def tag_item( item, search_artist: str | None = None, - search_title: str | None = None, + search_name: str | None = None, search_ids: list[str] | None = None, ) -> Proposal: """Find metadata for a single track. Return a `Proposal` consisting @@ -373,12 +376,12 @@ def tag_item( # Search terms. search_artist = search_artist or item.artist - search_title = search_title or item.title - log.debug("Item search terms: {} - {}", search_artist, search_title) + search_name = search_name or item.title + log.debug("Item search terms: {} - {}", search_artist, search_name) # Get and evaluate candidate metadata. for track_info in metadata_plugins.item_candidates( - item, search_artist, search_title + item, search_artist, search_name ): dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info) 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..467e0c191 100644 --- a/beets/ui/commands/import_/display.py +++ b/beets/ui/commands/import_/display.py @@ -1,15 +1,36 @@ +from __future__ import annotations + import os -from collections.abc import Sequence +from dataclasses import dataclass from functools import cached_property +from typing import TYPE_CHECKING, TypedDict + +from typing_extensions import NotRequired from beets import autotag, config, ui from beets.autotag import hooks from beets.util import displayable_path from beets.util.units import human_seconds_short +if TYPE_CHECKING: + from collections.abc import Sequence + + import confuse + + from beets.library.models import Item + from beets.ui import ColorName + VARIOUS_ARTISTS = "Various Artists" +class Line(TypedDict): + prefix: str + contents: str + suffix: str + width: NotRequired[int] + + +@dataclass class ChangeRepresentation: """Keeps track of all information needed to generate a (colored) text representation of the changes that will be made if an album or singleton's @@ -17,46 +38,46 @@ class ChangeRepresentation: TrackMatch object, accordingly. """ + cur_artist: str + cur_name: str + match: autotag.hooks.Match + @cached_property def changed_prefix(self) -> str: return ui.colorize("changed", "\u2260") - cur_artist = None - # cur_album set if album, cur_title set if singleton - cur_album = None - cur_title = None - match = None - indent_header = "" - indent_detail = "" + @cached_property + def _indentation_config(self) -> confuse.ConfigView: + return config["ui"]["import"]["indentation"] - def __init__(self): - # Read match header indentation width from config. - match_header_indent_width = config["ui"]["import"]["indentation"][ - "match_header" - ].as_number() - self.indent_header = ui.indent(match_header_indent_width) + @cached_property + def indent_header(self) -> str: + return ui.indent(self._indentation_config["match_header"].as_number()) - # Read match detail indentation width from config. - match_detail_indent_width = config["ui"]["import"]["indentation"][ - "match_details" - ].as_number() - self.indent_detail = ui.indent(match_detail_indent_width) + @cached_property + def indent_detail(self) -> str: + return ui.indent(self._indentation_config["match_details"].as_number()) - # Read match tracklist indentation width from config - match_tracklist_indent_width = config["ui"]["import"]["indentation"][ - "match_tracklist" - ].as_number() - self.indent_tracklist = ui.indent(match_tracklist_indent_width) - self.layout = config["ui"]["import"]["layout"].as_choice( - { - "column": 0, - "newline": 1, - } + @cached_property + def indent_tracklist(self) -> str: + return ui.indent( + self._indentation_config["match_tracklist"].as_number() + ) + + @cached_property + def layout(self) -> int: + return config["ui"]["import"]["layout"].as_choice( + {"column": 0, "newline": 1} ) def print_layout( - self, indent, left, right, separator=" -> ", max_width=None - ): + self, + indent: str, + left: Line, + right: Line, + separator: str = " -> ", + max_width: int | None = None, + ) -> None: if not max_width: # If no max_width provided, use terminal width max_width = ui.term_width() @@ -65,7 +86,7 @@ class ChangeRepresentation: else: ui.print_newline_layout(indent, left, right, separator, max_width) - def show_match_header(self): + def show_match_header(self) -> None: """Print out a 'header' identifying the suggested match (album name, artist name,...) and summarizing the changes that would be made should the user accept the match. @@ -78,19 +99,10 @@ class ChangeRepresentation: f"{self.indent_header}Match ({dist_string(self.match.distance)}):" ) - if isinstance(self.match.info, autotag.hooks.AlbumInfo): - # Matching an album - print that - artist_album_str = ( - f"{self.match.info.artist} - {self.match.info.album}" - ) - else: - # Matching a single track - artist_album_str = ( - f"{self.match.info.artist} - {self.match.info.title}" - ) + artist_name_str = f"{self.match.info.artist} - {self.match.info.name}" ui.print_( self.indent_header - + dist_colorize(artist_album_str, self.match.distance) + + dist_colorize(artist_name_str, self.match.distance) ) # Penalties. @@ -108,7 +120,7 @@ class ChangeRepresentation: url = ui.colorize("text_faint", f"{self.match.info.data_url}") ui.print_(f"{self.indent_header}{url}") - def show_match_details(self): + def show_match_details(self) -> None: """Print out the details of the match, including changes in album name and artist name. """ @@ -117,6 +129,8 @@ class ChangeRepresentation: if artist_r == VARIOUS_ARTISTS: # Hide artists for VA releases. artist_l, artist_r = "", "" + left: Line + right: Line if artist_l != artist_r: artist_l, artist_r = ui.colordiff(artist_l, artist_r) left = { @@ -130,39 +144,22 @@ class ChangeRepresentation: else: ui.print_(f"{self.indent_detail}*", "Artist:", artist_r) - if self.cur_album: - # Album - album_l, album_r = self.cur_album or "", self.match.info.album - if ( - self.cur_album != self.match.info.album - and self.match.info.album != VARIOUS_ARTISTS - ): - album_l, album_r = ui.colordiff(album_l, album_r) + if self.cur_name: + type_ = self.match.type + name_l, name_r = self.cur_name or "", self.match.info.name + if self.cur_name != self.match.info.name != VARIOUS_ARTISTS: + name_l, name_r = ui.colordiff(name_l, name_r) left = { - "prefix": f"{self.changed_prefix} Album: ", - "contents": album_l, + "prefix": f"{self.changed_prefix} {type_}: ", + "contents": name_l, "suffix": "", } - right = {"prefix": "", "contents": album_r, "suffix": ""} + right = {"prefix": "", "contents": name_r, "suffix": ""} self.print_layout(self.indent_detail, left, right) else: - ui.print_(f"{self.indent_detail}*", "Album:", album_r) - elif self.cur_title: - # Title - for singletons - title_l, title_r = self.cur_title or "", self.match.info.title - if self.cur_title != self.match.info.title: - title_l, title_r = ui.colordiff(title_l, title_r) - left = { - "prefix": f"{self.changed_prefix} Title: ", - "contents": title_l, - "suffix": "", - } - right = {"prefix": "", "contents": title_r, "suffix": ""} - self.print_layout(self.indent_detail, left, right) - else: - ui.print_(f"{self.indent_detail}*", "Title:", title_r) + ui.print_(f"{self.indent_detail}*", f"{type_}:", name_r) - def make_medium_info_line(self, track_info): + def make_medium_info_line(self, track_info: hooks.TrackInfo) -> str: """Construct a line with the current medium's info.""" track_media = track_info.get("media", "Media") # Build output string. @@ -177,7 +174,7 @@ class ChangeRepresentation: else: return "" - def format_index(self, track_info): + def format_index(self, track_info: hooks.TrackInfo | Item) -> str: """Return a string representing the track index of the given TrackInfo or Item object. """ @@ -198,12 +195,15 @@ class ChangeRepresentation: else: return str(index) - def make_track_numbers(self, item, track_info): + def make_track_numbers( + self, item, track_info: hooks.TrackInfo + ) -> tuple[str, str, bool]: """Format colored track indices.""" cur_track = self.format_index(item) new_track = self.format_index(track_info) changed = False # Choose color based on change. + highlight_color: ColorName if cur_track != new_track: changed = True if item.track in (track_info.index, track_info.medium_index): @@ -218,9 +218,11 @@ class ChangeRepresentation: return lhs_track, rhs_track, changed @staticmethod - def make_track_titles(item, track_info): + def make_track_titles( + item: Item, track_info: hooks.TrackInfo + ) -> tuple[str, str, bool]: """Format colored track titles.""" - new_title = track_info.title + new_title = track_info.name if not item.title.strip(): # If there's no title, we use the filename. Don't colordiff. cur_title = displayable_path(os.path.basename(item.path)) @@ -232,9 +234,12 @@ class ChangeRepresentation: return cur_col, new_col, cur_title != new_title @staticmethod - def make_track_lengths(item, track_info): + def make_track_lengths( + item: Item, track_info: hooks.TrackInfo + ) -> tuple[str, str, bool]: """Format colored track lengths.""" changed = False + highlight_color: ColorName if ( item.length and track_info.length @@ -258,7 +263,9 @@ class ChangeRepresentation: return lhs_length, rhs_length, changed - def make_line(self, item, track_info): + def make_line( + self, item: Item, track_info: hooks.TrackInfo + ) -> tuple[Line, Line]: """Extract changes from item -> new TrackInfo object, and colorize appropriately. Returns (lhs, rhs) for column printing. """ @@ -282,12 +289,12 @@ class ChangeRepresentation: # the case, thus the 'info' dictionary is unneeded. # penalties = penalty_string(self.match.distance.tracks[track_info]) - lhs = { + lhs: Line = { "prefix": f"{self.changed_prefix if changed else '*'} {lhs_track} ", "contents": lhs_title, "suffix": f" {lhs_length}", } - rhs = {"prefix": "", "contents": "", "suffix": ""} + rhs: Line = {"prefix": "", "contents": "", "suffix": ""} if not changed: # Only return the left side, as nothing changed. return (lhs, rhs) @@ -358,26 +365,18 @@ class ChangeRepresentation: class AlbumChange(ChangeRepresentation): - """Album change representation, setting cur_album""" + match: autotag.hooks.AlbumMatch - def __init__(self, cur_artist, cur_album, match): - super().__init__() - self.cur_artist = cur_artist - self.cur_album = cur_album - self.match = match - - def show_match_tracks(self): + def show_match_tracks(self) -> None: """Print out the tracks of the match, summarizing changes the match suggests for them. """ - # 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 or 0 + ) # Build up LHS and RHS for track difference display. The `lines` list # contains `(left, right)` tuples. - lines = [] + lines: list[tuple[Line, Line]] = [] medium = disctitle = None for item, track_info in pairs: # If the track is the first on a new medium, show medium @@ -426,21 +425,17 @@ class AlbumChange(ChangeRepresentation): class TrackChange(ChangeRepresentation): """Track change representation, comparing item with match.""" - def __init__(self, cur_artist, cur_title, match): - super().__init__() - self.cur_artist = cur_artist - self.cur_title = cur_title - self.match = match + match: autotag.hooks.TrackMatch -def show_change(cur_artist, cur_album, match): +def show_change( + cur_artist: str, cur_album: str, match: hooks.AlbumMatch +) -> None: """Print out a representation of the changes that will be made if an album's tags are changed according to `match`, which must be an AlbumMatch object. """ - change = AlbumChange( - cur_artist=cur_artist, cur_album=cur_album, match=match - ) + change = AlbumChange(cur_artist, cur_album, match) # Print the match header. change.show_match_header() @@ -452,13 +447,11 @@ def show_change(cur_artist, cur_album, match): change.show_match_tracks() -def show_item_change(item, match): +def show_item_change(item: Item, match: hooks.TrackMatch) -> None: """Print out the change that would occur by tagging `item` with the metadata from `match`, a TrackMatch object. """ - change = TrackChange( - cur_artist=item.artist, cur_title=item.title, match=match - ) + change = TrackChange(item.artist, item.title, match) # Print the match header. change.show_match_header() # Print the match details. diff --git a/beets/ui/commands/import_/session.py b/beets/ui/commands/import_/session.py index dcc80b793..9c8c8dd62 100644 --- a/beets/ui/commands/import_/session.py +++ b/beets/ui/commands/import_/session.py @@ -444,10 +444,7 @@ def choose_candidate( index = dist_colorize(index0, match.distance) dist = f"({(1 - match.distance) * 100:.1f}%)" distance = dist_colorize(dist, match.distance) - metadata = ( - f"{match.info.artist} -" - f" {match.info.title if singleton else match.info.album}" - ) + metadata = f"{match.info.artist} - {match.info.name}" if i == 0: metadata = dist_colorize(metadata, match.distance) else: 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 9cfa99969..fb054318b 100644 --- a/beetsplug/mbpseudo.py +++ b/beetsplug/mbpseudo.py @@ -278,11 +278,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()