diff --git a/beetsplug/mbpseudo.py b/beetsplug/mbpseudo.py index 9c8b8cdfd3..c96bf87568 100644 --- a/beetsplug/mbpseudo.py +++ b/beetsplug/mbpseudo.py @@ -18,6 +18,7 @@ import itertools from copy import deepcopy +from functools import cached_property from typing import TYPE_CHECKING, Any import mediafile @@ -271,7 +272,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" @@ -309,6 +310,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"] diff --git a/docs/changelog.rst b/docs/changelog.rst index 853080f35e..fc73f27893 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,9 +17,11 @@ New features - :ref:`import-cmd`: The ``--nomove`` / ``-M`` CLI flag can now be used to override the ``move: yes`` config option during import. -.. - Bug fixes - ~~~~~~~~~ +Bug fixes +~~~~~~~~~ + +- :doc:`plugins/mbpseudo`: Fix crashes when applying a pseudo-release. One in + ``PseudoAlbumInfo.raw_data`` and a ``sqlite3.ProgrammingError``. .. For plugin developers diff --git a/test/plugins/test_mbpseudo.py b/test/plugins/test_mbpseudo.py index 7f83f0bc42..0dd7f9bfd5 100644 --- a/test/plugins/test_mbpseudo.py +++ b/test/plugins/test_mbpseudo.py @@ -93,6 +93,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" @@ -236,6 +246,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")