From 7060b512b848fb36f49dcc091f65a6e7baa83d4f Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Tue, 20 Dec 2016 20:15:46 +0800 Subject: [PATCH 1/2] Add Composer, Lyricist and Arranger tags MusicBrainz provids composer, lyricist and arranger infomations related to individual recordings. This commit adds query parameters to fetch them, and write down to media files. Tagging mapping is implemented according MusicBrainz Picard's data: https://picard.musicbrainz.org/docs/mappings/ Signed-off-by: Shen-Ta Hsieh --- beets/autotag/__init__.py | 15 ++++++++ beets/autotag/hooks.py | 8 ++++- beets/autotag/mb.py | 33 +++++++++++++++-- beets/library.py | 2 ++ beets/mediafile.py | 52 +++++++++++++++++++++++++++ test/_common.py | 2 ++ "test/rsrc/unicode\342\200\231d.mp3" | Bin 12820 -> 12820 bytes test/test_mediafile.py | 2 ++ 8 files changed, 111 insertions(+), 3 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index b3cfa95418..09aed89ce6 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 1a9aaf9085..40db6e8d3f 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 @@ def __init__(self, title, track_id, artist=None, artist_id=None, 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 @@ def __init__(self, title, track_id, artist=None, artist_id=None, 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 78d382d878..9c80c149e5 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -54,8 +54,10 @@ def get_message(self): 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 5cffdbe343..32176b68d5 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 9db6ba4db8..65420761f9 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -752,6 +752,45 @@ def store(self, mutagen_file, value): 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 @@ def update(self, dict): ) 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 597f0dc38e..2e74185167 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\342\200\231d.mp3" "b/test/rsrc/unicode\342\200\231d.mp3" index a1418194129a59975c475decb4e845c71c2389a3..7be1e05186ffc38ed3f61c3d5bd56f5b8f37b151 100644 GIT binary patch delta 36 rcmbP|G9_h$B$H5p4+8@OHv Date: Wed, 21 Dec 2016 10:10:59 +0800 Subject: [PATCH 2/2] add compatibility to old version of musicbrainzngs Signed-off-by: Shen-Ta Hsieh --- beets/autotag/mb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 9c80c149e5..55ecfc185d 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -57,7 +57,9 @@ def get_message(self): 'labels', 'artist-credits', 'aliases', 'recording-level-rels', 'work-rels', 'work-level-rels', 'artist-rels'] -TRACK_INCLUDES = ['artists', 'aliases', 'work-level-rels', 'artist-rels'] +TRACK_INCLUDES = ['artists', 'aliases'] +if 'work-level-rels' in musicbrainzngs.VALID_INCLUDES['recording']: + TRACK_INCLUDES += ['work-level-rels', 'artist-rels'] def track_url(trackid):