diff --git a/beets/mediafile.py b/beets/mediafile.py index 95bbd309dd..ace8288b40 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -868,7 +868,7 @@ class VorbisImageStorageStyle(ListStorageStyle): base64-encoded. Values are `Image` objects. """ formats = ['OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', - 'OggFlac', 'APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio'] + 'OggFlac'] def __init__(self): super(VorbisImageStorageStyle, self).__init__( @@ -950,6 +950,75 @@ def delete(self, mutagen_file): mutagen_file.clear_pictures() +class APEv2ImageStorageStyle(ListStorageStyle): + """Store images in APEv2 tags. Values are `Image` objects. + """ + formats = ['APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio', 'OptimFROG'] + + _APE_COVER_TAG_NAMES = { + ImageType.other: 'Cover Art (other)', + ImageType.icon: 'Cover Art (icon)', + ImageType.other_icon: 'Cover Art (other icon)', + ImageType.front: 'Cover Art (front)', + ImageType.back: 'Cover Art (back)', + ImageType.leaflet: 'Cover Art (leaflet)', + ImageType.media: 'Cover Art (media)', + ImageType.lead_artist: 'Cover Art (lead)', + ImageType.artist: 'Cover Art (artist)', + ImageType.conductor: 'Cover Art (conductor)', + ImageType.group: 'Cover Art (band)', + ImageType.composer: 'Cover Art (composer)', + ImageType.lyricist: 'Cover Art (lyricist)', + ImageType.recording_location: 'Cover Art (studio)', + ImageType.recording_session: 'Cover Art (recording)', + ImageType.performance: 'Cover Art (performance)', + ImageType.screen_capture: 'Cover Art (movie scene)', + ImageType.fish: 'Cover Art (colored fish)', + ImageType.illustration: 'Cover Art (illustration)', + ImageType.artist_logo: 'Cover Art (band logo)', + ImageType.publisher_logo: 'Cover Art (publisher logo)'} + + def __init__(self): + super(APEv2ImageStorageStyle, self).__init__(key='') + + def fetch(self, mutagen_file): + images = [] + for cover_type, cover_tag in \ + APEv2ImageStorageStyle._APE_COVER_TAG_NAMES.iteritems(): + try: + frame = mutagen_file[cover_tag] + text_delimiter_index = frame.value.find('\x00') + comment = frame.value[0:text_delimiter_index] \ + if text_delimiter_index > 0 else None + image_data = frame.value[text_delimiter_index + 1:] + images.append(Image(data=image_data, type=cover_type, + desc=comment)) + except KeyError: + pass + + return images + + def set_list(self, mutagen_file, values): + self.delete(mutagen_file) + + for image in values: + image_type = image.type or ImageType.other + comment = image.desc or '' + image_data = comment + "\x00" + image.data + cover_tag = APEv2ImageStorageStyle._APE_COVER_TAG_NAMES[image_type] + mutagen_file[cover_tag] = image_data + + def delete(self, mutagen_file): + """Remove all images from the file. + """ + for cover_tag in \ + APEv2ImageStorageStyle._APE_COVER_TAG_NAMES.itervalues(): + try: + del mutagen_file[cover_tag] + except KeyError: + pass + + # MediaField is a descriptor that represents a single logical field. It # aggregates several StorageStyles describing how to access the data for # each file type. @@ -1206,6 +1275,7 @@ def __init__(self): ASFImageStorageStyle(), VorbisImageStorageStyle(), FlacImageStorageStyle(), + APEv2ImageStorageStyle(), ) def __get__(self, mediafile, _): diff --git a/test/rsrc/image.ape b/test/rsrc/image.ape new file mode 100644 index 0000000000..f8a559289e Binary files /dev/null and b/test/rsrc/image.ape differ diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 56a7bf5724..7a44f602c9 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -822,7 +822,8 @@ class FlacTest(ReadWriteTestBase, PartialTestMixin, } -class ApeTest(ReadWriteTestBase, unittest.TestCase): +class ApeTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, + unittest.TestCase): extension = 'ape' audio_properties = { 'length': 1.0,