32
32
_STATUS_PSEUDO = "Pseudo-Release"
33
33
34
34
35
- # TODO listen for import start and warn if MB plugin found but not before us
36
35
class MusicBrainzPseudoReleasePlugin (MetadataSourcePlugin ):
37
36
def __init__ (self , * args , ** kwargs ) -> None :
38
37
super ().__init__ (* args , ** kwargs )
@@ -42,13 +41,29 @@ def __init__(self, *args, **kwargs) -> None:
42
41
self ._pseudo_release_ids : dict [str , list [str ]] = {}
43
42
self ._intercepted_candidates : dict [str , AlbumInfo ] = {}
44
43
44
+ self .register_listener ("pluginload" , self ._on_plugins_loaded )
45
45
self .register_listener ("mb_album_extract" , self ._intercept_mb_releases )
46
46
self .register_listener (
47
47
"albuminfo_received" , self ._intercept_mb_candidates
48
48
)
49
49
50
50
self ._log .debug ("Desired scripts: {0}" , self ._scripts )
51
51
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
+
52
67
def _intercept_mb_releases (self , data : JSONDict ):
53
68
album_id = data ["id" ] if "id" in data else None
54
69
if (
@@ -119,6 +134,12 @@ def candidates(
119
134
album : str ,
120
135
va_likely : bool ,
121
136
) -> 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
+
122
143
if len (self ._scripts ) == 0 :
123
144
return []
124
145
@@ -205,8 +226,8 @@ def _get_pseudo_releases(
205
226
pseudo_release_ids : list [str ],
206
227
) -> list [AlbumInfo ]:
207
228
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 ):
210
231
pseudo_album_info = PseudoAlbumInfo (
211
232
pseudo_release = match ,
212
233
official_release = self ._intercepted_candidates [
@@ -217,7 +238,7 @@ def _get_pseudo_releases(
217
238
self ._log .debug (
218
239
"Using {0} release for distance calculations for album {1}" ,
219
240
pseudo_album_info .determine_best_ref (items ),
220
- pri ,
241
+ pr_id ,
221
242
)
222
243
pseudo_releases .append (pseudo_album_info )
223
244
return pseudo_releases
@@ -227,6 +248,23 @@ def _mb_plugin_simulation_matched(
227
248
items : Sequence [Item ],
228
249
official_candidates : list [AlbumInfo ],
229
250
) -> 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
+
230
268
matched = False
231
269
for official_candidate in official_candidates :
232
270
if official_candidate .album_id in self ._pseudo_release_ids :
@@ -267,6 +305,23 @@ def album_distance(
267
305
album_info : AlbumInfo ,
268
306
mapping : dict [Item , TrackInfo ],
269
307
) -> 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
+
270
325
if isinstance (album_info , PseudoAlbumInfo ):
271
326
if not isinstance (mapping , ImmutableMapping ):
272
327
self ._log .debug (
@@ -299,6 +354,18 @@ def item_candidates(
299
354
300
355
301
356
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
+
302
369
def __init__ (
303
370
self ,
304
371
pseudo_release : AlbumInfo ,
0 commit comments