Skip to content

Commit

Permalink
Add Composer, Lyricist and Arranger tags
Browse files Browse the repository at this point in the history
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 <ibmibmibm.tw@gmail.com>
  • Loading branch information
ibmibmibm committed Dec 20, 2016
1 parent cba3016 commit 4363f63
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 3 deletions.
14 changes: 14 additions & 0 deletions beets/autotag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ 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?

Expand Down Expand Up @@ -142,3 +149,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
8 changes: 7 additions & 1 deletion beets/autotag/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand All @@ -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
Expand All @@ -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'):
Expand Down
32 changes: 30 additions & 2 deletions beets/autotag/mb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -179,6 +181,32 @@ 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

Expand Down
2 changes: 2 additions & 0 deletions beets/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
52 changes: 52 additions & 0 deletions beets/mediafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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'),
Expand Down
2 changes: 2 additions & 0 deletions test/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Binary file modified test/rsrc/unicode’d.mp3
Binary file not shown.
2 changes: 2 additions & 0 deletions test/test_mediafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
'artist',
'album',
'genre',
'lyricist',
'composer',
'arranger',
'grouping',
'year',
'month',
Expand Down

0 comments on commit 4363f63

Please sign in to comment.