diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index b3cfa9541..09aed89ce 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -44,6 +44,14 @@ def apply_item_metadata(item, track_info): item.mb_artistid = track_info.artist_id if track_info.data_source: item.data_source = track_info.data_source + + if track_info.lyricist is not None: + item.lyricist = track_info.lyricist + if track_info.composer is not None: + item.composer = track_info.composer + if track_info.arranger is not None: + item.arranger = track_info.arranger + # At the moment, the other metadata is left intact (including album # and track number). Perhaps these should be emptied? @@ -142,3 +150,10 @@ def apply_metadata(album_info, mapping): if track_info.media is not None: item.media = track_info.media + + if track_info.lyricist is not None: + item.lyricist = track_info.lyricist + if track_info.composer is not None: + item.composer = track_info.composer + if track_info.arranger is not None: + item.arranger = track_info.arranger diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 1a9aaf908..40db6e8d3 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -142,6 +142,9 @@ class TrackInfo(object): - ``artist_credit``: Recording-specific artist name - ``data_source``: The original data source (MusicBrainz, Discogs, etc.) - ``data_url``: The data source release URL. + - ``lyricist``: individual track lyricist name + - ``composer``: individual track composer name + - ``arranger`: individual track arranger name Only ``title`` and ``track_id`` are required. The rest of the fields may be None. The indices ``index``, ``medium``, and ``medium_index`` @@ -151,7 +154,7 @@ class TrackInfo(object): length=None, index=None, medium=None, medium_index=None, medium_total=None, artist_sort=None, disctitle=None, artist_credit=None, data_source=None, data_url=None, - media=None): + media=None, lyricist=None, composer=None, arranger=None): self.title = title self.track_id = track_id self.artist = artist @@ -167,6 +170,9 @@ class TrackInfo(object): self.artist_credit = artist_credit self.data_source = data_source self.data_url = data_url + self.lyricist = lyricist + self.composer = composer + self.arranger = arranger # As above, work around a bug in python-musicbrainz-ngs. def decode(self, codec='utf-8'): diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 78d382d87..9c80c149e 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -54,8 +54,10 @@ class MusicBrainzAPIError(util.HumanReadableException): log = logging.getLogger('beets') RELEASE_INCLUDES = ['artists', 'media', 'recordings', 'release-groups', - 'labels', 'artist-credits', 'aliases'] -TRACK_INCLUDES = ['artists', 'aliases'] + 'labels', 'artist-credits', 'aliases', + 'recording-level-rels', 'work-rels', + 'work-level-rels', 'artist-rels'] +TRACK_INCLUDES = ['artists', 'aliases', 'work-level-rels', 'artist-rels'] def track_url(trackid): @@ -179,6 +181,33 @@ def track_info(recording, index=None, medium=None, medium_index=None, if recording.get('length'): info.length = int(recording['length']) / (1000.0) + lyricist = [] + composer = [] + for work_relation in recording.get('work-relation-list', ()): + if work_relation['type'] != 'performance': + continue + for artist_relation in work_relation['work'].get( + 'artist-relation-list', ()): + if 'type' in artist_relation: + type = artist_relation['type'] + if type == 'lyricist': + lyricist.append(artist_relation['artist']['name']) + elif type == 'composer': + composer.append(artist_relation['artist']['name']) + if lyricist: + info.lyricist = u', '.join(lyricist) + if composer: + info.composer = u', '.join(composer) + + arranger = [] + for artist_relation in recording.get('artist-relation-list', ()): + if 'type' in artist_relation: + type = artist_relation['type'] + if type == 'arranger': + arranger.append(artist_relation['artist']['name']) + if arranger: + info.arranger = u', '.join(arranger) + info.decode() return info diff --git a/beets/library.py b/beets/library.py index 5cffdbe34..32176b68d 100755 --- a/beets/library.py +++ b/beets/library.py @@ -415,7 +415,9 @@ class Item(LibModel): 'albumartist_sort': types.STRING, 'albumartist_credit': types.STRING, 'genre': types.STRING, + 'lyricist': types.STRING, 'composer': types.STRING, + 'arranger': types.STRING, 'grouping': types.STRING, 'year': types.PaddedInt(4), 'month': types.PaddedInt(2), diff --git a/beets/mediafile.py b/beets/mediafile.py index 9db6ba4db..65420761f 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -752,6 +752,45 @@ class MP3StorageStyle(StorageStyle): mutagen_file.tags.setall(self.key, [frame]) +class MP3PeopleStorageStyle(MP3StorageStyle): + """Store list of people in ID3 frames. + """ + def __init__(self, key, involvement='', **kwargs): + self.involvement = involvement + super(MP3PeopleStorageStyle, self).__init__(key, **kwargs) + + def store(self, mutagen_file, value): + frames = mutagen_file.tags.getall(self.key) + print(frames) + + # Try modifying in place. + found = False + for frame in frames: + if frame.encoding == mutagen.id3.Encoding.UTF8: + for pair in frame.people: + if pair[0].lower() == self.involvement.lower(): + pair[1] = value + found = True + + # Try creating a new frame. + if not found: + frame = mutagen.id3.Frames[self.key]( + encoding=mutagen.id3.Encoding.UTF8, + people=[[self.involvement, value]] + ) + print(frame) + mutagen_file.tags.add(frame) + + def fetch(self, mutagen_file): + for frame in mutagen_file.tags.getall(self.key): + for pair in frame.people: + if pair[0].lower() == self.involvement.lower(): + try: + return pair[1] + except IndexError: + return None + + class MP3ListStorageStyle(ListStorageStyle, MP3StorageStyle): """Store lists of data in multiple ID3 frames. """ @@ -1590,12 +1629,25 @@ class MediaFile(object): ) genre = genres.single_field() + lyricist = MediaField( + MP3StorageStyle('TEXT'), + MP4StorageStyle('----:com.apple.iTunes:LYRICIST'), + StorageStyle('LYRICIST'), + ASFStorageStyle('WM/Writer'), + ) composer = MediaField( MP3StorageStyle('TCOM'), MP4StorageStyle('\xa9wrt'), StorageStyle('COMPOSER'), ASFStorageStyle('WM/Composer'), ) + arranger = MediaField( + MP3PeopleStorageStyle('TIPL', involvement='arranger'), + MP4StorageStyle('----:com.apple.iTunes:Arranger'), + StorageStyle('ARRANGER'), + ASFStorageStyle('beets/Arranger'), + ) + grouping = MediaField( MP3StorageStyle('TIT1'), MP4StorageStyle('\xa9grp'), diff --git a/test/_common.py b/test/_common.py index 597f0dc38..2e7418516 100644 --- a/test/_common.py +++ b/test/_common.py @@ -65,7 +65,9 @@ def item(lib=None): albumartist=u'the album artist', album=u'the album', genre=u'the genre', + lyricist=u'the lyricist', composer=u'the composer', + arranger=u'the arranger', grouping=u'the grouping', year=1, month=2, diff --git a/test/rsrc/unicode’d.mp3 b/test/rsrc/unicode’d.mp3 index a14181941..7be1e0518 100644 Binary files a/test/rsrc/unicode’d.mp3 and b/test/rsrc/unicode’d.mp3 differ diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 463cd534d..3002b18b5 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -329,7 +329,9 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'artist', 'album', 'genre', + 'lyricist', 'composer', + 'arranger', 'grouping', 'year', 'month',