diff --git a/beets/mediafile.py b/beets/mediafile.py index 51d806944..74dc00b11 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -68,8 +68,8 @@ class StorageStyle(object): - key: The Mutagen key used to access the field's data. - list_elem: Store item as a single object or as first element of a list. - - as_type: Which type the value is stored as (unicode, int, or - bool). + - as_type: Which type the value is stored as (unicode, int, + bool, or str). - packing: If this value is packed in a multiple-value storage unit, which type of packing (in the packing enum). Otherwise, None. (Makes as_type irrelevant). @@ -78,8 +78,8 @@ class StorageStyle(object): - ID3 storage only: match against this 'desc' field as well as the key. """ - def __init__(self, key, list_elem = True, as_type = unicode, packing = None, - pack_pos = 0, id3_desc = None): + def __init__(self, key, list_elem = True, as_type = unicode, + packing = None, pack_pos = 0, id3_desc = None): self.key = key self.list_elem = list_elem self.as_type = as_type @@ -214,10 +214,19 @@ class MediaField(object): if entry is None: # no desc match return None else: + # Get the metadata frame object. try: - entry = obj.mgfile[style.key].text + frame = obj.mgfile[style.key] except KeyError: return None + + # For most frame types, the data is in the 'text' field. + # For UFID (used for the MusicBrainz track ID), it's in + # the 'data' field. + if isinstance(frame, mutagen.id3.UFID): + entry = frame.data + else: + entry = frame.text else: # Not MP3. try: @@ -244,7 +253,8 @@ class MediaField(object): else: out = val if obj.type == 'mp3': - if style.id3_desc is not None: # match on desc field + # Try to match on "desc" field. + if style.id3_desc is not None: frames = obj.mgfile.tags.getall(style.key) # try modifying in place @@ -258,10 +268,29 @@ class MediaField(object): # need to make a new frame? if not found: frame = mutagen.id3.Frames[style.key]( - encoding=3, desc=style.id3_desc, text=val) + encoding=3, + desc=style.id3_desc, + text=val + ) obj.mgfile.tags.add(frame) - else: # no match on desc; just replace based on key + # Try to match on "owner" field. + elif style.key.startswith('UFID:'): + owner = style.key.split(':', 1)[1] + frames = obj.mgfile.tags.getall(style.key) + + for frame in frames: + # Replace existing frame data. + if frame.owner == owner: + frame.data = out + else: + # New frame. + frame = mutagen.id3.UFID(owner=owner, data=val) + obj.mgfile.tags.setall('UFID', [frame]) + + # Just replace based on key. + else: + frame = mutagen.id3.Frames[style.key](encoding=3, text=val) obj.mgfile.tags.setall(style.key, [frame]) @@ -366,8 +395,8 @@ class MediaField(object): out = 0 else: out = int(out) - elif style.as_type == bool: - out = bool(out) + elif style.as_type in (bool, str): + out = style.as_type(out) # store the data self._storedata(obj, out, style) @@ -592,6 +621,30 @@ class MediaFile(object): as_type = bool), etc = StorageStyle('compilation') ) + + # MusicBrainz IDs. + mb_trackid = MediaField( + mp3 = StorageStyle('UFID:http://musicbrainz.org', + list_elem = False), + mp4 = StorageStyle( + '----:com.apple.iTunes:MusicBrainz Track Id', + as_type=str), + etc = StorageStyle('musicbrainz_trackid') + ) + mb_albumid = MediaField( + mp3 = StorageStyle('TXXX', id3_desc='MusicBrainz Album Id'), + mp4 = StorageStyle( + '----:com.apple.iTunes:MusicBrainz Album Id', + as_type=str), + etc = StorageStyle('musicbrainz_albumid') + ) + mb_artistid = MediaField( + mp3 = StorageStyle('TXXX', id3_desc='MusicBrainz Artist Id'), + mp4 = StorageStyle( + '----:com.apple.iTunes:MusicBrainz Artist Id', + as_type=str), + etc = StorageStyle('musicbrainz_artistid') + ) @property def length(self): diff --git a/test/rsrc/full.ape b/test/rsrc/full.ape index 5b47f23eb..5c9efc049 100644 Binary files a/test/rsrc/full.ape and b/test/rsrc/full.ape differ diff --git a/test/rsrc/full.flac b/test/rsrc/full.flac index b88b276b4..05cb5ffa8 100644 Binary files a/test/rsrc/full.flac and b/test/rsrc/full.flac differ diff --git a/test/rsrc/full.m4a b/test/rsrc/full.m4a index e2a952f12..32744686a 100755 Binary files a/test/rsrc/full.m4a and b/test/rsrc/full.m4a differ diff --git a/test/rsrc/full.mp3 b/test/rsrc/full.mp3 index 06313f20e..6256bfa64 100755 Binary files a/test/rsrc/full.mp3 and b/test/rsrc/full.mp3 differ diff --git a/test/rsrc/full.ogg b/test/rsrc/full.ogg index 2228edbdb..84229c70c 100644 Binary files a/test/rsrc/full.ogg and b/test/rsrc/full.ogg differ diff --git a/test/test_mediafile_basic.py b/test/test_mediafile_basic.py index 39a2cd872..81bc39d9d 100644 --- a/test/test_mediafile_basic.py +++ b/test/test_mediafile_basic.py @@ -66,6 +66,8 @@ def MakeWritingTest(path, correct_dict, field, testsuffix='_test'): self.value = not correct_dict[field] elif type(correct_dict[field]) is datetime.date: self.value = correct_dict[field] + datetime.timedelta(42) + elif type(correct_dict[field]) is str: + self.value = 'TestValue-' + str(field) else: raise ValueError('unknown field type ' + \ str(type(correct_dict[field]))) @@ -141,16 +143,22 @@ correct_dicts = { 'lyrics': u'the lyrics', 'comments': u'the comments', 'bpm': 6, - 'comp': True + 'comp': True, + 'mb_trackid': '8b882575-08a5-4452-a7a7-cbb8a1531f9e', + 'mb_albumid': '9e873859-8aa4-4790-b985-5a953e8ef628', + 'mb_artistid':'7cf0ea9d-86b9-4dad-ba9e-2355a64899ea', }, # Additional coverage for common cases when "total" fields are unset. - # Created with iTunes. + # Created with iTunes. (Also tests unset MusicBrainz fields.) 'partial': { 'track': 2, 'tracktotal': 0, 'disc': 4, - 'disctotal': 0 + 'disctotal': 0, + 'mb_trackid': '', + 'mb_albumid': '', + 'mb_artistid':'', }, 'min': { 'track': 0, @@ -178,7 +186,10 @@ correct_dicts = { 'lyrics': u'', 'comments': u'', 'bpm': 0, - 'comp': False + 'comp': False, + 'mb_trackid': u'', + 'mb_albumid': u'', + 'mb_artistid':u'', }, # Full release date.