mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
Update mbpseudo implementation for beets 2.5
This commit is contained in:
parent
a42cabb477
commit
229651dcad
3 changed files with 56 additions and 48 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,13 +80,14 @@ 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.get_official_release()
|
||||||
|
else:
|
||||||
yield album_info
|
yield album_info
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -95,16 +99,26 @@ 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])
|
||||||
|
try:
|
||||||
raw_pseudo_release = musicbrainzngs.get_release_by_id(
|
raw_pseudo_release = musicbrainzngs.get_release_by_id(
|
||||||
album_id, RELEASE_INCLUDES
|
album_id, RELEASE_INCLUDES
|
||||||
)
|
)
|
||||||
pseudo_release = super().album_info(raw_pseudo_release["release"])
|
pseudo_release = super().album_info(
|
||||||
|
raw_pseudo_release["release"]
|
||||||
|
)
|
||||||
return PseudoAlbumInfo(
|
return PseudoAlbumInfo(
|
||||||
pseudo_release=_merge_pseudo_and_actual_album(
|
pseudo_release=_merge_pseudo_and_actual_album(
|
||||||
pseudo_release, official_release
|
pseudo_release, official_release
|
||||||
),
|
),
|
||||||
official_release=official_release,
|
official_release=official_release,
|
||||||
data_source=self.data_source,
|
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,34 +167,20 @@ 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(
|
||||||
|
list(mapping.keys()), album_info.tracks
|
||||||
|
)
|
||||||
mapping.update(new_mappings)
|
mapping.update(new_mappings)
|
||||||
|
|
||||||
return super().album_distance(items, album_info, mapping)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def _extract_id(self, url: str) -> str | None:
|
def _extract_id(self, url: str) -> str | None:
|
||||||
return extract_release_id("MusicBrainz", url)
|
return extract_release_id("MusicBrainz", url)
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue