hooks: make AlbumMatch.mapping a tuple

This commit is contained in:
Šarūnas Nejus 2025-10-26 01:57:15 +00:00
parent c1904b1f69
commit c5cd219918
No known key found for this signature in database
12 changed files with 58 additions and 46 deletions

View file

@ -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 = (

View file

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

View file

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

View file

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

View file

@ -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 = []

View file

@ -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 = []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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