Repository: beetbox/beets

Issue:
Fetch `remixer` field from MusicBrainz
### Problem
Musicbrainz seems to have a field called `remixer` for remixes, this info isn't written to my files.

Example: https://musicbrainz.org/release/7c7f7ddf-c021-4ee8-993d-d1c330b4a36a

Using it for the artist field, similarly to the `artist_credit` option would also be nice.

According to the [Picard docs](https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html) there would even be a proper place to store it in ID3.

### Setup

* OS: Arch Linux
* Python version:  3.10
* beets version: 1.6.0
* Turning off plugins made problem go away (yes/no): no


Comments:

Patch:
diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py
index 3bd7e8c8..5b8d4513 100644
--- a/beets/autotag/mb.py
+++ b/beets/autotag/mb.py
@@ -202,6 +202,19 @@ def _flatten_artist_credit(credit):
     )
 
 
+def _get_related_artist_names(relations, relation_type):
+    """Given a list representing the artist relationships extract the names of
+    the remixers and concatenate them.
+    """
+    related_artists = []
+
+    for relation in relations:
+        if relation['type'] == relation_type:
+            related_artists.append(relation['artist']['name'])
+
+    return ', '.join(related_artists)
+
+
 def track_info(recording, index=None, medium=None, medium_index=None,
                medium_total=None):
     """Translates a MusicBrainz recording result dictionary into a beets
@@ -231,6 +244,12 @@ def track_info(recording, index=None, medium=None, medium_index=None,
         artist = recording['artist-credit'][0]['artist']
         info.artist_id = artist['id']
 
+    if recording.get('artist-relation-list'):
+        info.remixer = _get_related_artist_names(
+            recording['artist-relation-list'],
+            relation_type='remixer'
+        )
+
     if recording.get('length'):
         info.length = int(recording['length']) / (1000.0)
 
diff --git a/beets/library.py b/beets/library.py
index becf1939..98156397 100644
--- a/beets/library.py
+++ b/beets/library.py
@@ -466,6 +466,7 @@ class Item(LibModel):
         'artist': types.STRING,
         'artist_sort': types.STRING,
         'artist_credit': types.STRING,
+        'remixer': types.STRING,
         'album': types.STRING,
         'albumartist': types.STRING,
         'albumartist_sort': types.STRING,
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 646417f2..3703f422 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -8,6 +8,8 @@ Changelog goes here!
 
 New features:
 
+* We now import the remixer field from Musicbrainz into the library.
+  :bug:`4428`
 * :doc:`/plugins/mbsubmit`: Added a new `mbsubmit` command to print track information to be submitted to MusicBrainz after initial import.
   :bug:`4455`
 * Added `spotify_updated` field to track when the information was last updated.
diff --git a/docs/reference/config.rst b/docs/reference/config.rst
index afabe1aa..e59937dc 100644
--- a/docs/reference/config.rst
+++ b/docs/reference/config.rst
@@ -582,7 +582,7 @@ from_scratch
 ~~~~~~~~~~~~
 
 Either ``yes`` or ``no`` (default), controlling whether existing metadata is
-discarded when a match is applied. This corresponds to the ``--from_scratch``
+discarded when a match is applied. This corresponds to the ``--from-scratch``
 flag to ``beet import``.
 
 .. _quiet:


Test patch:
diff --git a/test/test_mb.py b/test/test_mb.py
index 0b39d6ce..f005c741 100644
--- a/test/test_mb.py
+++ b/test/test_mb.py
@@ -109,8 +109,8 @@ class MBAlbumInfoTest(_common.TestCase):
         })
         return release
 
-    def _make_track(self, title, tr_id, duration, artist=False, video=False,
-                    disambiguation=None):
+    def _make_track(self, title, tr_id, duration, artist=False,
+                    video=False, disambiguation=None, remixer=False):
         track = {
             'title': title,
             'id': tr_id,
@@ -128,6 +128,22 @@ class MBAlbumInfoTest(_common.TestCase):
                     'name': 'RECORDING ARTIST CREDIT',
                 }
             ]
+        if remixer:
+            track['artist-relation-list'] = [
+                {
+                    'type': 'remixer',
+                    'type-id': 'RELATION TYPE ID',
+                    'target': 'RECORDING REMIXER ARTIST ID',
+                    'direction': 'RECORDING RELATION DIRECTION',
+                    'artist':
+                        {
+                            'id': 'RECORDING REMIXER ARTIST ID',
+                            'type': 'RECORDING REMIXER ARTIST TYPE',
+                            'name': 'RECORDING REMIXER ARTIST NAME',
+                            'sort-name': 'RECORDING REMIXER ARTIST SORT NAME'
+                        }
+                }
+            ]
         if video:
             track['video'] = 'true'
         if disambiguation:
@@ -339,6 +355,12 @@ class MBAlbumInfoTest(_common.TestCase):
         self.assertEqual(track.artist_sort, 'TRACK ARTIST SORT NAME')
         self.assertEqual(track.artist_credit, 'TRACK ARTIST CREDIT')
 
+    def test_parse_recording_remixer(self):
+        tracks = [self._make_track('a', 'b', 1, remixer=True)]
+        release = self._make_release(None, tracks=tracks)
+        track = mb.album_info(release).tracks[0]
+        self.assertEqual(track.remixer, 'RECORDING REMIXER ARTIST NAME')
+
     def test_data_source(self):
         release = self._make_release()
         d = mb.album_info(release)