From 1387f302954b999a3b81af4e54f2b5005ecf66c4 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 19 May 2012 15:42:08 -0700 Subject: [PATCH] parse artist credits from MB responses (GC-286) --- beets/autotag/hooks.py | 8 ++++++-- beets/autotag/mb.py | 40 ++++++++++++++++++++++++++++------------ test/test_mb.py | 38 +++++++++++++++++++++++++++++++------- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index e8ec0a832..d3b0686cc 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -47,6 +47,7 @@ class AlbumInfo(object): - ``albumstatus``: MusicBrainz release status (Official, etc.) - ``media``: delivery mechanism (Vinyl, etc.) - ``albumdisambig``: MusicBrainz release disambiguation comment + - ``artist_credit``: Release-specific artist name The fields up through ``tracks`` are required. The others are optional and may be None. @@ -56,7 +57,7 @@ class AlbumInfo(object): label=None, mediums=None, artist_sort=None, releasegroup_id=None, catalognum=None, script=None, language=None, country=None, albumstatus=None, media=None, - albumdisambig=None): + albumdisambig=None, artist_credit=None): self.album = album self.album_id = album_id self.artist = artist @@ -79,6 +80,7 @@ class AlbumInfo(object): self.albumstatus = albumstatus self.media = media self.albumdisambig = albumdisambig + self.artist_credit = artist_credit class TrackInfo(object): """Describes a canonical track present on a release. Appears as part @@ -93,13 +95,14 @@ class TrackInfo(object): - ``medium_index``: the track's position on the disc - ``artist_sort``: name of the track artist for sorting - ``disctitle``: name of the individual medium (subtitle) + - ``artist_credit``: Recording-specific artist name Only ``title`` and ``track_id`` are required. The rest of the fields may be None. """ def __init__(self, title, track_id, artist=None, artist_id=None, length=None, medium=None, medium_index=None, - artist_sort=None, disctitle=None): + artist_sort=None, disctitle=None, artist_credit=None): self.title = title self.track_id = track_id self.artist = artist @@ -109,6 +112,7 @@ class TrackInfo(object): self.medium_index = medium_index self.artist_sort = artist_sort self.disctitle = disctitle + self.artist_credit = artist_credit # Aggregation of sources. diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index b6edd760a..d6d8c05f5 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -57,6 +57,26 @@ else: _mb_release_search = musicbrainzngs.search_releases _mb_recording_search = musicbrainzngs.search_recordings +def _flatten_artist_credit(credit): + """Given a list representing an ``artist-credit`` block, flatten the + data into a pair of strings: the "canonical" joined artist name and + the specific "credit" joined artist name. + """ + artist_parts = [] + artist_credit_parts = [] + for el in credit: + if isinstance(el, basestring): + artist_parts.append(el) + artist_credit_parts.append(el) + else: + cur_artist_name = el['artist']['name'] + artist_parts.append(cur_artist_name) + if 'name' in el: # Special artist credit. + artist_credit_parts.append(el['name']) + else: + artist_credit_parts.append(cur_artist_name) + return ''.join(artist_parts), ''.join(artist_credit_parts) + def track_info(recording, medium=None, medium_index=None): """Translates a MusicBrainz recording result dictionary into a beets ``TrackInfo`` object. ``medium_index``, if provided, is the track's @@ -67,12 +87,12 @@ def track_info(recording, medium=None, medium_index=None): medium=medium, medium_index=medium_index) - # Get the track artist credit. - if recording.get('artist-credit-phrase'): - info.artist = recording['artist-credit-phrase'] + if recording.get('artist-credit'): + # Get the artist names. + info.artist, info.artist_credit = \ + _flatten_artist_credit(recording['artist-credit']) - # Get the ID and sort name of the first artist. - if 'artist-credit' in recording: + # Get the ID and sort name of the first artist. artist = recording['artist-credit'][0]['artist'] info.artist_id = artist['id'] info.artist_sort = artist['sort-name'] @@ -97,13 +117,8 @@ def album_info(release): AlbumInfo object containing the interesting data about that release. """ # Get artist name using join phrases. - artist_parts = [] - for el in release['artist-credit']: - if isinstance(el, basestring): - artist_parts.append(el) - else: - artist_parts.append(el['artist']['name']) - artist_name = ''.join(artist_parts) + artist_name, artist_credit_name = \ + _flatten_artist_credit(release['artist-credit']) # Basic info. track_infos = [] @@ -127,6 +142,7 @@ def album_info(release): track_infos, mediums=len(release['medium-list']), artist_sort=release['artist-credit'][0]['artist']['sort-name'], + artist_credit=artist_credit_name, ) info.va = info.artist_id == VARIOUS_ARTISTS_ID info.asin = release.get('asin') diff --git a/test/test_mb.py b/test/test_mb.py index 33ef42bf9..3e7282ab7 100644 --- a/test/test_mb.py +++ b/test/test_mb.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2010, Adrian Sampson. +# Copyright 2012, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -30,11 +30,14 @@ class MBAlbumInfoTest(unittest.TestCase): 'disambiguation': 'DISAMBIGUATION', }, 'artist-credit': [ - {'artist': { - 'name': 'ARTIST NAME', - 'id': 'ARTIST ID', - 'sort-name': 'ARTIST SORT NAME', - }} + { + 'artist': { + 'name': 'ARTIST NAME', + 'id': 'ARTIST ID', + 'sort-name': 'ARTIST SORT NAME', + }, + 'name': 'ARTIST CREDIT', + } ], 'date': '3001', 'medium-list': [], @@ -64,13 +67,24 @@ class MBAlbumInfoTest(unittest.TestCase): }) return release - def _make_track(self, title, tr_id, duration): + def _make_track(self, title, tr_id, duration, artist=False): track = { 'title': title, 'id': tr_id, } if duration is not None: track['length'] = duration + if artist: + track['artist-credit'] = [ + { + 'artist': { + 'name': 'TRACK ARTIST NAME', + 'id': 'TRACK ARTIST ID', + 'sort-name': 'TRACK ARTIST SORT NAME', + }, + 'name': 'TRACK ARTIST CREDIT', + } + ] return track def test_parse_release_with_year(self): @@ -81,6 +95,7 @@ class MBAlbumInfoTest(unittest.TestCase): self.assertEqual(d.artist, 'ARTIST NAME') self.assertEqual(d.artist_id, 'ARTIST ID') self.assertEqual(d.year, 1984) + self.assertEqual(d.artist_credit, 'ARTIST CREDIT') def test_parse_release_type(self): release = self._make_release('1984') @@ -245,6 +260,15 @@ class MBAlbumInfoTest(unittest.TestCase): d = mb.album_info(release) self.assertEqual(d.language, None) + def test_parse_track_artist(self): + tracks = [self._make_track('a', 'b', 1, True)] + release = self._make_release(None, tracks=tracks) + track = mb.album_info(release).tracks[0] + self.assertEqual(track.artist, 'TRACK ARTIST NAME') + self.assertEqual(track.artist_id, 'TRACK ARTIST ID') + self.assertEqual(track.artist_sort, 'TRACK ARTIST SORT NAME') + self.assertEqual(track.artist_credit, 'TRACK ARTIST CREDIT') + def suite(): return unittest.TestLoader().loadTestsFromName(__name__)