mirror of
https://github.com/beetbox/beets.git
synced 2025-12-20 23:53:15 +01:00
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 <ibmibmibm.tw@gmail.com>
This commit is contained in:
parent
cba30163cd
commit
7060b512b8
8 changed files with 111 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -329,7 +329,9 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
|
|||
'artist',
|
||||
'album',
|
||||
'genre',
|
||||
'lyricist',
|
||||
'composer',
|
||||
'arranger',
|
||||
'grouping',
|
||||
'year',
|
||||
'month',
|
||||
|
|
|
|||
Loading…
Reference in a new issue