Use CAA data when embedding images into files

This includes both the image type (for vorbis, id3 and asf files) as
well as the comments (as description for vorbis, id3 and asf files).

Additionally, converts the `images` attribute of
picard.metadata.Metadata to a list of dicts instead of tuples for easier
access.
This commit is contained in:
Wieland Hoffmann
2012-10-06 12:26:39 +02:00
parent 1dc1c6653d
commit 6505d49207
15 changed files with 619 additions and 502 deletions

View File

@@ -89,8 +89,10 @@ AMAZON_SERVER = {
AMAZON_IMAGE_PATH = '/images/P/%s.%s.%sZZZZZZZ.jpg'
AMAZON_ASIN_URL_REGEX = re.compile(r'^http://(?:www.)?(.*?)(?:\:[0-9]+)?/.*/([0-9B][0-9A-Z]{9})(?:[^0-9A-Z]|$)')
def _coverart_downloaded(album, metadata, release, try_list, imagetype, data, http, error):
def _coverart_downloaded(album, metadata, release, try_list, imagedata, data, http, error):
album._requests -= 1
imagetype = imagedata["type"]
if error or len(data) < 1000:
if error:
album.log.error(str(http.errorString()))
@@ -101,9 +103,11 @@ def _coverart_downloaded(album, metadata, release, try_list, imagetype, data, ht
filename = None
if imagetype != 'front' and QObject.config.setting["caa_image_type_as_filename"]:
filename = imagetype
metadata.add_image(mime, data, filename)
metadata.add_image(mime, data, filename, imagedata["description"],
imagetype)
for track in album._new_tracks:
track.metadata.add_image(mime, data, filename)
track.metadata.add_image(mime, data, filename,
imagedata["description"], imagetype)
# If the image already was a front image, there might still be some
# other front images in the try_list - remove them.
@@ -157,7 +161,7 @@ def _caa_append_image_to_trylist(try_list, imagedata):
url = QUrl(imagedata["image"])
else:
url = QUrl(imagedata["thumbnails"][thumbsize])
_try_list_append_image_url(try_list, url, imagedata["types"][0])
_try_list_append_image_url(try_list, url, imagedata["types"][0], imagedata["comment"])
def coverart(album, metadata, release, try_list=None):
""" Gets all cover art URLs from the metadata and then attempts to
@@ -210,7 +214,7 @@ def _walk_try_list(album, metadata, release, try_list):
album.tagger.xmlws.download(
url['host'], url['port'], url['path'],
partial(_coverart_downloaded, album, metadata, release,
try_list, url['type']),
try_list, url),
priority=True, important=True)
else:
album._finalize_loading(None)
@@ -252,7 +256,7 @@ def _process_asin_relation(try_list, relation):
})
def _try_list_append_image_url(try_list, parsedUrl, imagetype="front"):
def _try_list_append_image_url(try_list, parsedUrl, imagetype="front", description=""):
QObject.log.debug("Adding %s image %s", imagetype, parsedUrl)
path = str(parsedUrl.encodedPath())
if parsedUrl.hasQuery():
@@ -261,6 +265,7 @@ def _try_list_append_image_url(try_list, parsedUrl, imagetype="front"):
'host': str(parsedUrl.host()),
'port': parsedUrl.port(80),
'path': str(path),
'type': imagetype.lower()
'type': imagetype.lower(),
'description': description,
})

View File

@@ -329,7 +329,10 @@ class File(QtCore.QObject, Item):
settings["cover_image_filename"], dirname, metadata, settings)
overwrite = settings["save_images_overwrite"]
counters = defaultdict(lambda: 0)
for mime, data, filename in metadata.images:
for image in metadata.images:
filename = image["filename"]
data = image["data"]
mime = image["mime"]
if filename is None:
filename = default_filename
else:

View File

@@ -141,12 +141,13 @@ class APEv2File(File):
for name, values in temp.items():
tags[str(name)] = values
if settings['save_images_to_tags']:
for mime, data, _fname in metadata.images:
cover_filename = 'Cover Art (Front)'
cover_filename += mimetype.get_extension(mime, '.jpg')
tags['Cover Art (Front)'] = cover_filename + '\0' + data
break # can't save more than one item with the same name
# (mp3tags does this, but it's against the specs)
for image in metadata.images:
if "front" == image["type"]:
cover_filename = 'Cover Art (Front)'
cover_filename += mimetype.get_extension(image["mime"], '.jpg')
tags['Cover Art (Front)'] = cover_filename + '\0' + image["data"]
break # can't save more than one item with the same name
# (mp3tags does this, but it's against the specs)
tags.save(encode_filename(filename))
class MusepackFile(APEv2File):

View File

@@ -18,6 +18,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from picard.file import File
from picard.formats.id3 import ID3_IMAGE_TYPE_MAP, ID3_REVERSE_IMAGE_TYPE_MAP
from picard.util import encode_filename
from picard.metadata import Metadata
from mutagen.asf import ASF, ASFByteArrayAttribute
@@ -47,7 +48,7 @@ def unpack_image(data):
pos += 2
pos += 2
image_data = data[pos:pos+size]
return (mime.decode("utf-16-le"), image_data, type)
return (mime.decode("utf-16-le"), image_data, type, description.decode("utf-16-le"))
def pack_image(mime, data, type=3, description=""):
"""
@@ -128,9 +129,10 @@ class ASFFile(File):
for name, values in file.tags.items():
if name == 'WM/Picture':
for image in values:
(mime, data, type) = unpack_image(image.value)
if type == 3: # Only cover images
metadata.add_image(mime, data)
(mime, data, type, description) = unpack_image(image.value)
imagetype = ID3_REVERSE_IMAGE_TYPE_MAP.get(type, "other")
metadata.add_image(mime, data, description=description,
type_=imagetype)
continue
elif name not in self.__RTRANS:
continue
@@ -152,8 +154,12 @@ class ASFFile(File):
file.tags.clear()
if settings['save_images_to_tags']:
cover = []
for mime, data, _fname in metadata.images:
tag_data = pack_image(mime, data, 3)
for image in metadata.images:
if self.config.setting["save_only_front_images_to_tags"] and image["type"] != "front":
continue
imagetype = ID3_IMAGE_TYPE_MAP.get(image["type"], 0)
tag_data = pack_image(image["mime"], image["data"], imagetype,
image["description"])
cover.append(ASFByteArrayAttribute(tag_data))
if cover:
file.tags['WM/Picture'] = cover

View File

@@ -20,6 +20,7 @@
import mutagen.apev2
import mutagen.mp3
import mutagen.trueaudio
from collections import defaultdict
from mutagen import id3
from picard.metadata import Metadata
from picard.file import File
@@ -34,8 +35,7 @@ from urlparse import urlparse
def patched_EncodedTextSpec_write(self, frame, value):
try:
enc, term = self._encodings[frame.encoding]
except AttributeError:
enc, term = self.encodings[frame.encoding]
except AttributeError: enc, term = self.encodings[frame.encoding]
return value.encode(enc, 'ignore') + term
id3.EncodedTextSpec.write = patched_EncodedTextSpec_write
@@ -61,6 +61,20 @@ id3.MultiSpec.write = patched_MultiSpec_write
id3.TCMP = compatid3.TCMP
id3.TSO2 = compatid3.TSO2
ID3_IMAGE_TYPE_MAP = {
"other": 0,
"obi": 0,
"tray": 0,
"spine": 0,
"sticker": 0,
"front": 3,
"back": 4,
"booklet": 5,
"medium": 6,
"track": 6,
}
ID3_REVERSE_IMAGE_TYPE_MAP = dict([(v,k) for k, v in ID3_IMAGE_TYPE_MAP.iteritems()])
class ID3File(File):
"""Generic ID3-based file."""
@@ -200,7 +214,9 @@ class ID3File(File):
else:
metadata['discnumber'] = value[0]
elif frameid == 'APIC':
metadata.add_image(frame.mime, frame.data)
imagetype = ID3_REVERSE_IMAGE_TYPE_MAP.get(frame.type, "other")
metadata.add_image(frame.mime, frame.data,
description=frame.desc, type_=imagetype)
elif frameid == 'POPM':
# Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5
if frame.email == self.config.setting['rating_user_email']:
@@ -247,8 +263,24 @@ class ID3File(File):
tags.add(id3.TPOS(encoding=0, text=text))
if settings['save_images_to_tags']:
for mime, data, _fname in metadata.images:
tags.add(id3.APIC(encoding=0, mime=mime, type=3, desc='', data=data))
# This is necessary because mutagens HashKey for APIC frames only
# includes the FrameID (APIC) and description - it's basically
# impossible to save two images, even of different types, without
# any description.
counters = defaultdict(lambda: 0)
for image in metadata.images:
desc = image["description"]
if self.config.setting["save_only_front_images_to_tags"] and image["type"] != "front":
continue
type_ = ID3_IMAGE_TYPE_MAP.get(image["type"], 0)
if counters[desc] > 0:
if desc:
image["description"] = "%s (%i)" % (desc, counters[desc])
else:
image["description"] = "(%i)" % counters[desc]
counters[desc] += 1
tags.add(id3.APIC(encoding=0, mime=image["mime"], type=type_,
desc=image["description"], data=image["data"]))
tmcl = mutagen.id3.TMCL(encoding=encoding, people=[])
tipl = mutagen.id3.TIPL(encoding=encoding, people=[])

View File

@@ -187,11 +187,14 @@ class MP4File(File):
if settings['save_images_to_tags']:
covr = []
for mime, data, _fname in metadata.images:
for image in metadata.images:
if self.config.setting["save_only_front_images_to_tags"] and image["type"] != "front":
continue
mime = image["mime"]
if mime == "image/jpeg":
covr.append(MP4Cover(data, MP4Cover.FORMAT_JPEG))
covr.append(MP4Cover(image["data"], MP4Cover.FORMAT_JPEG))
elif mime == "image/png":
covr.append(MP4Cover(data, MP4Cover.FORMAT_PNG))
covr.append(MP4Cover(image["data"], MP4Cover.FORMAT_PNG))
if covr:
file.tags["covr"] = covr

View File

@@ -25,6 +25,7 @@ import mutagen.oggspeex
import mutagen.oggtheora
import mutagen.oggvorbis
from picard.file import File
from picard.formats.id3 import ID3_IMAGE_TYPE_MAP, ID3_REVERSE_IMAGE_TYPE_MAP
from picard.metadata import Metadata
from picard.util import encode_filename, sanitize_date
@@ -78,12 +79,17 @@ class VCommentFile(File):
name = "totaldiscs"
elif name == "metadata_block_picture":
image = mutagen.flac.Picture(base64.standard_b64decode(value))
metadata.add_image(image.mime, image.data)
imagetype = ID3_REVERSE_IMAGE_TYPE_MAP.get(image.type, "other")
metadata.add_image(image.mime, image.data,
description=image.desc,
type_=imagetype)
continue
metadata.add(name, value)
if self._File == mutagen.flac.FLAC:
for image in file.pictures:
metadata.add_image(image.mime, image.data)
imagetype = ID3_REVERSE_IMAGE_TYPE_MAP.get(image.type, "other")
metadata.add_image(image.mime, image.data,
description=image.desc, type_=imagetype)
# Read the unofficial COVERART tags, for backward compatibillity only
if not "metadata_block_picture" in file.tags:
try:
@@ -139,16 +145,19 @@ class VCommentFile(File):
tags.setdefault(u"DISCTOTAL", []).append(metadata["totaldiscs"])
if settings['save_images_to_tags']:
for mime, data, filename in metadata.images:
image = mutagen.flac.Picture()
image.type = 3 # Cover image
image.data = data
image.mime = mime
for image in metadata.images:
if self.config.setting["save_only_front_images_to_tags"] and image["type"] != "front":
continue
picture = mutagen.flac.Picture()
picture.data = image["data"]
picture.mime = image["mime"]
picture.desc = image["description"]
picture.type = ID3_IMAGE_TYPE_MAP.get(image["type"], 0)
if self._File == mutagen.flac.FLAC:
file.add_picture(image)
file.add_picture(picture)
else:
tags.setdefault(u"METADATA_BLOCK_PICTURE", []).append(
base64.standard_b64encode(image.write()))
base64.standard_b64encode(picture.write()))
file.tags.update(tags)
kwargs = {}
if self._File == mutagen.flac.FLAC and settings["remove_id3_from_flac"]:

View File

@@ -42,8 +42,23 @@ class Metadata(dict):
self.images = []
self.length = 0
def add_image(self, mime, data, filename=None):
self.images.append((mime, data, filename))
def add_image(self, mime, data, filename=None, description="", type_="front"):
"""Adds the image ``data`` to this Metadata object.
Arguments:
mime -- The mimetype of the image
data -- The image data
filename -- The image filename, without an extension
description -- A description for the image
type_ -- The image type - this should be a lower-cased name from
http://musicbrainz.org/doc/Cover_Art/Types
"""
imagedict = {'mime': mime,
'data': data,
'filename': filename,
'description': description,
'type': type_}
self.images.append(imagedict)
def remove_image(self, index):
self.images.pop(index)

View File

@@ -99,7 +99,7 @@ class CoverArtBox(QtGui.QGroupBox):
if self.data:
if pixmap is None:
pixmap = QtGui.QPixmap()
pixmap.loadFromData(self.data[1])
pixmap.loadFromData(self.data["data"])
if not pixmap.isNull():
cover = QtGui.QPixmap(self.shadow)
pixmap = pixmap.scaled(121, 121, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
@@ -112,7 +112,13 @@ class CoverArtBox(QtGui.QGroupBox):
self.item = item
data = None
if metadata and metadata.images:
data = metadata.images[0]
for image in metadata.images:
if image["type"] == "front":
data = image
break
else:
# There's no front image, choose the first one available
data = metadata.images[0]
self.__set_data(data)
release = None
if metadata:

View File

@@ -69,7 +69,8 @@ class InfoDialog(QtGui.QDialog):
text = '<br/>'.join(map(lambda i: '<b>%s</b><br/>%s' % i, info))
self.ui.info.setText(text)
for mime, data, _fname in file.metadata.images:
for image in file.metadata.images:
data = image["data"]
item = QtGui.QListWidgetItem()
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data)

View File

@@ -33,6 +33,7 @@ class CoverOptionsPage(OptionsPage):
options = [
BoolOption("setting", "save_images_to_tags", True),
BoolOption("setting", "save_only_front_images_to_tags", False),
BoolOption("setting", "save_images_to_files", False),
TextOption("setting", "cover_image_filename", "cover"),
BoolOption("setting", "save_images_overwrite", False),
@@ -55,6 +56,7 @@ class CoverOptionsPage(OptionsPage):
def load(self):
self.ui.save_images_to_tags.setChecked(self.config.setting["save_images_to_tags"])
self.ui.cb_embed_front_only.setChecked(self.config.setting["save_only_front_images_to_tags"])
self.ui.save_images_to_files.setChecked(self.config.setting["save_images_to_files"])
self.ui.cover_image_filename.setText(self.config.setting["cover_image_filename"])
self.ui.save_images_overwrite.setChecked(self.config.setting["save_images_overwrite"])
@@ -75,6 +77,7 @@ class CoverOptionsPage(OptionsPage):
def save(self):
self.config.setting["save_images_to_tags"] = self.ui.save_images_to_tags.isChecked()
self.config.setting["save_only_front_images_to_tags"] = self.ui.cb_embed_front_only.isChecked()
self.config.setting["save_images_to_files"] = self.ui.save_images_to_files.isChecked()
self.config.setting["cover_image_filename"] = unicode(self.ui.cover_image_filename.text())
self.config.setting["ca_provider_use_amazon"] =\

View File

@@ -2,8 +2,8 @@
# Form implementation generated from reading ui file 'ui/infodialog.ui'
#
# Created: Tue May 29 19:44:14 2012
# by: PyQt4 UI code generator 4.8.3
# Created: Sat Oct 6 19:08:31 2012
# by: PyQt4 UI code generator 4.9.4
#
# WARNING! All changes made in this file will be lost!
@@ -43,7 +43,7 @@ class Ui_InfoDialog(object):
self.artwork_list.setIconSize(QtCore.QSize(170, 170))
self.artwork_list.setMovement(QtGui.QListView.Static)
self.artwork_list.setFlow(QtGui.QListView.LeftToRight)
self.artwork_list.setProperty(_fromUtf8("isWrapping"), False)
self.artwork_list.setProperty("isWrapping", False)
self.artwork_list.setResizeMode(QtGui.QListView.Fixed)
self.artwork_list.setSpacing(10)
self.artwork_list.setViewMode(QtGui.QListView.IconMode)

View File

@@ -2,8 +2,8 @@
# Form implementation generated from reading ui file 'ui/options_cover.ui'
#
# Created: Sun Sep 30 11:21:59 2012
# by: PyQt4 UI code generator 4.8.3
# Created: Sat Oct 6 19:08:31 2012
# by: PyQt4 UI code generator 4.9.4
#
# WARNING! All changes made in this file will be lost!
@@ -17,7 +17,7 @@ except AttributeError:
class Ui_CoverOptionsPage(object):
def setupUi(self, CoverOptionsPage):
CoverOptionsPage.setObjectName(_fromUtf8("CoverOptionsPage"))
CoverOptionsPage.resize(524, 502)
CoverOptionsPage.resize(525, 526)
self.verticalLayout = QtGui.QVBoxLayout(CoverOptionsPage)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.rename_files = QtGui.QGroupBox(CoverOptionsPage)
@@ -29,6 +29,9 @@ class Ui_CoverOptionsPage(object):
self.save_images_to_tags = QtGui.QCheckBox(self.rename_files)
self.save_images_to_tags.setObjectName(_fromUtf8("save_images_to_tags"))
self.vboxlayout.addWidget(self.save_images_to_tags)
self.cb_embed_front_only = QtGui.QCheckBox(self.rename_files)
self.cb_embed_front_only.setObjectName(_fromUtf8("cb_embed_front_only"))
self.vboxlayout.addWidget(self.cb_embed_front_only)
self.save_images_to_files = QtGui.QCheckBox(self.rename_files)
self.save_images_to_files.setObjectName(_fromUtf8("save_images_to_files"))
self.vboxlayout.addWidget(self.save_images_to_files)
@@ -111,6 +114,7 @@ class Ui_CoverOptionsPage(object):
self.verticalLayout.addItem(spacerItem1)
self.retranslateUi(CoverOptionsPage)
QtCore.QObject.connect(self.save_images_to_tags, QtCore.SIGNAL(_fromUtf8("clicked(bool)")), self.cb_embed_front_only.setEnabled)
QtCore.QMetaObject.connectSlotsByName(CoverOptionsPage)
CoverOptionsPage.setTabOrder(self.save_images_to_tags, self.save_images_to_files)
CoverOptionsPage.setTabOrder(self.save_images_to_files, self.cover_image_filename)
@@ -118,6 +122,7 @@ class Ui_CoverOptionsPage(object):
def retranslateUi(self, CoverOptionsPage):
self.rename_files.setTitle(_("Location"))
self.save_images_to_tags.setText(_("Embed cover images into tags"))
self.cb_embed_front_only.setText(_("Embed only front images"))
self.save_images_to_files.setText(_("Save cover images as separate files"))
self.label_3.setText(_("Use the following file name for images:"))
self.save_images_overwrite.setText(_("Overwrite the file if it already exists"))

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>524</width>
<height>502</height>
<width>525</width>
<height>526</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -30,6 +30,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cb_embed_front_only">
<property name="text">
<string>Embed only front images</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="save_images_to_files">
<property name="text">
@@ -220,5 +227,22 @@
<tabstop>cover_image_filename</tabstop>
</tabstops>
<resources/>
<connections/>
<connections>
<connection>
<sender>save_images_to_tags</sender>
<signal>clicked(bool)</signal>
<receiver>cb_embed_front_only</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>266</x>
<y>44</y>
</hint>
<hint type="destinationlabel">
<x>266</x>
<y>67</y>
</hint>
</hints>
</connection>
</connections>
</ui>