diff --git a/picard/coverart.py b/picard/coverart.py
index dc2d3d734..f8636ca5b 100644
--- a/picard/coverart.py
+++ b/picard/coverart.py
@@ -26,7 +26,7 @@ import re
import traceback
from functools import partial
from picard import config, log
-from picard.metadata import is_front_image
+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
@@ -84,12 +84,21 @@ def _coverart_downloaded(album, metadata, release, try_list, coverinfos, data, h
QObject.tagger.window.set_statusbar_message(N_("Coverart %s downloaded"),
http.url().toString())
mime = mimetype.get_from_data(data, default="image/jpeg")
- filename = None
- if not is_front_image(coverinfos) and config.setting["caa_image_type_as_filename"]:
- filename = coverinfos['type']
- metadata.add_image(mime, data, filename, coverinfos)
- for track in album._new_tracks:
- track.metadata.add_image(mime, data, filename, coverinfos)
+
+ 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), 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.
diff --git a/picard/file.py b/picard/file.py
index 6ae3f2f58..8c5727914 100644
--- a/picard/file.py
+++ b/picard/file.py
@@ -310,60 +310,13 @@ class File(QtCore.QObject, Item):
shutil.move(encode_filename(old_filename), encode_filename(new_filename))
return new_filename
- def _make_image_filename(self, image_filename, dirname, metadata):
- image_filename = self._script_to_filename(image_filename, metadata)
- if not image_filename:
- image_filename = "cover"
- if os.path.isabs(image_filename):
- filename = image_filename
- else:
- filename = os.path.join(dirname, image_filename)
- if config.setting['windows_compatibility'] or sys.platform == 'win32':
- filename = filename.replace('./', '_/').replace('.\\', '_\\')
- return encode_filename(filename)
-
def _save_images(self, dirname, metadata):
"""Save the cover images to disk."""
if not metadata.images:
return
- default_filename = self._make_image_filename(
- config.setting["cover_image_filename"], dirname, metadata)
- overwrite = config.setting["save_images_overwrite"]
counters = defaultdict(lambda: 0)
for image in metadata.images:
- filename = image["filename"]
- data = image["data"]
- mime = image["mime"]
- if filename is None:
- filename = default_filename
- else:
- filename = self._make_image_filename(filename, dirname, metadata)
- image_filename = filename
- ext = mimetype.get_extension(mime, ".jpg")
- if counters[filename] > 0:
- image_filename = "%s (%d)" % (filename, counters[filename])
- counters[filename] = counters[filename] + 1
- while os.path.exists(image_filename + ext) and not overwrite:
- if os.path.getsize(image_filename + ext) == len(data):
- log.debug("Identical file size, not saving %r", image_filename)
- break
- image_filename = "%s (%d)" % (filename, counters[filename])
- counters[filename] = counters[filename] + 1
- else:
- new_filename = image_filename + ext
- # Even if overwrite is enabled we don't need to write the same
- # image multiple times
- if (os.path.exists(new_filename) and
- os.path.getsize(new_filename) == len(data)):
- log.debug("Identical file size, not saving %r", image_filename)
- continue
- log.debug("Saving cover images to %r", image_filename)
- new_dirname = os.path.dirname(image_filename)
- if not os.path.isdir(new_dirname):
- os.makedirs(new_dirname)
- f = open(image_filename + ext, "wb")
- f.write(data)
- f.close()
+ image.save(dirname, metadata, counters)
def _move_additional_files(self, old_filename, new_filename):
"""Move extra files, like playlists..."""
diff --git a/picard/formats/apev2.py b/picard/formats/apev2.py
index eadcce5de..e358e5a1c 100644
--- a/picard/formats/apev2.py
+++ b/picard/formats/apev2.py
@@ -63,7 +63,7 @@ class APEv2File(File):
if '\0' in values.value:
descr, data = values.value.split('\0', 1)
mime = mimetype.get_from_data(data, descr, 'image/jpeg')
- metadata.add_image(mime, data)
+ metadata.make_and_add_image(mime, data)
# skip EXTERNAL and BINARY values
if values.kind != mutagen.apev2.TEXT:
continue
@@ -150,8 +150,8 @@ class APEv2File(File):
if not save_this_image_to_tags(image):
continue
cover_filename = 'Cover Art (Front)'
- cover_filename += mimetype.get_extension(image["mime"], '.jpg')
- tags['Cover Art (Front)'] = mutagen.apev2.APEValue(cover_filename + '\0' + image["data"], mutagen.apev2.BINARY)
+ cover_filename += mimetype.get_extension(image.mimetype, '.jpg')
+ tags['Cover Art (Front)'] = mutagen.apev2.APEValue(cover_filename + '\0' + image.data, mutagen.apev2.BINARY)
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))
diff --git a/picard/formats/asf.py b/picard/formats/asf.py
index 754107e2f..c4a3d6505 100644
--- a/picard/formats/asf.py
+++ b/picard/formats/asf.py
@@ -141,11 +141,8 @@ class ASFFile(File):
if name == 'WM/Picture':
for image in values:
(mime, data, type, description) = unpack_image(image.value)
- extras = {
- 'desc': description,
- 'type': image_type_from_id3_num(type)
- }
- metadata.add_image(mime, data, extras=extras)
+ metadata.make_and_add_image(mime, data, comment=description,
+ imagetype=image_type_from_id3_num(type))
continue
elif name not in self.__RTRANS:
continue
@@ -170,9 +167,9 @@ class ASFFile(File):
for image in metadata.images:
if not save_this_image_to_tags(image):
continue
- tag_data = pack_image(image["mime"], image["data"],
- image_type_as_id3_num(image['type']),
- image['desc'])
+ tag_data = pack_image(image.mimetype, image.data,
+ image_type_as_id3_num(image.imagetype),
+ image.description)
cover.append(ASFByteArrayAttribute(tag_data))
if cover:
file.tags['WM/Picture'] = cover
diff --git a/picard/formats/id3.py b/picard/formats/id3.py
index 60f3113db..d5ff2e5c9 100644
--- a/picard/formats/id3.py
+++ b/picard/formats/id3.py
@@ -259,11 +259,8 @@ class ID3File(File):
else:
log.error("Invalid %s value '%s' dropped in %r", frameid, frame.text[0], filename)
elif frameid == 'APIC':
- extras = {
- 'desc': frame.desc,
- 'type': image_type_from_id3_num(frame.type)
- }
- metadata.add_image(frame.mime, frame.data, extras=extras)
+ metadata.make_and_add_image(frame.mime, frame.data, comment=frame.desc,
+ imagetype=image_type_from_id3_num(frame.type))
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']:
@@ -318,7 +315,7 @@ class ID3File(File):
# any description.
counters = defaultdict(lambda: 0)
for image in metadata.images:
- desc = desctag = image['desc']
+ desc = desctag = image.description
if not save_this_image_to_tags(image):
continue
if counters[desc] > 0:
@@ -328,10 +325,10 @@ class ID3File(File):
desctag = "(%i)" % counters[desc]
counters[desc] += 1
tags.add(id3.APIC(encoding=0,
- mime=image["mime"],
- type=image_type_as_id3_num(image['type']),
+ mime=image.mimetype,
+ type=image_type_as_id3_num(image.imagetype),
desc=desctag,
- data=image["data"]))
+ data=image.data))
tmcl = mutagen.id3.TMCL(encoding=encoding, people=[])
tipl = mutagen.id3.TIPL(encoding=encoding, people=[])
diff --git a/picard/formats/mp4.py b/picard/formats/mp4.py
index 91df4fed4..576f92cf4 100644
--- a/picard/formats/mp4.py
+++ b/picard/formats/mp4.py
@@ -141,9 +141,9 @@ class MP4File(File):
elif name == "covr":
for value in values:
if value.imageformat == value.FORMAT_JPEG:
- metadata.add_image("image/jpeg", value)
+ metadata.make_and_add_image("image/jpeg", value)
elif value.imageformat == value.FORMAT_PNG:
- metadata.add_image("image/png", value)
+ metadata.make_and_add_image("image/png", value)
self._info(metadata, file)
return metadata
@@ -194,11 +194,11 @@ class MP4File(File):
for image in metadata.images:
if not save_this_image_to_tags(image):
continue
- mime = image["mime"]
+ mime = image.mimetype
if mime == "image/jpeg":
- covr.append(MP4Cover(image["data"], MP4Cover.FORMAT_JPEG))
+ covr.append(MP4Cover(image.data, MP4Cover.FORMAT_JPEG))
elif mime == "image/png":
- covr.append(MP4Cover(image["data"], MP4Cover.FORMAT_PNG))
+ covr.append(MP4Cover(image.data, MP4Cover.FORMAT_PNG))
if covr:
file.tags["covr"] = covr
diff --git a/picard/formats/vorbis.py b/picard/formats/vorbis.py
index 538005ec3..7635997ec 100644
--- a/picard/formats/vorbis.py
+++ b/picard/formats/vorbis.py
@@ -96,27 +96,22 @@ class VCommentFile(File):
name = "totaldiscs"
elif name == "metadata_block_picture":
image = mutagen.flac.Picture(base64.standard_b64decode(value))
- extras = {
- 'desc': image.desc,
- 'type': image_type_from_id3_num(image.type)
- }
- metadata.add_image(image.mime, image.data, extras=extras)
+ metadata.make_and_add_image(image.mime, image.data,
+ comment=image.desc,
+ imagetype=image_type_from_id3_num(image.type))
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:
- extras = {
- 'desc': image.desc,
- 'type': image_type_from_id3_num(image.type)
- }
- metadata.add_image(image.mime, image.data, extras=extras)
+ metadata.make_and_add_image(image.mime, image.data, comment=image.desc,
+ imagetype=image_type_from_id3_num(image.type))
# Read the unofficial COVERART tags, for backward compatibillity only
if not "metadata_block_picture" in file.tags:
try:
for index, data in enumerate(file["COVERART"]):
- metadata.add_image(file["COVERARTMIME"][index],
+ metadata.make_and_add_image(file["COVERARTMIME"][index],
base64.standard_b64decode(data)
)
except KeyError:
@@ -175,10 +170,10 @@ class VCommentFile(File):
if not save_this_image_to_tags(image):
continue
picture = mutagen.flac.Picture()
- picture.data = image["data"]
- picture.mime = image["mime"]
- picture.desc = image['desc']
- picture.type = image_type_as_id3_num(image['type'])
+ picture.data = image.data
+ picture.mime = image.mimetype
+ picture.desc = image.description
+ picture.type = image_type_as_id3_num(image.imagetype)
if self._File == mutagen.flac.FLAC:
file.add_picture(picture)
else:
diff --git a/picard/metadata.py b/picard/metadata.py
index a70b79b49..e7af4d78f 100644
--- a/picard/metadata.py
+++ b/picard/metadata.py
@@ -15,13 +15,29 @@
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+import os.path
+import shutil
+import sys
+import tempfile
+import traceback
+
+from hashlib import md5
+from os import fdopen, unlink
from PyQt4.QtCore import QObject
-from picard import config
+from picard import config, log
from picard.plugin import ExtensionPoint
from picard.similarity import similarity2
-from picard.util import load_release_type_scores
+from picard.util import (
+ encode_filename,
+ load_release_type_scores,
+ mimetype as mime,
+ replace_non_ascii,
+ replace_win32_incompat,
+ unaccent,
+)
from picard.mbxml import artist_credit_from_node
MULTI_VALUED_JOINER = '; '
@@ -39,9 +55,105 @@ def is_front_image(image):
def save_this_image_to_tags(image):
if not config.setting["save_only_front_images_to_tags"]:
return True
- if is_front_image(image):
- return True
- return False
+ return image.is_front_image
+
+
+class Image(object):
+ """Wrapper around images. Instantiating an object of this class can raise
+ an IOError or OSError due to the usage of tempfiles underneath.
+ """
+
+ def __init__(self, data, mimetype="image/jpeg", imagetype="front",
+ comment="", filename=None, datahash=""):
+ self.description = comment
+ (fd, self._tempfile_filename) = tempfile.mkstemp(prefix="picard")
+ with fdopen(fd, "wb") as imagefile:
+ imagefile.write(data)
+ log.debug("Saving image (hash=%s) to %r" % (datahash,
+ self._tempfile_filename))
+ self.datalength = len(data)
+ self.extension = mime.get_extension(mime, ".jpg")
+ self.filename = filename
+ self.imagetype = imagetype
+ self.is_front_image = imagetype == "front"
+ self.mimetype = mimetype
+
+ def _make_image_filename(self, filename, dirname, metadata):
+ if config.setting["ascii_filenames"]:
+ if isinstance(filename, unicode):
+ filename = unaccent(filename)
+ filename = replace_non_ascii(filename)
+ if not filename:
+ filename = "cover"
+ if not os.path.isabs(filename):
+ filename = os.path.join(dirname, filename)
+ # replace incompatible characters
+ if config.setting["windows_compatibility"] or sys.platform == "win32":
+ filename = replace_win32_incompat(filename)
+ # remove null characters
+ filename = filename.replace("\x00", "")
+ return encode_filename(filename)
+
+ def save(self, dirname, metadata, counters):
+ """Saves this image.
+
+ :dirname: The name of the directory that contains the audio file
+ :metadata: A metadata object
+ :counters: A dictionary mapping filenames to the amount of how many
+ images with that filename were already saved in `dirname`.
+ """
+ if self.filename is not None:
+ 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
+ else:
+ log.debug("Using default file name %s",
+ config.setting["cover_image_filename"])
+ filename = config.setting["cover_image_filename"]
+ filename = self._make_image_filename(filename, dirname, metadata)
+
+ overwrite = config.setting["save_images_overwrite"]
+ ext = self.extension
+ image_filename = filename
+ if counters[filename] > 0:
+ image_filename = "%s (%d)" % (filename, counters[filename])
+ counters[filename] = counters[filename] + 1
+ while os.path.exists(image_filename + ext) and not overwrite:
+ if os.path.getsize(image_filename + ext) == self.datalength:
+ log.debug("Identical file size, not saving %r", image_filename)
+ break
+ image_filename = "%s (%d)" % (filename, counters[filename])
+ counters[filename] = counters[filename] + 1
+ else:
+ new_filename = image_filename + ext
+ # Even if overwrite is enabled we don't need to write the same
+ # image multiple times
+ if (os.path.exists(new_filename) and
+ os.path.getsize(new_filename) == self.datalength):
+ log.debug("Identical file size, not saving %r", image_filename)
+ return
+ log.debug("Saving cover images to %r", image_filename)
+ new_dirname = os.path.dirname(image_filename)
+ if not os.path.isdir(new_dirname):
+ os.makedirs(new_dirname)
+ shutil.copyfile(self._tempfile_filename, new_filename)
+
+ @property
+ def data(self):
+ """Reads the data from the temporary file created for this image. May
+ raise IOErrors or OSErrors.
+ """
+ with open(self._tempfile_filename, "rb") as imagefile:
+ return imagefile.read()
+
+ def _delete(self):
+ log.debug("Unlinking %s", self._tempfile_filename)
+ try:
+ unlink(self._tempfile_filename)
+ except OSError, e:
+ log.error(traceback.format_exc())
class Metadata(dict):
@@ -61,26 +173,30 @@ class Metadata(dict):
self.images = []
self.length = 0
- def add_image(self, mime, data, filename=None, extras=None):
- """Adds the image ``data`` to this Metadata object.
+ def make_and_add_image(self, mime, data, filename=None, comment="",
+ imagetype="front"):
+ """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.
Arguments:
mime -- The mimetype of the image
data -- The image data
filename -- The image filename, without an extension
- extras -- extra informations about image as dict
- 'desc' : image description or comment, default to ''
- 'type' : main type as a string, default to 'front'
- 'front': if set, CAA front flag is true for this image
+ comment -- image description or comment, default to ''
+ imagetype -- main type as a string, default to 'front'
"""
- imagedict = {'mime': mime,
- 'data': data,
- 'filename': filename,
- 'type': 'front',
- 'desc': ''}
- if extras is not None:
- imagedict.update(extras)
- self.images.append(imagedict)
+ m = md5()
+ m.update(data)
+ datahash = m.hexdigest()
+ QObject.tagger.images.lock()
+ image = QObject.tagger.images[datahash]
+ if image is None:
+ image = Image(data, mime, imagetype, comment, filename,
+ datahash=datahash)
+ QObject.tagger.images[datahash] = image
+ QObject.tagger.images.unlock()
+ self.images.append(image)
def remove_image(self, index):
self.images.pop(index)
diff --git a/picard/tagger.py b/picard/tagger.py
index 1180ce853..fb568d317 100644
--- a/picard/tagger.py
+++ b/picard/tagger.py
@@ -30,7 +30,9 @@ import os.path
import re
import shutil
import signal
+import socket
import sys
+from collections import defaultdict
from functools import partial
from itertools import chain
@@ -76,6 +78,7 @@ from picard.util import (
check_io_encoding,
uniqify,
is_hidden_path,
+ LockableDefaultDict
)
from picard.webservice import XmlWebService
@@ -107,6 +110,25 @@ class Tagger(QtGui.QApplication):
self.save_thread_pool = QtCore.QThreadPool(self)
self.save_thread_pool.setMaxThreadCount(1)
+ if not sys.platform == "win32":
+ # Set up signal handling
+ # It's not possible to call all available functions from signal
+ # handlers, therefore we need to set up a QSocketNotifier to listen
+ # on a socket. Sending data through a socket can be done in a
+ # signal handler, so we use the socket to notify the application of
+ # the signal.
+ # This code is adopted from
+ # https://qt-project.org/doc/qt-4.8/unix-signals.html
+ self.signalfd = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0)
+
+ self.signalnotifier = QtCore.QSocketNotifier(self.signalfd[1].fileno(),
+ QtCore.QSocketNotifier.Read, self)
+ self.signalnotifier.activated.connect(self.sighandler)
+
+ signal.signal(signal.SIGHUP, self.signal)
+ signal.signal(signal.SIGINT, self.signal)
+ signal.signal(signal.SIGTERM, self.signal)
+
# Setup logging
if debug or "PICARD_DEBUG" in os.environ:
log.log_levels = log.log_levels | log.LOG_DEBUG
@@ -165,6 +187,7 @@ class Tagger(QtGui.QApplication):
self.albums = {}
self.release_groups = {}
self.mbid_redirects = {}
+ self.images = LockableDefaultDict(lambda: None)
self.unmatched_files = UnmatchedFiles()
self.nats = None
self.window = MainWindow()
@@ -205,6 +228,8 @@ class Tagger(QtGui.QApplication):
self.nats.update()
def exit(self):
+ log.debug("exit")
+ map(lambda i: i._delete(), self.images.itervalues())
self.stopping = True
self._acoustid.done()
self.thread_pool.waitForDone()
@@ -543,6 +568,18 @@ class Tagger(QtGui.QApplication):
def instance(cls):
return cls.__instance
+ def signal(self, signum, frame):
+ log.debug("signal %i received", signum)
+ # Send a notification about a received signal from the signal handler
+ # to Qt.
+ self.signalfd[0].sendall("a")
+
+ def sighandler(self):
+ self.signalnotifier.setEnabled(False)
+ self.exit()
+ self.quit()
+ self.signalnotifier.setEnabled(True)
+
def help():
print """Usage: %s [OPTIONS] [FILE] [FILE] ...
@@ -577,4 +614,5 @@ def main(localedir=None, autoupdate=True):
elif opt in ("-d", "--debug"):
kwargs["debug"] = True
tagger = Tagger(args, localedir, autoupdate, **kwargs)
+ tagger.startTimer(1000)
sys.exit(tagger.run())
diff --git a/picard/ui/coverartbox.py b/picard/ui/coverartbox.py
index c8a3ad344..c5141ce6c 100644
--- a/picard/ui/coverartbox.py
+++ b/picard/ui/coverartbox.py
@@ -23,7 +23,6 @@ from picard import config, log
from picard.album import Album
from picard.track import Track
from picard.file import File
-from picard.metadata import is_front_image
from picard.util import webbrowser2, encode_filename
@@ -104,7 +103,7 @@ class CoverArtBox(QtGui.QGroupBox):
if self.data:
if pixmap is None:
pixmap = QtGui.QPixmap()
- pixmap.loadFromData(self.data["data"])
+ pixmap.loadFromData(self.data.data)
if not pixmap.isNull():
offx, offy, w, h = (1, 1, 121, 121)
cover = QtGui.QPixmap(self.shadow)
@@ -123,7 +122,7 @@ class CoverArtBox(QtGui.QGroupBox):
data = None
if metadata and metadata.images:
for image in metadata.images:
- if is_front_image(image):
+ if image.is_front_image:
data = image
break
else:
@@ -189,16 +188,16 @@ class CoverArtBox(QtGui.QGroupBox):
self.__set_data([mime, data], pixmap=pixmap)
if isinstance(self.item, Album):
album = self.item
- album.metadata.add_image(mime, data)
+ album.metadata.make_and_add_image(mime, data)
for track in album.tracks:
- track.metadata.add_image(mime, data)
+ track.metadata.make_and_add_image(mime, data)
for file in album.iterfiles():
- file.metadata.add_image(mime, data)
+ file.metadata.make_and_add_image(mime, data)
elif isinstance(self.item, Track):
track = self.item
- track.metadata.add_image(mime, data)
+ track.metadata.make_and_add_image(mime, data)
for file in track.iterfiles():
- file.metadata.add_image(mime, data)
+ file.metadata.make_and_add_image(mime, data)
elif isinstance(self.item, File):
file = self.item
- file.metadata.add_image(mime, data)
+ file.metadata.make_and_add_image(mime, data)
diff --git a/picard/ui/infodialog.py b/picard/ui/infodialog.py
index d2169db8e..feabb662a 100644
--- a/picard/ui/infodialog.py
+++ b/picard/ui/infodialog.py
@@ -49,7 +49,11 @@ class InfoDialog(PicardDialog):
return
for image in images:
- data = image["data"]
+ try:
+ data = image.data
+ except (OSError, IOError), e:
+ log.error(traceback.format_exc())
+ continue
size = len(data)
item = QtGui.QListWidgetItem()
pixmap = QtGui.QPixmap()
@@ -105,8 +109,8 @@ class FileInfoDialog(InfoDialog):
ch = str(ch)
info.append((_('Channels:'), ch))
text = '
'.join(map(lambda i: '%s
%s' %
- (cgi.escape(i[0]),
- cgi.escape(i[1])), info))
+ (QtCore.Qt.escape(i[0]),
+ QtCore.Qt.escape(i[1])), info))
self.ui.info.setText(text)
@@ -124,7 +128,7 @@ class AlbumInfoDialog(InfoDialog):
if album.errors:
tabWidget.setTabText(tab_index, _("&Errors"))
text = '
'.join(map(lambda s: '%s' %
- '
'.join(unicode(cgi.escape(s))
+ '
'.join(unicode(QtCore.Qt.escape(s))
.replace('\t', ' ')
.replace(' ', ' ')
.splitlines()
diff --git a/picard/util/__init__.py b/picard/util/__init__.py
index a1b033d92..5213f7a4a 100644
--- a/picard/util/__init__.py
+++ b/picard/util/__init__.py
@@ -27,6 +27,19 @@ from PyQt4 import QtCore
from encodings import rot_13
from string import Template
from functools import partial
+from collections import defaultdict
+
+
+class LockableDefaultDict(defaultdict):
+ def __init__(self, default):
+ defaultdict.__init__(self, default)
+ self.__lock = QtCore.QReadWriteLock()
+
+ def lock(self):
+ self.__lock.lockForWrite()
+
+ def unlock(self):
+ self.__lock.unlock()
def asciipunct(s):
diff --git a/test/test_formats.py b/test/test_formats.py
index 8c7e5919d..e880f81d9 100644
--- a/test/test_formats.py
+++ b/test/test_formats.py
@@ -1,11 +1,14 @@
import os.path
+import picard.formats
import unittest
import shutil
-from tempfile import mkstemp
+
+
+from PyQt4 import QtCore
+from picard.util import LockableDefaultDict
from picard import config, log
from picard.metadata import Metadata
-import picard.formats
-from PyQt4 import QtCore
+from tempfile import mkstemp
settings = {
@@ -33,6 +36,7 @@ class FakeTagger(QtCore.QObject):
QtCore.QObject.config = config
QtCore.QObject.log = log
self.tagger_stats_changed.connect(self.emit)
+ self.images = LockableDefaultDict(lambda: None)
def emit(self, *args):
pass
@@ -504,6 +508,7 @@ class TestCoverArt(unittest.TestCase):
QtCore.QObject.tagger = FakeTagger()
def _tear_down(self):
+ map(lambda i: i._delete(), QtCore.QObject.tagger.images.itervalues())
os.unlink(self.filename)
def test_asf(self):
@@ -544,13 +549,13 @@ class TestCoverArt(unittest.TestCase):
f = picard.formats.open(self.filename)
metadata = Metadata()
imgdata = tests[t]['head'] + dummyload
- metadata.add_image(tests[t]['mime'], imgdata)
+ metadata.make_and_add_image(tests[t]['mime'], imgdata)
f._save(self.filename, metadata)
f = picard.formats.open(self.filename)
loaded_metadata = f._load(self.filename)
image = loaded_metadata.images[0]
- self.assertEqual(image["mime"], tests[t]['mime'])
- self.assertEqual(image["data"], imgdata)
+ self.assertEqual(image.mimetype, tests[t]['mime'])
+ self.assertEqual(image.data, imgdata)
finally:
self._tear_down()