Update mbpseudo implementation for beets 2.5

This commit is contained in:
asardaes 2025-10-13 15:55:24 -06:00
parent a42cabb477
commit 229651dcad
3 changed files with 56 additions and 48 deletions

View file

@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
import lap import lap
import numpy as np import numpy as np
from beets import config, logging, metadata_plugins from beets import config, logging, metadata_plugins, plugins
from beets.autotag import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch, hooks from beets.autotag import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch, hooks
from beets.util import get_most_common_tags from beets.util import get_most_common_tags
@ -274,12 +274,17 @@ def tag_album(
log.debug("Searching for album ID: {}", search_id) log.debug("Searching for album ID: {}", search_id)
if info := metadata_plugins.album_for_id(search_id): if info := metadata_plugins.album_for_id(search_id):
_add_candidate(items, candidates, info) _add_candidate(items, candidates, info)
if opt_candidate := candidates.get(info.album_id):
plugins.send("album_matched", match=opt_candidate)
# Use existing metadata or text search. # Use existing metadata or text search.
else: else:
# Try search based on current ID. # Try search based on current ID.
if info := match_by_id(items): if info := match_by_id(items):
_add_candidate(items, candidates, info) _add_candidate(items, candidates, info)
for candidate in candidates.values():
plugins.send("album_matched", match=candidate)
rec = _recommendation(list(candidates.values())) rec = _recommendation(list(candidates.values()))
log.debug("Album ID match recommendation is {}", rec) log.debug("Album ID match recommendation is {}", rec)
if candidates and not config["import"]["timid"]: if candidates and not config["import"]["timid"]:
@ -313,6 +318,8 @@ def tag_album(
items, search_artist, search_album, va_likely items, search_artist, search_album, va_likely
): ):
_add_candidate(items, candidates, matched_candidate) _add_candidate(items, candidates, matched_candidate)
if opt_candidate := candidates.get(matched_candidate.album_id):
plugins.send("album_matched", match=opt_candidate)
log.debug("Evaluating {} candidates.", len(candidates)) log.debug("Evaluating {} candidates.", len(candidates))
# Sort and get the recommendation. # Sort and get the recommendation.

View file

