mirror of
https://github.com/beetbox/beets.git
synced 2026-02-19 22:03:05 +01:00
simplify MediaFile art interface: no type is included
The interface no longer specifies the type of the image embedded in the file; it just returns a bytestring blob. When a type must be stored, it is inferred using the imghdr module, which shoudl reduce the potential for weird bugs when the formats don't correspond.
This commit is contained in:
parent
b48ee61466
commit
73c4bedc41
3 changed files with 51 additions and 67 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# This file is part of beets.
|
||||
# Copyright 2010, Adrian Sampson.
|
||||
# Copyright 2011, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
|
|
@ -38,6 +38,7 @@ import mutagen.monkeysaudio
|
|||
import datetime
|
||||
import re
|
||||
import base64
|
||||
import imghdr
|
||||
from beets.util.enumeration import enum
|
||||
|
||||
__all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile']
|
||||
|
|
@ -466,15 +467,35 @@ class CompositeDateField(object):
|
|||
self.month_field.__set__(obj, val.month)
|
||||
self.day_field.__set__(obj, val.day)
|
||||
|
||||
imagekind = enum('JPEG', 'PNG', name='imagekind')
|
||||
mime2kind = {'image/jpeg': imagekind.JPEG, 'image/png': imagekind.PNG}
|
||||
|
||||
class ImageField(object):
|
||||
"""A descriptor providing access to a file's embedded album art.
|
||||
Returns a `(data, kind)` pair where `data` is a bytesting and `kind`
|
||||
is an `imagekind` (either JPEG or PNG). If no album art is present,
|
||||
returns `None` instead of a tuple.
|
||||
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.
|
||||
"""
|
||||
@classmethod
|
||||
def _mime(cls, 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'
|
||||
|
||||
@classmethod
|
||||
def _mp4kind(cls, data):
|
||||
"""Return the MPEG-4 image type code of the data. If the image
|
||||
is not a PNG or JPEG, JPEG is assumed.
|
||||
"""
|
||||
kind = imghdr.what(None, h=data)
|
||||
if kind == 'png':
|
||||
return mutagen.mp4.MP4Cover.FORMAT_PNG
|
||||
else:
|
||||
return mutagen.mp4.MP4Cover.FORMAT_JPEG
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj.type == 'mp3':
|
||||
# Look for APIC frames.
|
||||
|
|
@ -486,23 +507,15 @@ class ImageField(object):
|
|||
# No APIC frame.
|
||||
return None
|
||||
|
||||
if picframe.mime in mime2kind:
|
||||
return (picframe.data, mime2kind[picframe.mime])
|
||||
else:
|
||||
# Unsupported image type.
|
||||
return None
|
||||
return picframe.data
|
||||
|
||||
elif obj.type == 'mp4':
|
||||
if 'covr' in obj.mgfile:
|
||||
covers = obj.mgfile['covr']
|
||||
if covers:
|
||||
cover = covers[0]
|
||||
if cover.format == cover.FORMAT_JPEG:
|
||||
kind = imagekind.JPEG
|
||||
else:
|
||||
kind = imagekind.PNG
|
||||
# cover is an MP4Cover, which is a subclass of str.
|
||||
return (cover, kind)
|
||||
return cover
|
||||
|
||||
# No cover found.
|
||||
return None
|
||||
|
|
@ -515,18 +528,7 @@ class ImageField(object):
|
|||
if 'metadata_block_picture' not in obj.mgfile:
|
||||
# Try legacy COVERART tags.
|
||||
if 'coverart' in obj.mgfile and obj.mgfile['coverart']:
|
||||
data = base64.b64decode(obj.mgfile['coverart'][0])
|
||||
if 'coverartmime' in obj.mgfile and \
|
||||
obj.mgfile['coverartmime']:
|
||||
mime = obj.mgfile['coverartmime'][0]
|
||||
if mime in mime2kind:
|
||||
kind = mime2kind[mime]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
# Default to JPEG.
|
||||
kind = imagekind.JPEG
|
||||
return (data, kind)
|
||||
return base64.b64decode(obj.mgfile['coverart'][0])
|
||||
return None
|
||||
|
||||
for data in obj.mgfile["metadata_block_picture"]:
|
||||
|
|
@ -538,20 +540,12 @@ class ImageField(object):
|
|||
else:
|
||||
return None
|
||||
|
||||
if pic.mime in mime2kind:
|
||||
return (pic.data, mime2kind[pic.mime])
|
||||
else:
|
||||
# Unsupported.
|
||||
return None
|
||||
return pic.data
|
||||
|
||||
def __set__(self, obj, val):
|
||||
if val is not None:
|
||||
try:
|
||||
data, kind = val
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError('value must be a (data, kind) pair')
|
||||
if not isinstance(kind, imagekind):
|
||||
raise ValueError('kind must be an imagekind')
|
||||
if not isinstance(val, str):
|
||||
raise ValueError('value must be a byte string or None')
|
||||
|
||||
if obj.type == 'mp3':
|
||||
# Clear all APIC frames.
|
||||
|
|
@ -560,25 +554,21 @@ class ImageField(object):
|
|||
# If we're clearing the image, we're done.
|
||||
return
|
||||
|
||||
mime = 'image/jpeg' if kind == imagekind.JPEG else 'image/png'
|
||||
picframe = mutagen.id3.APIC(
|
||||
encoding = 3,
|
||||
mime = mime,
|
||||
mime = self._mime(val),
|
||||
type = 3, # front cover
|
||||
desc = u'',
|
||||
data = data,
|
||||
data = val,
|
||||
)
|
||||
obj.mgfile['APIC'] = picframe
|
||||
|
||||
elif obj.type == 'mp4':
|
||||
if val is None and 'covr' in obj.mgfile:
|
||||
del obj.mgfile['covr']
|
||||
if val is None:
|
||||
if 'covr' in obj.mgfile:
|
||||
del obj.mgfile['covr']
|
||||
else:
|
||||
if kind == imagekind.JPEG:
|
||||
fmt = mutagen.mp4.MP4Cover.FORMAT_JPEG
|
||||
else:
|
||||
fmt = mutagen.mp4.MP4Cover.FORMAT_PNG
|
||||
cover = mutagen.mp4.MP4Cover(data, fmt)
|
||||
cover = mutagen.mp4.MP4Cover(val, self._mp4kind(val))
|
||||
obj.mgfile['covr'] = [cover]
|
||||
|
||||
else:
|
||||
|
|
@ -595,10 +585,9 @@ class ImageField(object):
|
|||
|
||||
# Add new art if provided.
|
||||
if val is not None:
|
||||
mime = 'image/jpeg' if kind == imagekind.JPEG else 'image/png'
|
||||
pic = mutagen.flac.Picture()
|
||||
pic.data = data
|
||||
pic.mime = mime
|
||||
pic.data = val
|
||||
pic.mime = self._mime(val)
|
||||
obj.mgfile['metadata_block_picture'] = [
|
||||
base64.b64encode(pic.write())
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,12 +13,7 @@ def _embed(path, items):
|
|||
"""
|
||||
data = open(syspath(path), 'rb').read()
|
||||
kindstr = imghdr.what(None, data)
|
||||
|
||||
if kindstr == 'jpeg':
|
||||
kind = mediafile.imagekind.JPEG
|
||||
elif kindstr == 'png':
|
||||
kind = mediafile.imagekind.PNG
|
||||
else:
|
||||
if kindstr not in ('jpeg', 'png'):
|
||||
log.error('A file of type %s is not allowed as coverart.' % kindstr)
|
||||
return
|
||||
|
||||
|
|
@ -26,7 +21,7 @@ def _embed(path, items):
|
|||
log.debug('Embedding album art.')
|
||||
for item in items:
|
||||
f = mediafile.MediaFile(syspath(item.path))
|
||||
f.art = (data, kind)
|
||||
f.art = data
|
||||
f.save()
|
||||
|
||||
options = {
|
||||
|
|
@ -93,19 +88,19 @@ def extract(lib, outpath, query):
|
|||
log.error('No album art present in %s - %s.' %
|
||||
(item.artist, item.title))
|
||||
return
|
||||
data, kind = art
|
||||
|
||||
# Add an extension to the filename.
|
||||
if kind == mediafile.imagekind.JPEG:
|
||||
outpath += '.jpg'
|
||||
else:
|
||||
outpath += '.png'
|
||||
ext = imghdr.what(None, h=art)
|
||||
if not ext:
|
||||
log.error('Unknown image type.')
|
||||
return
|
||||
outpath += '.' + ext
|
||||
|
||||
log.info('Extracting album art from: %s - %s\n'
|
||||
'To: %s' % \
|
||||
(item.artist, item.title, outpath))
|
||||
with open(syspath(outpath), 'wb') as f:
|
||||
f.write(data)
|
||||
f.write(art)
|
||||
|
||||
# Automatically embed art into imported albums.
|
||||
@EmbedCoverArtPlugin.listen('album_imported')
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
|
|||
|
||||
# generate the new value we'll try storing
|
||||
if field == 'art':
|
||||
self.value = ('xxx', beets.mediafile.imagekind.PNG)
|
||||
self.value = 'xxx'
|
||||
elif type(correct_dict[field]) is unicode:
|
||||
self.value = u'TestValue: ' + field
|
||||
elif type(correct_dict[field]) is int:
|
||||
|
|
|
|||
Loading…
Reference in a new issue