diff --git a/picard/coverart.py b/picard/coverart.py index ecc098279..a3e81b439 100644 --- a/picard/coverart.py +++ b/picard/coverart.py @@ -6,6 +6,7 @@ # Copyright (C) 2007, 2010, 2011 Lukáš Lalinský # Copyright (C) 2011 Michael Wiencek # Copyright (C) 2011-2012 Wieland Hoffmann +# Copyright (C) 2013-2014 Laurent Monin # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -26,7 +27,6 @@ import re import traceback from functools import partial from picard import config, log -from picard.metadata import Image, is_front_image from picard.util import mimetype, parse_amazon_url from picard.const import CAA_HOST, CAA_PORT from PyQt4.QtCore import QUrl, QObject @@ -67,84 +67,22 @@ AMAZON_SERVER = { }, } -AMAZON_IMAGE_PATH = '/images/P/%s.%s.%sZZZZZZZ.jpg' +AMAZON_IMAGE_PATH = '/images/P/%(asin)s.%(serverid)s.%(size)s.jpg' - -def _coverart_http_error(album, http): - album.error_append(u'Coverart error: %s' % (unicode(http.errorString()))) - - -def _coverart_downloaded(album, metadata, release, try_list, coverinfos, data, http, error): - album._requests -= 1 - - if error or len(data) < 1000: - if error: - _coverart_http_error(album, http) - else: - QObject.tagger.window.set_statusbar_message( - N_("Cover art of type '%(type)s' downloaded for %(albumid)s from %(host)s"), - { - 'type': coverinfos['type'].title(), - 'albumid': album.id, - 'host': coverinfos['host'] - } - ) - mime = mimetype.get_from_data(data, default="image/jpeg") - - try: - metadata.make_and_add_image(mime, data, - imagetype=coverinfos['type'], - comment=coverinfos['desc']) - for track in album._new_tracks: - track.metadata.make_and_add_image(mime, data, - imagetype=coverinfos['type'], - comment=coverinfos['desc']) - except (IOError, OSError) as e: - album.error_append(e.message) - album._finalize_loading(error=True) - # It doesn't make sense to store/download more images if we can't - # save them in the temporary folder, abort. - return - - # If the image already was a front image, there might still be some - # other front images in the try_list - remove them. - if is_front_image(coverinfos): - for item in try_list[:]: - if is_front_image(item) and 'archive.org' not in item['host']: - # Hosts other than archive.org only provide front images - try_list.remove(item) - _walk_try_list(album, metadata, release, try_list) - - -def _caa_json_downloaded(album, metadata, release, try_list, data, http, error): - album._requests -= 1 - caa_front_found = False - if error: - _coverart_http_error(album, http) - else: - try: - caa_data = json.loads(data) - except ValueError: - log.debug("Invalid JSON: %s", http.url().toString()) - else: - caa_types = config.setting["caa_image_types"] - caa_types = map(unicode.lower, caa_types) - for image in caa_data["images"]: - if config.setting["caa_approved_only"] and not image["approved"]: - continue - if not image["types"] and "unknown" in caa_types: - image["types"] = [u"Unknown"] - imagetypes = map(unicode.lower, image["types"]) - for imagetype in imagetypes: - if imagetype == "front": - caa_front_found = True - if imagetype in caa_types: - _caa_append_image_to_trylist(try_list, image) - break - - if error or not caa_front_found: - _fill_try_list(album, release, try_list) - _walk_try_list(album, metadata, release, try_list) +# First item in the list will be tried first +AMAZON_SIZES = ( + # huge size option is only available for items + # that have a ZOOMing picture on its amazon web page + # and it doesn't work for all of the domain names + #'_SCRM_', # huge size + 'LZZZZZZZ', # large size, option format 1 + #'_SCLZZZZZZZ_', # large size, option format 3 + 'MZZZZZZZ', # default image size, format 1 + #'_SCMZZZZZZZ_', # medium size, option format 3 + #'TZZZZZZZ', # medium image size, option format 1 + #'_SCTZZZZZZZ_', # small size, option format 3 + #'THUMBZZZ', # small size, option format 1 +) _CAA_THUMBNAIL_SIZE_MAP = { 0: "small", @@ -152,146 +90,376 @@ _CAA_THUMBNAIL_SIZE_MAP = { } -def _caa_append_image_to_trylist(try_list, imagedata): - """Adds URLs to `try_list` depending on the users CAA image size settings.""" - imagesize = config.setting["caa_image_size"] - thumbsize = _CAA_THUMBNAIL_SIZE_MAP.get(imagesize, None) - if thumbsize is None: - url = QUrl(imagedata["image"]) - else: - url = QUrl(imagedata["thumbnails"][thumbsize]) - extras = { - 'type': imagedata["types"][0].lower(), # FIXME: we pass only 1 type - 'desc': imagedata["comment"], - 'front': imagedata['front'], # front image indicator from CAA - } - _try_list_append_image_url(try_list, url, extras) +class CoverArtImage: + + support_types = False + # consider all images as front if types aren't supported by provider + is_front = True + + def __init__(self, url=None, types=[u'front'], comment=''): + if url is not None: + self.parse_url(url) + else: + self.url = None + self.types = types + self.comment = comment + + def parse_url(self, url): + self.url = QUrl(url) + self.host = str(self.url.host()) + self.port = self.url.port(80) + self.path = str(self.url.encodedPath()) + if self.url.hasQuery(): + self.path += '?' + str(self.url.encodedQuery()) + + def is_front_image(self): + # CAA has a flag for "front" image, use it in priority + if self.is_front: + return True + # no caa front flag, use type instead + return u'front' in self.types + + def __repr__(self): + p = [] + if self.url is not None: + p.append("url=%r" % self.url.toString()) + p.append("types=%r" % self.types) + if self.comment: + p.append("comment=%r" % self.comment) + return "%s(%s)" % (self.__class__.__name__, ", ".join(p)) + + def __unicode__(self): + p = [u'Image'] + if self.url is not None: + p.append(u"from %s" % self.url.toString()) + p.append(u"of type %s" % u','.join(self.types)) + if self.comment: + p.append(u"and comment '%s'" % self.comment) + return u' '.join(p) + + def __str__(self): + return unicode(self).encode('utf-8') -def coverart(album, metadata, release, try_list=None): - """ Gets all cover art URLs from the metadata and then attempts to - download the album art. """ +class CaaCoverArtImage(CoverArtImage): - # try_list will be None for the first call - if try_list is None: - try_list = [] + is_front = False + support_types = True + + +class CoverArt: + + def __init__(self, album, metadata, release): + self._queue_new() + self.album = album + self.metadata = metadata + self.release = release + self.caa_types = map(unicode.lower, config.setting["caa_image_types"]) + self.len_caa_types = len(self.caa_types) + self.front_image_found = False + + def __repr__(self): + return "CoverArt for %r" % (self.album) + + def retrieve(self): + """Retrieve available cover art images for the release""" + + if self._caa_has_suitable_artwork(): + self._xmlws_download( + CAA_HOST, + CAA_PORT, + "/release/%s/" % self.metadata["musicbrainz_albumid"], + self._caa_json_downloaded, + priority=True, + important=False + ) + else: + self._queue_from_relationships() + self._download_next_in_queue() + + def _caa_has_suitable_artwork(self): + """Check if CAA artwork has to be downloaded""" + if not config.setting['ca_provider_use_caa']: + log.debug("Cover Art Archive disabled by user") + return False + if not self.len_caa_types: + log.debug("User disabled all Cover Art Archive types") + return False # MB web service indicates if CAA has artwork # http://tickets.musicbrainz.org/browse/MBS-4536 - has_caa_artwork = False - caa_types = map(unicode.lower, config.setting["caa_image_types"]) + if 'cover_art_archive' not in self.release.children: + log.debug("No Cover Art Archive information for %s" + % self.release.id) + return False - if 'cover_art_archive' in release.children: - caa_node = release.children['cover_art_archive'][0] - has_caa_artwork = (caa_node.artwork[0].text == 'true') + caa_node = self.release.children['cover_art_archive'][0] + caa_has_suitable_artwork = caa_node.artwork[0].text == 'true' - if len(caa_types) == 2 and ('front' in caa_types or 'back' in caa_types): - # The OR cases are there to still download and process the CAA - # JSON file if front or back is enabled but not in the CAA and - # another type (that's neither front nor back) is enabled. - # For example, if both front and booklet are enabled and the - # CAA only has booklet images, the front element in the XML - # from the webservice will be false (thus front_in_caa is False - # as well) but it's still necessary to download the booklet - # images by using the fact that back is enabled but there are - # no back images in the CAA. - front_in_caa = caa_node.front[0].text == 'true' or 'front' not in caa_types - back_in_caa = caa_node.back[0].text == 'true' or 'back' not in caa_types - has_caa_artwork = has_caa_artwork and (front_in_caa or back_in_caa) + if not caa_has_suitable_artwork: + log.debug("There are no images in the Cover Art Archive for %s" + % self.release.id) + return False - elif len(caa_types) == 1 and ('front' in caa_types or 'back' in caa_types): - front_in_caa = caa_node.front[0].text == 'true' and 'front' in caa_types - back_in_caa = caa_node.back[0].text == 'true' and 'back' in caa_types - has_caa_artwork = has_caa_artwork and (front_in_caa or back_in_caa) + want_front = 'front' in self.caa_types + want_back = 'back' in self.caa_types + caa_has_front = caa_node.front[0].text == 'true' + caa_has_back = caa_node.back[0].text == 'true' - if config.setting['ca_provider_use_caa'] and has_caa_artwork\ - and len(caa_types) > 0: - log.debug("There are suitable images in the cover art archive for %s" - % release.id) - album._requests += 1 - album.tagger.xmlws.download( - CAA_HOST, CAA_PORT, "/release/%s/" % - metadata["musicbrainz_albumid"], - partial(_caa_json_downloaded, album, metadata, release, try_list), - priority=True, important=False) + if self.len_caa_types == 2 and (want_front or want_back): + # The OR cases are there to still download and process the CAA + # JSON file if front or back is enabled but not in the CAA and + # another type (that's neither front nor back) is enabled. + # For example, if both front and booklet are enabled and the + # CAA only has booklet images, the front element in the XML + # from the webservice will be false (thus front_in_caa is False + # as well) but it's still necessary to download the booklet + # images by using the fact that back is enabled but there are + # no back images in the CAA. + front_in_caa = caa_has_front or not want_front + back_in_caa = caa_has_back or not want_back + caa_has_suitable_artwork = front_in_caa or back_in_caa + + elif self.len_caa_types == 1 and (want_front or want_back): + front_in_caa = caa_has_front and want_front + back_in_caa = caa_has_back and want_back + caa_has_suitable_artwork = front_in_caa or back_in_caa + + if not caa_has_suitable_artwork: + log.debug("There are no suitable images in the Cover Art Archive for %s" + % self.release.id) else: - log.debug("There are no suitable images in the cover art archive for %s" - % release.id) - _fill_try_list(album, release, try_list) - _walk_try_list(album, metadata, release, try_list) + log.debug("There are suitable images in the Cover Art Archive for %s" + % self.release.id) + return caa_has_suitable_artwork -def _fill_try_list(album, release, try_list): - """Fills ``try_list`` by looking at the relationships in ``release``.""" - try: - if 'relation_list' in release.children: - for relation_list in release.relation_list: - if relation_list.target_type == 'url': - for relation in relation_list.relation: - # Use the URL of a cover art link directly - if config.setting['ca_provider_use_whitelist']\ - and (relation.type == 'cover art link' or - relation.type == 'has_cover_art_at'): - _try_list_append_image_url(try_list, QUrl(relation.target[0].text)) - elif config.setting['ca_provider_use_amazon']\ - and (relation.type == 'amazon asin' or - relation.type == 'has_Amazon_ASIN'): - _process_asin_relation(try_list, relation) - except AttributeError: - album.error_append(traceback.format_exc()) + def _coverart_http_error(self, http): + """Append http error to album errors""" + self.album.error_append(u'Coverart error: %s' % + (unicode(http.errorString()))) + def _coverart_downloaded(self, coverartimage, data, http, error): + """Handle finished download, save it to metadata""" + self.album._requests -= 1 -def _walk_try_list(album, metadata, release, try_list): - """Downloads each item in ``try_list``. If there are none left, loading of - ``album`` will be finalized.""" - if len(try_list) == 0: - album._finalize_loading(None) - elif album.id not in album.tagger.albums: - return - else: - # We still have some items to try! - album._requests += 1 - coverinfos = try_list.pop(0) - QObject.tagger.window.set_statusbar_message( - N_("Downloading cover art of type '%(type)s' for %(albumid)s from %(host)s ..."), - { - 'type': coverinfos['type'], - 'albumid': album.id, - 'host': coverinfos['host'] - } + if error: + self._coverart_http_error(http) + elif len(data) < 1000: + log.warning("Not enough data, skipping %s" % coverartimage) + else: + self._message( + N_("Cover art of type '%(type)s' downloaded for %(albumid)s from %(host)s"), + { + 'type': ','.join(coverartimage.types), + 'albumid': self.album.id, + 'host': coverartimage.host + } + ) + mime = mimetype.get_from_data(data, default="image/jpeg") + + try: + self.metadata.make_and_add_image( + mime, + data, + types=coverartimage.types, + comment=coverartimage.comment, + is_front=coverartimage.is_front + ) + for track in self.album._new_tracks: + track.metadata.make_and_add_image( + mime, + data, + types=coverartimage.types, + comment=coverartimage.comment, + is_front=coverartimage.is_front + ) + # If the image already was a front image, + # there might still be some other non-CAA front + # images in the queue - ignore them. + if not self.front_image_found: + self.front_image_found = coverartimage.is_front_image() + + except (IOError, OSError) as e: + self.album.error_append(e.message) + self.album._finalize_loading(error=True) + # It doesn't make sense to store/download more images if we can't + # save them in the temporary folder, abort. + return + + self._download_next_in_queue() + + def _caa_json_downloaded(self, data, http, error): + """Parse CAA JSON file and queue CAA cover art images for download""" + self.album._requests -= 1 + caa_front_found = False + if error: + self._coverart_http_error(http) + else: + try: + caa_data = json.loads(data) + except ValueError: + self.album.error_append( + "Invalid JSON: %s", http.url().toString()) + else: + for image in caa_data["images"]: + if config.setting["caa_approved_only"] and not image["approved"]: + continue + # if image has no type set, we still want it to match + # pseudo type 'unknown' + if not image["types"]: + image["types"] = [u"unknown"] + else: + image["types"] = map(unicode.lower, image["types"]) + # only keep enabled caa types + types = set(image["types"]).intersection( + set(self.caa_types)) + if types: + if not caa_front_found: + caa_front_found = u'front' in types + self._queue_from_caa(image) + + if error or not caa_front_found: + self._queue_from_relationships() + self._download_next_in_queue() + + def _queue_from_caa(self, image): + """Queue images depending on the CAA image size settings.""" + imagesize = config.setting["caa_image_size"] + thumbsize = _CAA_THUMBNAIL_SIZE_MAP.get(imagesize, None) + if thumbsize is None: + url = image["image"] + else: + url = image["thumbnails"][thumbsize] + coverartimage = CaaCoverArtImage( + url, + types=image["types"], + comment=image["comment"], ) - album.tagger.xmlws.download( - coverinfos['host'], coverinfos['port'], coverinfos['path'], - partial(_coverart_downloaded, album, metadata, release, try_list, coverinfos), - priority=True, important=False) + # front image indicator from CAA + coverartimage.is_front = bool(image['front']) + self._queue_put(coverartimage) + def _queue_from_relationships(self): + """Queue images by looking at the release's relationships. + """ + use_whitelist = config.setting['ca_provider_use_whitelist'] + use_amazon = config.setting['ca_provider_use_amazon'] + if not (use_whitelist or use_amazon): + return + log.debug("Trying to get cover art from release relationships ...") + try: + if 'relation_list' in self.release.children: + for relation_list in self.release.relation_list: + if relation_list.target_type == 'url': + for relation in relation_list.relation: + # Use the URL of a cover art link directly + if use_whitelist \ + and (relation.type == 'cover art link' or + relation.type == 'has_cover_art_at'): + self._queue_from_cover_art_relation(relation) + elif use_amazon \ + and (relation.type == 'amazon asin' or + relation.type == 'has_Amazon_ASIN'): + self._queue_from_asin_relation(relation) + except AttributeError: + self.album.error_append(traceback.format_exc()) -def _process_asin_relation(try_list, relation): - amz = parse_amazon_url(relation.target[0].text) - if amz is not None: + def _queue_from_cover_art_relation(self, relation): + """Queue from cover art relationships""" + log.debug("Found cover art link in whitelist") + url = relation.target[0].text + self._queue_put(CoverArtImage(url)) + + def _queue_from_asin_relation(self, relation): + """Queue cover art images from Amazon""" + amz = parse_amazon_url(relation.target[0].text) + if amz is None: + return + log.debug("Found ASIN relation : %s %s", amz['host'], amz['asin']) if amz['host'] in AMAZON_SERVER: serverInfo = AMAZON_SERVER[amz['host']] else: serverInfo = AMAZON_SERVER['amazon.com'] host = serverInfo['server'] - path_l = AMAZON_IMAGE_PATH % (amz['asin'], serverInfo['id'], 'L') - path_m = AMAZON_IMAGE_PATH % (amz['asin'], serverInfo['id'], 'M') - _try_list_append_image_url(try_list, QUrl("http://%s:%s" % (host, path_l))) - _try_list_append_image_url(try_list, QUrl("http://%s:%s" % (host, path_m))) + for size in AMAZON_SIZES: + path = AMAZON_IMAGE_PATH % { + 'asin': amz['asin'], + 'serverid': serverInfo['id'], + 'size': size + } + url = "http://%s:%s" % (host, path) + self._queue_put(CoverArtImage(url)) + + def _download_next_in_queue(self): + """Downloads next item in queue. + If there are none left, loading of album will be finalized. + """ + if self._queue_empty(): + self.album._finalize_loading(None) + return + + if self.album.id not in self.album.tagger.albums: + return + + # We still have some items to try! + coverartimage = self._queue_get() + if not coverartimage.support_types and self.front_image_found: + # we already have one front image, no need to try other type-less + # sources + log.debug("Skipping %r, one front image is already available", + coverartimage) + self._download_next_in_queue() + return + + self._message( + N_("Downloading cover art of type '%(type)s' for %(albumid)s from %(host)s ..."), + { + 'type': ','.join(coverartimage.types), + 'albumid': self.album.id, + 'host': coverartimage.host + } + ) + self._xmlws_download( + coverartimage.host, + coverartimage.port, + coverartimage.path, + partial(self._coverart_downloaded, coverartimage), + priority=True, + important=False + ) + + def _queue_put(self, coverartimage): + "Add an image to queue" + log.debug("Queing %r for download", coverartimage) + self.__queue.append(coverartimage) + + def _queue_get(self): + "Get next image and remove it from queue" + return self.__queue.pop(0) + + def _queue_empty(self): + "Returns True if the queue is empty" + return not self.__queue + + def _queue_new(self): + "Initialize the queue" + self.__queue = [] + + def _message(self, *args, **kwargs): + """Display message to status bar""" + QObject.tagger.window.set_statusbar_message(*args, **kwargs) + + def _xmlws_download(self, *args, **kwargs): + """xmlws.download wrapper""" + self.album._requests += 1 + self.album.tagger.xmlws.download(*args, **kwargs) -def _try_list_append_image_url(try_list, parsedUrl, extras=None): - path = str(parsedUrl.encodedPath()) - if parsedUrl.hasQuery(): - path += '?' + parsedUrl.encodedQuery() - coverinfos = { - 'host': str(parsedUrl.host()), - 'port': parsedUrl.port(80), - 'path': str(path), - 'type': 'front', - 'desc': '' - } - if extras is not None: - coverinfos.update(extras) - log.debug("Adding %s image %s", coverinfos['type'], parsedUrl.toString()) - try_list.append(coverinfos) +def coverart(album, metadata, release): + """Gets all cover art URLs from the metadata and then attempts to + download the album art. """ + + coverart = CoverArt(album, metadata, release) + coverart.retrieve() + log.debug("New %r", coverart) diff --git a/picard/formats/asf.py b/picard/formats/asf.py index 6b0eed69d..75e95d627 100644 --- a/picard/formats/asf.py +++ b/picard/formats/asf.py @@ -19,7 +19,7 @@ from picard import config, log from picard.file import File -from picard.formats.id3 import image_type_from_id3_num, image_type_as_id3_num +from picard.formats.id3 import types_and_front, image_type_as_id3_num from picard.util import encode_filename from picard.metadata import Metadata, save_this_image_to_tags from mutagen.asf import ASF, ASFByteArrayAttribute @@ -141,8 +141,9 @@ class ASFFile(File): if name == 'WM/Picture': for image in values: (mime, data, type, description) = unpack_image(image.value) + types, is_front = types_and_front(type) metadata.make_and_add_image(mime, data, comment=description, - imagetype=image_type_from_id3_num(type)) + types=types, is_front=is_front) continue elif name not in self.__RTRANS: continue @@ -168,7 +169,7 @@ class ASFFile(File): if not save_this_image_to_tags(image): continue tag_data = pack_image(image.mimetype, image.data, - image_type_as_id3_num(image.imagetype), + image_type_as_id3_num(image.maintype()), image.description) cover.append(ASFByteArrayAttribute(tag_data)) if cover: diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 5afb0738f..7aacfc89f 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -92,6 +92,12 @@ def image_type_as_id3_num(texttype): return __ID3_IMAGE_TYPE_MAP.get(texttype, 0) +def types_and_front(id3type): + imgtype = image_type_from_id3_num(id3type) + is_front = imgtype == 'front' + return [unicode(imgtype)], is_front + + class ID3File(File): """Generic ID3-based file.""" @@ -255,8 +261,9 @@ class ID3File(File): else: log.error("Invalid %s value '%s' dropped in %r", frameid, frame.text[0], filename) elif frameid == 'APIC': + types, is_front = types_and_front(frame.type) metadata.make_and_add_image(frame.mime, frame.data, comment=frame.desc, - imagetype=image_type_from_id3_num(frame.type)) + types=types, is_front=is_front) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == config.setting['rating_user_email']: @@ -322,7 +329,7 @@ class ID3File(File): counters[desc] += 1 tags.add(id3.APIC(encoding=0, mime=image.mimetype, - type=image_type_as_id3_num(image.imagetype), + type=image_type_as_id3_num(image.maintype()), desc=desctag, data=image.data)) diff --git a/picard/formats/vorbis.py b/picard/formats/vorbis.py index bf9161988..1e42f9bca 100644 --- a/picard/formats/vorbis.py +++ b/picard/formats/vorbis.py @@ -32,7 +32,7 @@ except ImportError: with_opus = False from picard import config, log from picard.file import File -from picard.formats.id3 import image_type_from_id3_num, image_type_as_id3_num +from picard.formats.id3 import types_and_front, image_type_as_id3_num from picard.metadata import Metadata, save_this_image_to_tags from picard.util import encode_filename, sanitize_date @@ -96,17 +96,20 @@ class VCommentFile(File): name = "totaldiscs" elif name == "metadata_block_picture": image = mutagen.flac.Picture(base64.standard_b64decode(value)) + types, is_front = types_and_front(image.type) metadata.make_and_add_image(image.mime, image.data, comment=image.desc, - imagetype=image_type_from_id3_num(image.type)) + types=types, + is_front=is_front) continue elif name in self.__translate: name = self.__translate[name] metadata.add(name, value) if self._File == mutagen.flac.FLAC: for image in file.pictures: + types, is_front = types_and_front(image.type) metadata.make_and_add_image(image.mime, image.data, comment=image.desc, - imagetype=image_type_from_id3_num(image.type)) + types=types, is_front=is_front) # Read the unofficial COVERART tags, for backward compatibillity only if not "metadata_block_picture" in file.tags: try: @@ -173,7 +176,7 @@ class VCommentFile(File): picture.data = image.data picture.mime = image.mimetype picture.desc = image.description - picture.type = image_type_as_id3_num(image.imagetype) + picture.type = image_type_as_id3_num(image.maintype()) if self._File == mutagen.flac.FLAC: file.add_picture(picture) else: diff --git a/picard/metadata.py b/picard/metadata.py index fa5e6c37e..964bbf941 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -45,19 +45,10 @@ from picard.mbxml import artist_credit_from_node MULTI_VALUED_JOINER = '; ' -def is_front_image(image): - # CAA has a flag for "front" image, use it in priority - caa_front = image.get('front', None) - if caa_front is None: - # no caa front flag, use type instead - return (image['type'] == 'front') - return caa_front - - def save_this_image_to_tags(image): if not config.setting["save_only_front_images_to_tags"]: return True - return image.is_front_image + return image.is_front class Image(object): @@ -66,8 +57,8 @@ class Image(object): an IOError or OSError due to the usage of tempfiles underneath. """ - def __init__(self, data, mimetype="image/jpeg", imagetype="front", - comment="", filename=None, datahash=""): + def __init__(self, data, mimetype="image/jpeg", types=[u"front"], + comment="", filename=None, datahash="", is_front=True): self.description = comment (fd, self._tempfile_filename) = tempfile.mkstemp(prefix="picard") with fdopen(fd, "wb") as imagefile: @@ -77,10 +68,13 @@ class Image(object): self.datalength = len(data) self.extension = mime.get_extension(mime, ".jpg") self.filename = filename - self.imagetype = imagetype - self.is_front_image = imagetype == "front" + self.types = types + self.is_front = is_front self.mimetype = mimetype + def maintype(self): + return self.types[0] + def _make_image_filename(self, filename, dirname, metadata): if config.setting["ascii_filenames"]: if isinstance(filename, unicode): @@ -110,8 +104,8 @@ class Image(object): log.debug("Using the custom file name %s", self.filename) filename = self.filename elif config.setting["caa_image_type_as_filename"]: - log.debug("Using image type %s", self.imagetype) - filename = self.imagetype + filename = self.maintype() + log.debug("Make filename from types: %r -> %r", self.types, filename) else: log.debug("Using default file name %s", config.setting["cover_image_filename"]) @@ -180,7 +174,7 @@ class Metadata(dict): self.length = 0 def make_and_add_image(self, mime, data, filename=None, comment="", - imagetype="front"): + types=[u"front"], is_front=True): """Build a new image object from ``data`` and adds it to this Metadata object. If an image with the same MD5 hash has already been added to any Metadata object, that file will be reused. @@ -190,7 +184,8 @@ class Metadata(dict): data -- The image data filename -- The image filename, without an extension comment -- image description or comment, default to '' - imagetype -- main type as a string, default to 'front' + types -- list of types, default to [u'front'] + is_front -- mark image as front image """ m = md5() m.update(data) @@ -198,8 +193,9 @@ class Metadata(dict): QObject.tagger.images.lock() image = QObject.tagger.images[datahash] if image is None: - image = Image(data, mime, imagetype, comment, filename, - datahash=datahash) + image = Image(data, mime, types, comment, filename, + datahash=datahash, + is_front=is_front) QObject.tagger.images[datahash] = image QObject.tagger.images.unlock() self.images.append(image) diff --git a/picard/ui/coverartbox.py b/picard/ui/coverartbox.py index 9a2122f0c..f20aba56d 100644 --- a/picard/ui/coverartbox.py +++ b/picard/ui/coverartbox.py @@ -122,7 +122,7 @@ class CoverArtBox(QtGui.QGroupBox): data = None if metadata and metadata.images: for image in metadata.images: - if image.is_front_image: + if image.is_front: data = image break else: