Skip to content
Merged
10 changes: 9 additions & 1 deletion beetsplug/mbpseudo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import itertools
from copy import deepcopy
from functools import cached_property
from typing import TYPE_CHECKING, Any

import mediafile
Expand Down Expand Up @@ -266,7 +267,7 @@ def _adjust_final_album_match(self, match: AlbumMatch):
)
album_info.use_pseudo_as_ref()
new_pairs, *_ = assign_items(match.items, album_info.tracks)
album_info.mapping = dict(new_pairs)
match.mapping = dict(new_pairs)

if album_info.data_source == self.data_source:
album_info.data_source = "MusicBrainz"
Expand Down Expand Up @@ -304,6 +305,13 @@ def __init__(
if k not in kwargs:
self[k] = v

@cached_property
def raw_data(self):
# Info.raw_data does self.__class__(**self.copy()) which fails for
# PseudoAlbumInfo since __init__ requires pseudo_release and
# official_release. Construct a plain AlbumInfo instead.
return AlbumInfo(**self.copy()).raw_data

def get_official_release(self) -> AlbumInfo:
return self.__dict__["_official_release"]

Expand Down
9 changes: 9 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ Bug fixes
plural field names. :bug:`6483`
- :doc:`plugins/fetchart`: Error when a configured source does not exist or
sources configuration is empty. :bug:`6336`
- :doc:`plugins/mbpseudo`: Fix two crashes when applying a pseudo-release match
during import. ``PseudoAlbumInfo.raw_data`` now constructs a plain
``AlbumInfo`` instead of calling ``self.__class__(**self.copy())``, which
failed with ``TypeError`` because ``PseudoAlbumInfo.__init__`` requires
``pseudo_release`` and ``official_release`` arguments not present in the flat
copy. ``_adjust_final_album_match`` now correctly updates ``match.mapping``
instead of writing to ``album_info.mapping``, which stored a dict value inside
the ``AttrDict``-based ``Info`` object and caused a
``sqlite3.ProgrammingError`` when saving flex fields.

For plugin developers
~~~~~~~~~~~~~~~~~~~~~
Expand Down
43 changes: 43 additions & 0 deletions test/plugins/test_mbpseudo.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ def test_determine_best_ref(
info.use_pseudo_as_ref()
assert info.data_source == "test"

def test_raw_data(
self, official_release_info: AlbumInfo, pseudo_release_info: AlbumInfo
):
# raw_data calls self.__class__(**self.copy()), which failed for
# PseudoAlbumInfo because its __init__ requires pseudo_release and
# official_release args that are not present in the flat copy() dict.
info = PseudoAlbumInfo(pseudo_release_info, official_release_info)
data = info.raw_data
assert data["album"] == "In Bloom"


class TestMBPseudoMixin(PluginMixin):
plugin = "mbpseudo"
Expand Down Expand Up @@ -235,6 +245,39 @@ def test_final_adjustment(
assert match.info.album_id == "pseudo"
assert match.info.album == "In Bloom"

def test_final_adjustment_updates_match_mapping(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
official_release_info: AlbumInfo,
pseudo_release_info: AlbumInfo,
):
# Regression test: _adjust_final_album_match must update match.mapping,
# not album_info.mapping. Writing to album_info (an AttrDict/dict subclass)
# stored a {Item: TrackInfo} dict under "mapping", which then leaked into
# item_data and caused sqlite3.ProgrammingError on flex field storage.
pseudo_album_info = PseudoAlbumInfo(
pseudo_release=pseudo_release_info,
official_release=official_release_info,
data_source=mbpseudo_plugin.data_source,
)
item = Item()
item["title"] = "百花繚乱"
original_track = pseudo_album_info.tracks[0]

match = AlbumMatch(
distance=Distance(),
info=pseudo_album_info,
mapping={item: original_track},
extra_items=[],
extra_tracks=[],
)

mbpseudo_plugin._adjust_final_album_match(match)

# match.mapping must be reassigned; album_info must not store a dict value
assert "mapping" not in pseudo_album_info
assert not any(isinstance(v, dict) for v in pseudo_album_info.values())


class TestMBPseudoPluginCustomTagsOnly(TestMBPseudoMixin):
@pytest.fixture(scope="class")
Expand Down
Loading