Implement extended image lists for mediafiles.

Makes the test of 80eded77b1 work.
This commit is contained in:
Thomas Scholtes 2014-03-08 17:27:01 +01:00
parent a9257ae57b
commit c5c87ac46c
2 changed files with 124 additions and 23 deletions

View file

@ -250,6 +250,16 @@ def _sc_encode(gain, peak):
return (u' %08X' * 10) % values
def _image_mime_type(data):
"""Return the MIME type (either image/png or image/jpeg) of the
image data (a bytestring).
"""
kind = imghdr.what(None, h=data)
if kind == 'png':
return 'image/png'
else:
# Currently just fall back to JPEG.
return 'image/jpeg'
# StorageStyle classes describe strategies for accessing values in
# Mutagen file objects.
@ -637,23 +647,28 @@ class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle):
self.as_type = str
def fetch(self, mutagen_file):
try:
frames = mutagen_file.tags.getall(self.key)
return [frame.data for frame in frames]
except IndexError:
return None
frames = mutagen_file.tags.getall(self.key)
images = []
for frame in mutagen_file.tags.getall(self.key):
images.append(TagImage(data=frame.data, desc=frame.desc,
type=TagImage.TYPES[frame.type]))
return images
def store(self, mutagen_file, images):
image = images[0]
frame = mutagen.id3.APIC(
encoding=3,
type=3, # FrontCover
mime=ImageField._mime(image),
desc=u'',
data=image
)
mutagen_file.tags.setall(self.key, [frame])
def store(self, mutagen_file, frames):
mutagen_file.tags.setall(self.key, frames)
def serialize(self, image):
assert isinstance(image, TagImage)
frame = mutagen.id3.Frames[self.key]()
frame.data = image.data
frame.mime = image.mime_type
frame.desc = (image.desc or u'').encode('utf8')
frame.encoding = 3 # UTF-8 encoding of desc
if image.type:
frame.type = list(TagImage.TYPES).index(image.type)
else:
frame.type = 0
return frame
class MP3SoundCheckStorageStyle(SoundCheckStorageStyleMixin, MP3DescStorageStyle):
@ -688,7 +703,7 @@ class ASFImageStorageStyle(ListStorageStyle):
for image in images:
pic = mutagen.asf.ASFByteArrayAttribute()
pic.value = _pack_asf_image(ImageField._mime(image), image)
pic.value = _pack_asf_image(_image_mime_type(image), image)
mutagen_file['WM/Picture'] = [pic]
@ -730,7 +745,7 @@ class VorbisImageStorageStyle(ListStorageStyle):
if image_data is not None:
pic = mutagen.flac.Picture()
pic.data = image_data
pic.mime = ImageField._mime(image_data)
pic.mime = _image_mime_type(image_data)
mutagen_file['metadata_block_picture'] = [
base64.b64encode(pic.write())
]
@ -758,7 +773,7 @@ class FlacImageStorageStyle(ListStorageStyle):
pic = mutagen.flac.Picture()
pic.data = image
pic.type = 3 # front cover
pic.mime = ImageField._mime(image)
pic.mime = _image_mime_type(image)
mutagen_file.add_picture(pic)
@ -916,15 +931,17 @@ class DateItemField(MediaField):
self.date_field._set_date_tuple(mediafile, *items)
class ImageField(MediaField):
class CoverArtField(MediaField):
"""A descriptor providing access to a file's embedded album art.
Holds a bytestring reflecting the image data. The image should
either be a JPEG or a PNG for cross-format compatibility. It's
probably a bad idea to use anything but these two formats.
"""
# TODO make this into shim when ImageField is implemented for all
# formats.
def __init__(self):
super(ImageField, self).__init__(
super(CoverArtField, self).__init__(
MP3ImageStorageStyle(),
MP4ImageStorageStyle(),
ASFImageStorageStyle(),
@ -946,10 +963,21 @@ class ImageField(MediaField):
return 'image/jpeg'
def __get__(self, mediafile, _):
if mediafile.type == 'mp3':
try:
return mediafile.images[0].data
except IndexError:
return None
for style in self.styles(mediafile):
return style.get(mediafile.mgfile)
def __set__(self, mediafile, data):
if mediafile.type == 'mp3':
if data:
mediafile.images = [TagImage(data=data)]
else:
mediafile.images = []
return
if data is not None:
if not isinstance(data, str):
raise ValueError('value must be a byte string or None')
@ -957,6 +985,76 @@ class ImageField(MediaField):
style.set(mediafile.mgfile, data)
class ImageListField(MediaField):
def __init__(self):
super(ImageListField, self).__init__(
MP3ImageStorageStyle(),
MP4ImageStorageStyle(),
ASFImageStorageStyle(),
VorbisImageStorageStyle(),
FlacImageStorageStyle(),
out_type=str,
)
def __get__(self, mediafile, _):
images = []
for style in self.styles(mediafile):
images.extend(style.get_list(mediafile.mgfile))
return images
def __set__(self, mediafile, images):
for style in self.styles(mediafile):
style.set_list(mediafile.mgfile, images)
class TagImage(object):
"""Strucuture representing image data and metadata that can be
stored and retrieved from tags.
The structure has four properties.
* ``data`` The binary data of the image
* ``desc`` An optional descritpion of the image
* ``type`` A string denoting the type in relation to the music.
Must be one of the ``TYPES`` enum.
* ``mime_type`` Read-only property that contains the mime type of
the binary data
"""
TYPES = enum([
'other',
'icon',
'other icon',
'front',
'back',
'leaflet',
'media',
'lead artist',
'artist',
'conductor',
'group',
'composer',
'lyricist',
'recording location',
'recording session',
'performance',
'screen capture',
'fish',
'illustration',
'artist logo',
'publisher logo',
], name='TageImage.TYPES')
def __init__(self, data, desc=None, type=None):
self.data = data
self.desc = desc
self.type = type
@property
def mime_type(self):
if self.data:
return _image_mime_type(self.data)
# MediaFile is a collection of fields.
@ -1302,8 +1400,11 @@ class MediaFile(object):
ASFStorageStyle('beets/Album Artist Credit'),
)
# Album art.
art = ImageField()
# Legacy album art field
art = CoverArtField()
# Image list
images = ImageListField()
# MusicBrainz IDs.
mb_trackid = MediaField(

View file

@ -116,7 +116,7 @@ class ImageStructureTestMixin(object):
mediafile = MediaFile(mediafile.path)
self.assertEqual(len(mediafile.images), 3)
self.assertExtendedImageAttributes(mediafile.images[2],
desc='another cover', type='composer')
desc='the composer', type=TagImage.TYPES.composer)
@unittest.skip('editing list by reference is not implemented yet')
def test_mutate_image_structure(self):