Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions beetsplug/mbsync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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.
Expand All @@ -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()
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~
Expand Down
25 changes: 25 additions & 0 deletions docs/plugins/mbsync.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----

Expand Down
125 changes: 125 additions & 0 deletions test/plugins/test_mbsync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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"