@ -72,6 +72,7 @@ EventType = Literal[
"album_imported", "album_imported",
"album_removed", "album_removed",
"albuminfo_received", "albuminfo_received",
"album_matched",
"before_choose_candidate", "before_choose_candidate",
"before_item_moved", "before_item_moved",
"cli_exit", "cli_exit",

View file

@ -16,6 +16,7 @@
from __future__ import annotations from __future__ import annotations
import traceback
from copy import deepcopy from copy import deepcopy
from typing import TYPE_CHECKING, Any, Iterable, Sequence from typing import TYPE_CHECKING, Any, Iterable, Sequence
@ -23,12 +24,13 @@ import musicbrainzngs
from typing_extensions import override from typing_extensions import override
from beets.autotag.distance import Distance, distance from beets.autotag.distance import Distance, distance
from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.autotag.hooks import AlbumInfo
from beets.autotag.match import assign_items from beets.autotag.match import assign_items
from beets.plugins import find_plugins from beets.plugins import find_plugins
from beets.util.id_extractors import extract_release_id from beets.util.id_extractors import extract_release_id
from beetsplug.musicbrainz import ( from beetsplug.musicbrainz import (
RELEASE_INCLUDES, RELEASE_INCLUDES,
MusicBrainzAPIError,
MusicBrainzPlugin, MusicBrainzPlugin,
_merge_pseudo_and_actual_album, _merge_pseudo_and_actual_album,
) )
@ -50,6 +52,7 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
self._log.debug("Desired scripts: {0}", self._scripts) self._log.debug("Desired scripts: {0}", self._scripts)
self.register_listener("pluginload", self._on_plugins_loaded) self.register_listener("pluginload", self._on_plugins_loaded)
self.register_listener("album_matched", self._adjust_final_album_match)
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def _on_plugins_loaded(self): def _on_plugins_loaded(self):
@ -77,14 +80,15 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
items, artist, album, va_likely items, artist, album, va_likely
): ):
if isinstance(album_info, PseudoAlbumInfo): if isinstance(album_info, PseudoAlbumInfo):
yield album_info.get_official_release()
self._log.debug( self._log.debug(
"Using {0} release for distance calculations for album {1}", "Using {0} release for distance calculations for album {1}",
album_info.determine_best_ref(items), album_info.determine_best_ref(items),
album_info.album_id, album_info.album_id,
) )
yield album_info # first yield pseudo to give it priority
yield album_info yield album_info.get_official_release()
else:
yield album_info
@override @override
def album_info(self, release: JSONDict) -> AlbumInfo: def album_info(self, release: JSONDict) -> AlbumInfo:
@ -95,17 +99,27 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
return official_release return official_release
elif pseudo_release_ids := self._intercept_mb_release(release): elif pseudo_release_ids := self._intercept_mb_release(release):
album_id = self._extract_id(pseudo_release_ids[0]) album_id = self._extract_id(pseudo_release_ids[0])
raw_pseudo_release = musicbrainzngs.get_release_by_id( try:
album_id, RELEASE_INCLUDES raw_pseudo_release = musicbrainzngs.get_release_by_id(
) album_id, RELEASE_INCLUDES
pseudo_release = super().album_info(raw_pseudo_release["release"]) )
return PseudoAlbumInfo( pseudo_release = super().album_info(
pseudo_release=_merge_pseudo_and_actual_album( raw_pseudo_release["release"]
pseudo_release, official_release )
), return PseudoAlbumInfo(
official_release=official_release, pseudo_release=_merge_pseudo_and_actual_album(
data_source=self.data_source, pseudo_release, official_release
) ),
official_release=official_release,
data_source="MusicBrainz",
)
except musicbrainzngs.MusicBrainzError as exc:
raise MusicBrainzAPIError(
exc,
"get pseudo-release by ID",
album_id,
traceback.format_exc(),
)
else: else:
return official_release return official_release
@ -153,33 +167,19 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
else: else:
return None return None
@override def _adjust_final_album_match(self, match: AlbumMatch):
def album_distance( album_info = match.info
self,
items: Sequence[Item],
album_info: AlbumInfo,
mapping: dict[Item, TrackInfo],
) -> Distance:
"""We use this function more like a listener for the extra details we are
injecting.
For instances of ``PseudoAlbumInfo`` whose corresponding ``mapping`` is _not_ an
instance of ``ImmutableMapping``, we know at this point that all penalties from
the normal auto-tagging flow have been applied, so we can switch to the metadata
from the pseudo-release for the final proposal.
"""
if isinstance(album_info, PseudoAlbumInfo): if isinstance(album_info, PseudoAlbumInfo):
if not isinstance(mapping, ImmutableMapping): mapping = match.mapping
self._log.debug( self._log.debug(
"Switching {0} to pseudo-release source for final proposal", "Switching {0} to pseudo-release source for final proposal",
album_info.album_id, album_info.album_id,
) )
album_info.use_pseudo_as_ref() album_info.use_pseudo_as_ref()
new_mappings, _, _ = assign_items(items, album_info.tracks) new_mappings, _, _ = assign_items(
mapping.update(new_mappings) list(mapping.keys()), album_info.tracks
)
return super().album_distance(items, album_info, mapping) mapping.update(new_mappings)
@override @override
def _extract_id(self, url: str) -> str | None: def _extract_id(self, url: str) -> str | None:
@ -218,12 +218,17 @@ class PseudoAlbumInfo(AlbumInfo):
return self.__dict__["_official_release"] return self.__dict__["_official_release"]
def determine_best_ref(self, items: Sequence[Item]) -> str: def determine_best_ref(self, items: Sequence[Item]) -> str:
ds = self.data_source
self.data_source = None
self.use_pseudo_as_ref() self.use_pseudo_as_ref()
pseudo_dist = self._compute_distance(items) pseudo_dist = self._compute_distance(items)
self.use_official_as_ref() self.use_official_as_ref()
official_dist = self._compute_distance(items) official_dist = self._compute_distance(items)
self.data_source = ds
if official_dist < pseudo_dist: if official_dist < pseudo_dist:
self.use_official_as_ref() self.use_official_as_ref()
return "official" return "official"
@ -233,7 +238,7 @@ class PseudoAlbumInfo(AlbumInfo):
def _compute_distance(self, items: Sequence[Item]) -> Distance: def _compute_distance(self, items: Sequence[Item]) -> Distance:
mapping, _, _ = assign_items(items, self.tracks) mapping, _, _ = assign_items(items, self.tracks)
return distance(items, self, ImmutableMapping(mapping)) return distance(items, self, mapping)
def use_pseudo_as_ref(self): def use_pseudo_as_ref(self):
self.__dict__["_pseudo_source"] = True self.__dict__["_pseudo_source"] = True
@ -258,8 +263,3 @@ class PseudoAlbumInfo(AlbumInfo):
result[k] = deepcopy(v, memo) result[k] = deepcopy(v, memo)
return result return result
class ImmutableMapping(dict[Item, TrackInfo]):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)