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 @@ -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"
Expand Down Expand Up @@ -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"]

Expand Down
8 changes: 5 additions & 3 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
Loading