diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 0a999c002c..5752f061b0 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -16,7 +16,7 @@ from collections import defaultdict -from beets import library, metadata_plugins, ui, util +from beets import config, library, metadata_plugins, ui, util from beets.autotag.distance import Distance from beets.autotag.hooks import AlbumMatch, TrackMatch from beets.plugins import BeetsPlugin, apply_item_changes @@ -25,6 +25,11 @@ class MBSyncPlugin(BeetsPlugin): def __init__(self): super().__init__() + self.config.add( + { + "excluded_fields": [], + }, + ) def commands(self): cmd = ui.Subcommand("mbsync", help="update metadata from musicbrainz") @@ -69,6 +74,24 @@ def func(self, lib, opts, args): self.singletons(lib, args, move, pretend, write) self.albums(lib, args, move, pretend, write) + def _get_excluded_fields(self): + """Return a list of fields to be exluded from updates. + + When ``musicbrainz.genres`` is False, also include the + ``genres`` field. + """ + fields = set(self.config["excluded_fields"].as_str_seq()) + if not config["musicbrainz"]["genres"]: + fields.update(["genres"]) + return fields + + def noneify_fields(self, obj): + """ + Reset the given ``fields`` on an object ``obj`` to None. + """ + for field in self._get_excluded_fields(): + setattr(obj, field, None) + def singletons(self, lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for items matched by query. @@ -90,6 +113,9 @@ def singletons(self, lib, query, move, pretend, write): ) continue + # Ignore excluded fields by setting them back to None + self.noneify_fields(track_info) + # Apply. with lib.transaction(): TrackMatch(Distance(), track_info, item).apply_metadata() @@ -118,12 +144,17 @@ def albums(self, lib, query, move, pretend, write): ) continue + # Ignore excluded fields by setting them back to None + self.noneify_fields(album_info) + # Map release track and recording MBIDs to their information. # Recordings can appear multiple times on a release, so each MBID # maps to a list of TrackInfo objects. releasetrack_index = {} track_index = defaultdict(list) for track_info in album_info.tracks: + # Ignore excluded fields by setting them back to None + self.noneify_fields(track_info) releasetrack_index[track_info.release_track_id] = track_info track_index[track_info.track_id].append(track_info) @@ -164,8 +195,13 @@ def albums(self, lib, query, move, pretend, write): changed = False # Find any changed item to apply changes to album. any_changed_item = items[0] + + # Ignore any excluded fields when displaying changes + fields = set(library.Album.item_keys) - set( + self._get_excluded_fields() + ) for item in items: - item_changed = ui.show_model_changes(item) + item_changed = ui.show_model_changes(item, fields=fields) changed |= item_changed if item_changed: any_changed_item = item diff --git a/docs/changelog.rst b/docs/changelog.rst index 1713b2debe..600b65e826 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -38,6 +38,9 @@ New features - :doc:`plugins/replaygain`: Conflicting replay gain tags are now removed on write. RG_* tags are removed when setting R128_* and vice versa. - :doc:`plugins/fetchart`: Add support for WebP images. +- :doc:`plugins/mbsync`: Add support excluding fields from being updated. The + genres tag will no longer be updated/wiped when ``musicbrainz.genres`` is + ``false``. Bug fixes ~~~~~~~~~ diff --git a/docs/plugins/mbsync.rst b/docs/plugins/mbsync.rst index 9d0b793fb0..6d90e29411 100644 --- a/docs/plugins/mbsync.rst +++ b/docs/plugins/mbsync.rst @@ -10,6 +10,31 @@ contains correct tags, you can speed up the initial import by importing files "as-is" and then using ``mbsync`` to write tags according to your beets configuration. +Configuration +------------- + +This plugin can be configured like other metadata source plugins as described in +:ref:`metadata-source-plugin-configuration`. + +.. code-block:: yaml + + mbsync: + excluded_fields: [] + +.. conf:: excluded_fields + :default: [] + + A list of fields to be excluded from updates when mbsync runs. + + Example: + + .. code-block:: yaml + + mbsync: + excluded_fields: + - genres + - composer + Usage ----- diff --git a/test/plugins/test_mbsync.py b/test/plugins/test_mbsync.py index 714b374e32..01bf6543ea 100644 --- a/test/plugins/test_mbsync.py +++ b/test/plugins/test_mbsync.py @@ -22,6 +22,10 @@ class MbsyncCliTest(PluginTestCase): plugin = "mbsync" + def setUp(self): + super().setUp() + self.config["musicbrainz"]["genres"] = True + @patch( "beets.metadata_plugins.album_for_id", Mock( @@ -88,3 +92,124 @@ def test_custom_format(self): assert "mbsync: Skipping album with no mb_albumid: 'no id'" in logs assert "mbsync: Skipping singleton with no mb_trackid: 'no id'" in logs + + @patch( + "beets.metadata_plugins.album_for_id", + Mock( + side_effect=lambda *_: AlbumInfo( + album_id="album id", + album="new album", + tracks=[ + TrackInfo( + track_id="track id", + title="new title", + genres=["new genre"], + ) + ], + genres=["new genre"], + ) + ), + ) + @patch( + "beets.metadata_plugins.track_for_id", + Mock( + side_effect=lambda *_: TrackInfo( + track_id="singleton id", + title="new title", + genres=["new genre"], + ) + ), + ) + def test_genres_field_is_ignored_when_musicbrainz_genres_is_false(self): + self.config["musicbrainz"]["genres"] = False + album_item = Item( + album="old album", + mb_albumid="album id", + mb_trackid="track id", + data_source="data_source", + genres=["old genre"], + ) + self.lib.add_album([album_item]) + + singleton = Item( + title="old title", + mb_trackid="singleton id", + data_source="data_source", + genres=["old genre"], + ) + self.lib.add(singleton) + + self.run_command("mbsync") + + singleton.load() + assert singleton.title == "new title" + assert singleton.genres == ["old genre"] + + album_item.load() + assert album_item.title == "new title" + assert album_item.mb_trackid == "track id" + assert album_item.get_album().album == "new album" + assert album_item.get_album().genres == ["old genre"] + + @patch( + "beets.metadata_plugins.album_for_id", + Mock( + side_effect=lambda *_: AlbumInfo( + album_id="album id", + album="new album", + tracks=[ + TrackInfo( + track_id="track id", + title="new title", + genres=["new genre"], + ) + ], + genres=["new genre"], + script="new script", + ) + ), + ) + @patch( + "beets.metadata_plugins.track_for_id", + Mock( + side_effect=lambda *_: TrackInfo( + track_id="singleton id", + title="new title", + genres=["new genre"], + script="new script", + ) + ), + ) + def test_excluded_fields_are_excluded_correctly(self): + self.config["mbsync"]["excluded_fields"] = ["script"] + album_item = Item( + album="old album", + mb_albumid="album id", + mb_trackid="track id", + data_source="data_source", + genres=["old genre"], + script="old script", + ) + self.lib.add_album([album_item]) + + singleton = Item( + title="old title", + mb_trackid="singleton id", + data_source="data_source", + script="old script", + ) + self.lib.add(singleton) + + self.run_command("mbsync") + + singleton.load() + assert singleton.title == "new title" + assert singleton.genres == ["new genre"] + assert singleton.script == "old script" + + album_item.load() + assert album_item.title == "new title" + assert album_item.mb_trackid == "track id" + assert album_item.get_album().album == "new album" + assert album_item.get_album().genres == ["new genre"] + assert album_item.get_album().script == "old script"