Skip to content

Commit 252e5e2

Browse files
committed
Add some documentation and plugins-loaded check
1 parent 2bc240e commit 252e5e2

File tree

1 file changed

+71
-4
lines changed

1 file changed

+71
-4
lines changed

beetsplug/mbpseudo.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
_STATUS_PSEUDO = "Pseudo-Release"
3333

3434

35-
# TODO listen for import start and warn if MB plugin found but not before us
3635
class MusicBrainzPseudoReleasePlugin(MetadataSourcePlugin):
3736
def __init__(self, *args, **kwargs) -> None:
3837
super().__init__(*args, **kwargs)
@@ -42,13 +41,29 @@ def __init__(self, *args, **kwargs) -> None:
4241
self._pseudo_release_ids: dict[str, list[str]] = {}
4342
self._intercepted_candidates: dict[str, AlbumInfo] = {}
4443

44+
self.register_listener("pluginload", self._on_plugins_loaded)
4545
self.register_listener("mb_album_extract", self._intercept_mb_releases)
4646
self.register_listener(
4747
"albuminfo_received", self._intercept_mb_candidates
4848
)
4949

5050
self._log.debug("Desired scripts: {0}", self._scripts)
5151

52+
def _on_plugins_loaded(self):
53+
mb_index = None
54+
self_index = -1
55+
for i, plugin in enumerate(find_plugins()):
56+
if isinstance(plugin, mbplugin.MusicBrainzPlugin):
57+
mb_index = i
58+
elif isinstance(plugin, MusicBrainzPseudoReleasePlugin):
59+
self_index = i
60+
61+
if mb_index and self_index < mb_index:
62+
self._log.warning(
63+
"The mbpseudo plugin was loaded before the musicbrainz plugin"
64+
", this will result in redundant network calls"
65+
)
66+
5267
def _intercept_mb_releases(self, data: JSONDict):
5368
album_id = data["id"] if "id" in data else None
5469
if (
@@ -119,6 +134,12 @@ def candidates(
119134
album: str,
120135
va_likely: bool,
121136
) -> Iterable[AlbumInfo]:
137+
"""Even though a candidate might have extra and/or missing tracks, the set of paths from the items that
138+
were actually matched (which are stored in the corresponding ``mapping``) must be a subset of the set of
139+
paths from the input items. This helps us figure out which intercepted candidate might be relevant for
140+
the items we get in this call even if other candidates have been concurrently intercepted as well.
141+
"""
142+
122143
if len(self._scripts) == 0:
123144
return []
124145

@@ -205,8 +226,8 @@ def _get_pseudo_releases(
205226
pseudo_release_ids: list[str],
206227
) -> list[AlbumInfo]:
207228
pseudo_releases: list[AlbumInfo] = []
208-
for pri in pseudo_release_ids:
209-
if match := self._mb.album_for_id(pri):
229+
for pr_id in pseudo_release_ids:
230+
if match := self._mb.album_for_id(pr_id):
210231
pseudo_album_info = PseudoAlbumInfo(
211232
pseudo_release=match,
212233
official_release=self._intercepted_candidates[
@@ -217,7 +238,7 @@ def _get_pseudo_releases(
217238
self._log.debug(
218239
"Using {0} release for distance calculations for album {1}",
219240
pseudo_album_info.determine_best_ref(items),
220-
pri,
241+
pr_id,
221242
)
222243
pseudo_releases.append(pseudo_album_info)
223244
return pseudo_releases
@@ -227,6 +248,23 @@ def _mb_plugin_simulation_matched(
227248
items: Sequence[Item],
228249
official_candidates: list[AlbumInfo],
229250
) -> bool:
251+
"""Simulate how we would have been called if the MusicBrainz plugin had actually executed.
252+
253+
At this point we already called ``self._mb.candidates()``,
254+
which emits the ``mb_album_extract`` events,
255+
so now we simulate:
256+
257+
1. Intercepting the ``AlbumInfo`` candidate that would have come in the ``albuminfo_received`` event.
258+
2. Intercepting the distance calculation of the aforementioned candidate to store its mapping.
259+
260+
If the official candidate is already a pseudo-release, we clean up internal state.
261+
This is needed because the MusicBrainz plugin emits official releases even if
262+
it receives a pseudo-release as input, so the chain would actually be:
263+
pseudo-release input -> official release with pseudo emitted -> intercepted -> pseudo-release resolved (again)
264+
265+
To avoid resolving again in the last step, we remove the pseudo-release's id.
266+
"""
267+
230268
matched = False
231269
for official_candidate in official_candidates:
232270
if official_candidate.album_id in self._pseudo_release_ids:
@@ -267,6 +305,23 @@ def album_distance(
267305
album_info: AlbumInfo,
268306
mapping: dict[Item, TrackInfo],
269307
) -> Distance:
308+
"""We use this function more like a listener for the extra details we are injecting.
309+
310+
For instances of ``PseudoAlbumInfo`` whose corresponding ``mapping`` is _not_ an
311+
instance of ``ImmutableMapping``, we know at this point that all penalties from the
312+
normal auto-tagging flow have been applied, so we can switch to the metadata from
313+
the pseudo-release for the final proposal.
314+
315+
Other instances of ``AlbumInfo`` must come from other plugins, so we just check if
316+
we intercepted them as candidates with pseudo-releases and store their ``mapping``.
317+
This is needed because the real listeners we use never expose information from the
318+
input ``Item``s, so we intercept that here.
319+
320+
The paths from the items are used to figure out which pseudo-releases should be
321+
provided for them, which is specially important for concurrent stage execution
322+
where we might have intercepted releases from different import tasks when we run.
323+
"""
324+
270325
if isinstance(album_info, PseudoAlbumInfo):
271326
if not isinstance(mapping, ImmutableMapping):
272327
self._log.debug(
@@ -299,6 +354,18 @@ def item_candidates(
299354

300355

301356
class PseudoAlbumInfo(AlbumInfo):
357+
"""This is a not-so-ugly hack.
358+
359+
We want the pseudo-release to result in a distance that is lower or equal to that of the official release,
360+
otherwise it won't qualify as a good candidate. However, if the input is in a script that's different from
361+
the pseudo-release (and we want to translate/transliterate it in the library), it will receive unwanted penalties.
362+
363+
This class is essentially a view of the ``AlbumInfo`` of both official and pseudo-releases,
364+
where it's possible to change the details that are exposed to other parts of the auto-tagger,
365+
enabling a "fair" distance calculation based on the current input's script but still preferring
366+
the translation/transliteration in the final proposal.
367+
"""
368+
302369
def __init__(
303370
self,
304371
pseudo_release: AlbumInfo,

0 commit comments

Comments
 (0)