diff --git a/contrib/plugins/titlecase.py b/contrib/plugins/titlecase.py
index bce28d3bf..2e03fa144 100644
--- a/contrib/plugins/titlecase.py
+++ b/contrib/plugins/titlecase.py
@@ -26,7 +26,7 @@ def utitle(string):
for i in xrange(1, len(string)):
s = string[i]
# Special case apostrophe in the middle of a word.
- if u"'" == s and string[i-1].isalpha(): cap = False
+ if s in u"’'" and string[i-1].isalpha(): cap = False
elif iswbound(s): cap = True
elif cap and s.isalpha():
cap = False
diff --git a/picard/acoustid.py b/picard/acoustid.py
index b2f81b824..f38766fca 100644
--- a/picard/acoustid.py
+++ b/picard/acoustid.py
@@ -94,18 +94,20 @@ class AcoustIDClient(QtCore.QObject):
metadata_el = doc.append_child('metadata')
acoustid_el = metadata_el.append_child('acoustid')
recording_list_el = acoustid_el.append_child('recording_list')
- acoustid_id = None
- results = document.response[0].results[0].children.get('result')
- if results:
- result = results[0]
- acoustid_id = result.id[0].text
- if 'recordings' in result.children:
- for recording in result.recordings[0].recording:
- parse_recording(recording)
+ status = document.response[0].status[0].text
+ if status == 'ok':
+ results = document.response[0].results[0].children.get('result')
+ if results:
+ result = results[0]
+ file.metadata['acoustid_id'] = result.id[0].text
+ if 'recordings' in result.children:
+ for recording in result.recordings[0].recording:
+ parse_recording(recording)
+ else:
+ error_message = document.response[0].error[0].message[0].text
+ self.log.error("Fingerprint lookup failed: %r", error_message)
- if acoustid_id is not None:
- file.metadata['acoustid_id'] = acoustid_id
next(doc, http, error)
def _lookup_fingerprint(self, next, filename, result=None, error=None):
@@ -127,7 +129,7 @@ class AcoustIDClient(QtCore.QObject):
file.acoustid_length = length
self.tagger.acoustidmanager.add(file, None)
params['fingerprint'] = fingerprint
- params['duration'] = str((file.metadata.length or 1000 * length) / 1000)
+ params['duration'] = str(length)
else:
type, trackid = result
params['trackid'] = trackid
diff --git a/picard/album.py b/picard/album.py
index 2a9a81068..7a744002f 100644
--- a/picard/album.py
+++ b/picard/album.py
@@ -32,6 +32,7 @@ from picard.script import ScriptParser
from picard.ui.item import Item
from picard.util import format_time, queue, mbid_validate, asciipunct
from picard.cluster import Cluster
+from picard.collection import Collection, user_collections, load_user_collections
from picard.mbxml import (
release_group_to_metadata,
release_to_metadata,
@@ -117,6 +118,15 @@ class Album(DataObject, Item):
m['totaldiscs'] = release_node.medium_list[0].count
+ # Add album to collections
+ if "collection_list" in release_node.children:
+ for node in release_node.collection_list[0].collection:
+ if node.editor[0].text.lower() == self.config.setting["username"].lower():
+ if node.id not in user_collections:
+ user_collections[node.id] = \
+ Collection(node.id, node.name[0].text, node.release_list[0].count)
+ user_collections[node.id].releases.add(self.id)
+
# Run album metadata plugins
try:
run_album_metadata_processors(self, m, release_node)
@@ -270,7 +280,8 @@ class Album(DataObject, Item):
self._new_tracks = []
self._requests = 1
require_authentication = False
- inc = ['release-groups', 'media', 'recordings', 'artist-credits', 'artists', 'aliases', 'labels', 'isrcs']
+ inc = ['release-groups', 'media', 'recordings', 'artist-credits',
+ 'artists', 'aliases', 'labels', 'isrcs', 'collections']
if self.config.setting['release_ars'] or self.config.setting['track_ars']:
inc += ['artist-rels', 'release-rels', 'url-rels', 'recording-rels', 'work-rels']
if self.config.setting['track_ars']:
@@ -378,6 +389,9 @@ class Album(DataObject, Item):
def can_refresh(self):
return True
+ def can_view_info(self):
+ return bool(self.loaded and self.metadata and self.metadata.images)
+
def is_album_like(self):
return True
diff --git a/picard/cluster.py b/picard/cluster.py
index 3ccfe729b..4fcd18149 100644
--- a/picard/cluster.py
+++ b/picard/cluster.py
@@ -162,7 +162,9 @@ class Cluster(QtCore.QObject, Item):
self.tagger.move_files_to_album(self.files, match[1].id)
def lookup_metadata(self):
- """ Try to identify the cluster using the existing metadata. """
+ """Try to identify the cluster using the existing metadata."""
+ if self.lookup_task:
+ return
self.tagger.window.set_statusbar_message(N_("Looking up the metadata for cluster %s..."), self.metadata['album'])
self.lookup_task = self.tagger.xmlws.find_releases(self._lookup_finished,
artist=self.metadata['albumartist'],
@@ -278,6 +280,10 @@ class ClusterList(list, Item):
def can_browser_lookup(self):
return False
+ def lookup_metadata(self):
+ for cluster in self:
+ cluster.lookup_metadata()
+
class ClusterDict(object):
diff --git a/picard/collection.py b/picard/collection.py
new file mode 100644
index 000000000..c10ed5770
--- /dev/null
+++ b/picard/collection.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+#
+# Picard, the next-generation MusicBrainz tagger
+# Copyright (C) 2013 Michael Wiencek
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# 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.
+
+from PyQt4 import QtCore
+from picard.util import partial
+
+
+user_collections = {}
+
+
+class Collection(QtCore.QObject):
+
+ def __init__(self, id, name, size):
+ self.id = id
+ self.name = name
+ self.pending = set()
+ self.size = int(size)
+ self.releases = set()
+
+ def add_releases(self, ids, callback):
+ ids = ids - self.pending
+ if ids:
+ self.pending.update(ids)
+ self.tagger.xmlws.put_to_collection(self.id, list(ids),
+ partial(self._add_finished, ids, callback))
+
+ def remove_releases(self, ids, callback):
+ ids = ids - self.pending
+ if ids:
+ self.pending.update(ids)
+ self.tagger.xmlws.delete_from_collection(self.id, list(ids),
+ partial(self._remove_finished, ids, callback))
+
+ def _add_finished(self, ids, callback, document, reply, error):
+ self.pending.difference_update(ids)
+ if not error:
+ count = len(ids)
+ self.releases.update(ids)
+ self.size += count
+ callback()
+
+ self.tagger.window.set_statusbar_message(
+ ungettext('Added %i release to collection "%s"',
+ 'Added %i releases to collection "%s"', count) % (count, self.name))
+
+ def _remove_finished(self, ids, callback, document, reply, error):
+ self.pending.difference_update(ids)
+ if not error:
+ count = len(ids)
+ self.releases.difference_update(ids)
+ self.size -= count
+ callback()
+
+ self.tagger.window.set_statusbar_message(
+ ungettext('Removed %i release from collection "%s"',
+ 'Removed %i releases from collection "%s"', count) % (count, self.name))
+
+
+def load_user_collections(callback=None):
+ tagger = QtCore.QObject.tagger
+
+ def request_finished(document, reply, error):
+ if error:
+ tagger.window.set_statusbar_message(_("Error loading collections: %s"), unicode(reply.errorString()))
+ return
+ collection_list = document.metadata[0].collection_list[0]
+ if "collection" in collection_list.children:
+ new_collections = set()
+
+ for node in collection_list.collection:
+ new_collections.add(node.id)
+ collection = user_collections.get(node.id)
+ if collection is None:
+ user_collections[node.id] = Collection(node.id, node.name[0].text, node.release_list[0].count)
+ else:
+ collection.name = node.name[0].text
+ collection.size = int(node.release_list[0].count)
+
+ for id in set(user_collections.iterkeys()) - new_collections:
+ del user_collections[id]
+
+ if callback:
+ callback()
+
+ setting = QtCore.QObject.config.setting
+ if setting["username"] and setting["password"]:
+ tagger.xmlws.get_collection_list(partial(request_finished))
diff --git a/picard/const.py b/picard/const.py
index c04eaceb5..f43802478 100644
--- a/picard/const.py
+++ b/picard/const.py
@@ -23,6 +23,7 @@ __builtin__.__dict__['N_'] = lambda a: a
# AcoustID client API key
ACOUSTID_KEY = '0zClDiGo'
+ACOUSTID_HOST = 'api.acoustid.org'
FPCALC_NAMES = ['fpcalc', 'pyfpcalc']
# Various Artists MBID
diff --git a/picard/coverart.py b/picard/coverart.py
index 4b534d85a..6ca896650 100644
--- a/picard/coverart.py
+++ b/picard/coverart.py
@@ -126,16 +126,16 @@ def _caa_json_downloaded(album, metadata, release, try_list, data, http, error):
caa_types = QObject.config.setting["caa_image_types"].split()
caa_types = map(unicode.lower, caa_types)
for image in caa_data["images"]:
+ if QObject.config.setting["caa_approved_only"] and not image["approved"]:
+ continue
+ if not image["types"] and 'unknown' in caa_types:
+ _caa_append_image_to_trylist(try_list, image)
imagetypes = map(unicode.lower, image["types"])
for imagetype in imagetypes:
if imagetype == "front":
caa_front_found = True
if imagetype in caa_types:
- if QObject.config.setting["caa_approved_only"]:
- if image["approved"]:
- _caa_append_image_to_trylist(try_list, image)
- else:
- _caa_append_image_to_trylist(try_list, image)
+ _caa_append_image_to_trylist(try_list, image)
break
if error or not caa_front_found:
@@ -166,7 +166,40 @@ def coverart(album, metadata, release, try_list=None):
# try_list will be None for the first call
if try_list is None:
try_list = []
- if QObject.config.setting['ca_provider_use_caa']:
+
+ # MB web service indicates if CAA has artwork
+ # http://tickets.musicbrainz.org/browse/MBS-4536
+ has_caa_artwork = False
+ caa_types = map(unicode.lower,
+ QObject.config.setting["caa_image_types"].split())
+
+ if 'cover_art_archive' in release.children:
+ caa_node = release.children['cover_art_archive'][0]
+ has_caa_artwork = bool(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)
+
+ 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)
+
+ if QObject.config.setting['ca_provider_use_caa'] and has_caa_artwork\
+ and len(caa_types) > 0:
+ QObject.log.debug("There are suitable images in the cover art archive for %s"
+ % release.id)
album._requests += 1
album.tagger.xmlws.download(
"coverartarchive.org", 80, "/release/%s/" %
@@ -174,6 +207,8 @@ def coverart(album, metadata, release, try_list=None):
partial(_caa_json_downloaded, album, metadata, release, try_list),
priority=True, important=True)
else:
+ QObject.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)
@@ -248,7 +283,6 @@ def _process_asin_relation(try_list, relation):
else:
serverInfo = AMAZON_SERVER['amazon.com']
host = serverInfo['server']
- print "HOST", host
path_l = AMAZON_IMAGE_PATH % (asin, serverInfo['id'], 'L')
path_m = AMAZON_IMAGE_PATH % (asin, serverInfo['id'], 'M')
_try_list_append_image_url(try_list, QUrl("http://%s:%s" % (host, path_l)))
diff --git a/picard/coverartarchive.py b/picard/coverartarchive.py
new file mode 100644
index 000000000..9e574177d
--- /dev/null
+++ b/picard/coverartarchive.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+#
+# Picard, the next-generation MusicBrainz tagger
+# Copyright (C) 2013 Laurent Monin
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# 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.
+
+# list of types from http://musicbrainz.org/doc/Cover_Art/Types
+# order of declaration is preserved in selection box
+CAA_TYPES = [
+ {'name': "front", 'title': N_("Front")},
+ {'name': "back", 'title': N_("Back")},
+ {'name': "booklet", 'title': N_("Booklet")},
+ {'name': "medium", 'title': N_("Medium")},
+ {'name': "tray", 'title': N_("Tray")},
+ {'name': "obi", 'title': N_("Obi")},
+ {'name': "spine", 'title': N_("Spine")},
+ {'name': "track", 'title': N_("Track")},
+ {'name': "sticker", 'title': N_("Sticker")},
+ {'name': "other", 'title': N_("Other")},
+ {'name': "unknown", 'title': N_("Unknown")}, # pseudo type, used for the no type case
+]
+
+CAA_TYPES_SEPARATOR = ' ' #separator to use when joining/splitting list of types
diff --git a/picard/file.py b/picard/file.py
index a9e23ae94..ae5c52b9d 100644
--- a/picard/file.py
+++ b/picard/file.py
@@ -127,18 +127,19 @@ class File(QtCore.QObject, Item):
def copy_metadata(self, metadata):
acoustid = self.metadata["acoustid_id"]
- preserve = self.config.setting["preserved_tags"].strip()
- if preserve:
- saved_metadata = {}
- for tag in re.split(r"\s+", preserve):
- values = self.orig_metadata.getall(tag)
- if values:
- saved_metadata[tag] = values
- self.metadata.copy(metadata)
- for tag, values in saved_metadata.iteritems():
- self.metadata.set(tag, values)
- else:
- self.metadata.copy(metadata)
+ preserve = self.config.setting["preserved_tags"].strip() + " ".join([
+ "~bitrate", "~bits_per_sample", "~format", "~channels",
+ "~filename", "~dirname", "~extension"])
+ saved_metadata = {}
+
+ for tag in re.split(r"\s+", preserve):
+ values = self.orig_metadata.getall(tag)
+ if values:
+ saved_metadata[tag] = values
+ self.metadata.copy(metadata)
+ for tag, values in saved_metadata.iteritems():
+ self.metadata.set(tag, values)
+
self.metadata["acoustid_id"] = acoustid
def has_error(self):
@@ -215,8 +216,8 @@ class File(QtCore.QObject, Item):
self.base_filename = os.path.basename(new_filename)
length = self.orig_metadata.length
temp_info = {}
- for info in ('~#bitrate', '~#sample_rate', '~#channels',
- '~#bits_per_sample', '~format'):
+ for info in ('~bitrate', '~sample_rate', '~channels',
+ '~bits_per_sample', '~format'):
temp_info[info] = self.orig_metadata[info]
if self.config.setting["clear_existing_tags"]:
self.orig_metadata.copy(self.metadata)
@@ -248,13 +249,13 @@ class File(QtCore.QObject, Item):
metadata[name] = sanitize_filename(metadata[name])
format = format.replace("\t", "").replace("\n", "")
filename = ScriptParser().eval(format, metadata, self)
- # replace incompatible characters
- if settings["windows_compatible_filenames"] or sys.platform == "win32":
- filename = replace_win32_incompat(filename)
if settings["ascii_filenames"]:
if isinstance(filename, unicode):
filename = unaccent(filename)
filename = replace_non_ascii(filename)
+ # replace incompatible characters
+ if settings["windows_compatible_filenames"] or sys.platform == "win32":
+ filename = replace_win32_incompat(filename)
# remove null characters
filename = filename.replace("\x00", "")
return filename
@@ -462,13 +463,13 @@ class File(QtCore.QObject, Item):
if hasattr(file.info, 'length'):
metadata.length = int(file.info.length * 1000)
if hasattr(file.info, 'bitrate') and file.info.bitrate:
- metadata['~#bitrate'] = file.info.bitrate / 1000.0
+ metadata['~bitrate'] = file.info.bitrate / 1000.0
if hasattr(file.info, 'sample_rate') and file.info.sample_rate:
- metadata['~#sample_rate'] = file.info.sample_rate
+ metadata['~sample_rate'] = file.info.sample_rate
if hasattr(file.info, 'channels') and file.info.channels:
- metadata['~#channels'] = file.info.channels
+ metadata['~channels'] = file.info.channels
if hasattr(file.info, 'bits_per_sample') and file.info.bits_per_sample:
- metadata['~#bits_per_sample'] = file.info.bits_per_sample
+ metadata['~bits_per_sample'] = file.info.bits_per_sample
metadata['~format'] = self.__class__.__name__.replace('File', '')
self._add_path_to_metadata(metadata)
@@ -549,7 +550,9 @@ class File(QtCore.QObject, Item):
self.tagger.move_file_to_nat(self, track.id, node=track)
def lookup_metadata(self):
- """ Try to identify the file using the existing metadata. """
+ """Try to identify the file using the existing metadata."""
+ if self.lookup_task:
+ return
self.tagger.window.set_statusbar_message(N_("Looking up the metadata for file %s..."), self.filename)
self.clear_lookup_task()
metadata = self.metadata
diff --git a/picard/formats/wav.py b/picard/formats/wav.py
index 0c5062c0c..01f7ad544 100644
--- a/picard/formats/wav.py
+++ b/picard/formats/wav.py
@@ -30,9 +30,9 @@ class WAVFile(File):
self.log.debug("Loading file %r", filename)
f = wave.open(encode_filename(filename), "rb")
metadata = Metadata()
- metadata['~#channels'] = f.getnchannels()
- metadata['~#bits_per_sample'] = f.getsampwidth() * 8
- metadata['~#sample_rate'] = f.getframerate()
+ metadata['~channels'] = f.getnchannels()
+ metadata['~bits_per_sample'] = f.getsampwidth() * 8
+ metadata['~sample_rate'] = f.getframerate()
metadata.length = 1000 * f.getnframes() / f.getframerate()
metadata['~format'] = 'Microsoft WAVE'
self._add_path_to_metadata(metadata)
diff --git a/picard/mbxml.py b/picard/mbxml.py
index 74802a712..d7b96ae2b 100644
--- a/picard/mbxml.py
+++ b/picard/mbxml.py
@@ -312,7 +312,11 @@ def release_group_to_metadata(node, m, config, release_group=None):
for name, nodes in node.children.iteritems():
if not nodes:
continue
- if name == 'first_release_date':
+ if name == 'title':
+ m['~releasegroup'] = nodes[0].text
+ elif name == 'disambiguation':
+ m['~releasegroupcomment'] = nodes[0].text
+ elif name == 'first_release_date':
m['originaldate'] = nodes[0].text
elif name == 'tag_list':
add_folksonomy_tags(nodes[0], release_group)
diff --git a/picard/tagger.py b/picard/tagger.py
index d49ac3254..3fe2e7932 100644
--- a/picard/tagger.py
+++ b/picard/tagger.py
@@ -62,6 +62,7 @@ from picard.file import File
from picard.formats import open as open_file
from picard.track import Track, NonAlbumTrack
from picard.releasegroup import ReleaseGroup
+from picard.collection import load_user_collections
from picard.ui.mainwindow import MainWindow
from picard.ui.itemviews import BaseTreeView
from picard.plugin import PluginManager
@@ -151,6 +152,8 @@ class Tagger(QtGui.QApplication):
self.xmlws = XmlWebService()
+ load_user_collections()
+
# Initialize fingerprinting
self._acoustid = acoustid.AcoustIDClient()
self._acoustid.init()
@@ -557,7 +560,7 @@ class Tagger(QtGui.QApplication):
def autotag(self, objects):
for obj in objects:
- if isinstance(obj, (File, Cluster)) and not obj.lookup_task:
+ if obj.can_autotag():
obj.lookup_metadata()
# =======================================================================
diff --git a/picard/ui/collectionmenu.py b/picard/ui/collectionmenu.py
new file mode 100644
index 000000000..19ac3a992
--- /dev/null
+++ b/picard/ui/collectionmenu.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+#
+# Picard, the next-generation MusicBrainz tagger
+# Copyright (C) 2013 Michael Wiencek
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# 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.
+
+from PyQt4 import QtCore, QtGui
+from picard.collection import user_collections, load_user_collections
+
+
+class CollectionMenu(QtGui.QMenu):
+
+ def __init__(self, albums, *args):
+ QtGui.QMenu.__init__(self, *args)
+ self.ids = set(a.id for a in albums)
+ self.actions_by_id = {}
+ self.separator = self.addSeparator()
+ self.refresh_action = self.addAction(_("Refresh List"))
+ self.update_collections()
+
+ def update_collections(self):
+ for id, collection in user_collections.iteritems():
+ action = self.actions_by_id.get(collection.id)
+ if action:
+ action.defaultWidget().updateText()
+ else:
+ action = QtGui.QWidgetAction(self)
+ action.setDefaultWidget(CollectionCheckBox(self, collection))
+ self.insertAction(self.separator, action)
+ self.actions_by_id[collection.id] = action
+
+ for id, action in self.actions_by_id.items():
+ if id not in user_collections:
+ self.removeAction(action)
+ del self.actions_by_id[id]
+
+ def refresh_list(self):
+ self.refresh_action.setEnabled(False)
+ load_user_collections(self.update_collections)
+
+ def mouseReleaseEvent(self, event):
+ # Not using self.refresh_action.triggered because it closes the menu
+ if self.actionAt(event.pos()) == self.refresh_action and self.refresh_action.isEnabled():
+ self.refresh_list()
+
+
+class CollectionCheckBox(QtGui.QCheckBox):
+
+ def __init__(self, menu, collection):
+ self.menu = menu
+ self.collection = collection
+ QtGui.QCheckBox.__init__(self, self.label())
+
+ releases = collection.releases & menu.ids
+ if len(releases) == len(menu.ids):
+ self.setCheckState(QtCore.Qt.Checked)
+ elif not releases:
+ self.setCheckState(QtCore.Qt.Unchecked)
+ else:
+ self.setCheckState(QtCore.Qt.PartiallyChecked)
+
+ def nextCheckState(self):
+ ids = self.menu.ids
+ if ids & self.collection.pending:
+ return
+ diff = ids - self.collection.releases
+ if diff:
+ self.collection.add_releases(diff, self.updateText)
+ self.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.collection.remove_releases(ids & self.collection.releases, self.updateText)
+ self.setCheckState(QtCore.Qt.Unchecked)
+
+ def updateText(self):
+ self.setText(self.label())
+
+ def label(self):
+ c = self.collection
+ return ungettext("%s (%i release)", "%s (%i releases)", c.size) % (c.name, c.size)
diff --git a/picard/ui/coverartbox.py b/picard/ui/coverartbox.py
index 8f21a7f10..f3f5ac9d9 100644
--- a/picard/ui/coverartbox.py
+++ b/picard/ui/coverartbox.py
@@ -101,10 +101,15 @@ class CoverArtBox(QtGui.QGroupBox):
pixmap = QtGui.QPixmap()
pixmap.loadFromData(self.data["data"])
if not pixmap.isNull():
+ offx, offy, w, h = (1, 1, 121, 121)
cover = QtGui.QPixmap(self.shadow)
- pixmap = pixmap.scaled(121, 121, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
+ pixmap = pixmap.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
painter = QtGui.QPainter(cover)
- painter.drawPixmap(1, 1, pixmap)
+ bgcolor = QtGui.QColor.fromRgb(0, 0, 0, 128)
+ painter.fillRect(QtCore.QRectF(offx, offy, w, h), bgcolor)
+ x = offx + (w - pixmap.width()) / 2
+ y = offy + (h - pixmap.height()) / 2
+ painter.drawPixmap(x, y, pixmap)
painter.end()
self.coverArt.setPixmap(cover)
@@ -151,7 +156,7 @@ class CoverArtBox(QtGui.QGroupBox):
path = encode_filename(unicode(url.toLocalFile()))
if os.path.exists(path):
f = open(path, 'rb')
- mime = 'image/png' if '.png' in path.lower() else 'image/jpeg'
+ mime = 'image/png' if path.lower().endswith('.png') else 'image/jpeg'
data = f.read()
f.close()
self.load_remote_image(mime, data)
diff --git a/picard/ui/infodialog.py b/picard/ui/infodialog.py
index 7afbe8327..7289911a4 100644
--- a/picard/ui/infodialog.py
+++ b/picard/ui/infodialog.py
@@ -25,18 +25,50 @@ from picard.ui.ui_infodialog import Ui_InfoDialog
class InfoDialog(QtGui.QDialog):
- def __init__(self, file, parent=None):
+ def __init__(self, obj, parent=None):
QtGui.QDialog.__init__(self, parent)
- self.file = file
+ self.obj = obj
self.ui = Ui_InfoDialog()
self.ui.setupUi(self)
self.ui.buttonBox.accepted.connect(self.accept)
self.ui.buttonBox.rejected.connect(self.reject)
- self.setWindowTitle(_("Info") + " - " + file.base_filename)
- self.load_info()
+ self.setWindowTitle(_("Info"))
+ self._display_tabs()
- def load_info(self):
- file = self.file
+ def _display_tabs(self):
+ self._display_info_tab()
+ self._display_artwork_tab()
+
+ def _display_artwork_tab(self):
+ tab = self.ui.artwork_tab
+ images = self.obj.metadata.images
+ if not images:
+ self.tab_hide(tab)
+ return
+
+ for image in images:
+ data = image["data"]
+ item = QtGui.QListWidgetItem()
+ pixmap = QtGui.QPixmap()
+ pixmap.loadFromData(data)
+ icon = QtGui.QIcon(pixmap)
+ item.setIcon(icon)
+ self.ui.artwork_list.addItem(item)
+
+ def tab_hide(self, widget):
+ tab = self.ui.tabWidget
+ index = tab.indexOf(widget)
+ tab.removeTab(index)
+
+
+class FileInfoDialog(InfoDialog):
+
+ def __init__(self, file, parent=None):
+ InfoDialog.__init__(self, file, parent)
+ self.setWindowTitle(_("Info") + " - " + file.base_filename)
+
+ def _display_info_tab(self):
+ file = self.obj
info = []
info.append((_('Filename:'), file.filename))
if '~format' in file.orig_metadata:
@@ -54,14 +86,14 @@ class InfoDialog(QtGui.QDialog):
pass
if file.orig_metadata.length:
info.append((_('Length:'), format_time(file.orig_metadata.length)))
- if '~#bitrate' in file.orig_metadata:
- info.append((_('Bitrate:'), '%s kbps' % file.orig_metadata['~#bitrate']))
- if '~#sample_rate' in file.orig_metadata:
- info.append((_('Sample rate:'), '%s Hz' % file.orig_metadata['~#sample_rate']))
- if '~#bits_per_sample' in file.orig_metadata:
- info.append((_('Bits per sample:'), str(file.orig_metadata['~#bits_per_sample'])))
- if '~#channels' in file.orig_metadata:
- ch = file.orig_metadata['~#channels']
+ if '~bitrate' in file.orig_metadata:
+ info.append((_('Bitrate:'), '%s kbps' % file.orig_metadata['~bitrate']))
+ if '~sample_rate' in file.orig_metadata:
+ info.append((_('Sample rate:'), '%s Hz' % file.orig_metadata['~sample_rate']))
+ if '~bits_per_sample' in file.orig_metadata:
+ info.append((_('Bits per sample:'), str(file.orig_metadata['~bits_per_sample'])))
+ if '~channels' in file.orig_metadata:
+ ch = file.orig_metadata['~channels']
if ch == 1: ch = _('Mono')
elif ch == 2: ch = _('Stereo')
else: ch = str(ch)
@@ -69,11 +101,13 @@ class InfoDialog(QtGui.QDialog):
text = '
'.join(map(lambda i: '%s
%s' % i, info))
self.ui.info.setText(text)
- for image in file.metadata.images:
- data = image["data"]
- item = QtGui.QListWidgetItem()
- pixmap = QtGui.QPixmap()
- pixmap.loadFromData(data)
- icon = QtGui.QIcon(pixmap)
- item.setIcon(icon)
- self.ui.artwork_list.addItem(item)
+
+class AlbumInfoDialog(InfoDialog):
+
+ def __init__(self, album, parent=None):
+ InfoDialog.__init__(self, album, parent)
+ self.setWindowTitle(_("Album Info"))
+
+ def _display_info_tab(self):
+ tab = self.ui.info_tab
+ self.tab_hide(tab)
diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py
index 263c570fe..057fcbe1b 100644
--- a/picard/ui/itemviews.py
+++ b/picard/ui/itemviews.py
@@ -28,6 +28,7 @@ from picard.util import encode_filename, icontheme, partial
from picard.config import Option, TextOption
from picard.plugin import ExtensionPoint
from picard.ui.ratingwidget import RatingWidget
+from picard.ui.collectionmenu import CollectionMenu
class BaseAction(QtGui.QAction):
@@ -271,6 +272,8 @@ class BaseTreeView(QtGui.QTreeWidget):
menu.addAction(self.window.analyze_action)
plugin_actions = list(_file_actions)
elif isinstance(obj, Album):
+ if can_view_info:
+ menu.addAction(self.window.view_info_action)
menu.addAction(self.window.browser_lookup_action)
menu.addSeparator()
menu.addAction(self.window.refresh_action)
@@ -279,24 +282,31 @@ class BaseTreeView(QtGui.QTreeWidget):
menu.addAction(self.window.save_action)
menu.addAction(self.window.remove_action)
+ bottom_separator = False
+
if isinstance(obj, Album) and not isinstance(obj, NatAlbum) and obj.loaded:
releases_menu = QtGui.QMenu(_("&Other versions"), menu)
menu.addSeparator()
menu.addMenu(releases_menu)
loading = releases_menu.addAction(_('Loading...'))
loading.setEnabled(False)
+ bottom_separator = True
- def _add_other_versions():
- releases_menu.removeAction(loading)
- for version in obj.release_group.versions:
- action = releases_menu.addAction(version["name"])
- action.setCheckable(True)
- if obj.id == version["id"]:
- action.setChecked(True)
- action.triggered.connect(partial(obj.switch_release_version, version["id"]))
+ if len(self.selectedIndexes()) == len(MainPanel.columns):
+ def _add_other_versions():
+ releases_menu.removeAction(loading)
+ for version in obj.release_group.versions:
+ action = releases_menu.addAction(version["name"])
+ action.setCheckable(True)
+ if obj.id == version["id"]:
+ action.setChecked(True)
+ action.triggered.connect(partial(obj.switch_release_version, version["id"]))
- _add_other_versions() if obj.release_group.loaded else \
- obj.release_group.load_versions(_add_other_versions)
+ _add_other_versions() if obj.release_group.loaded else \
+ obj.release_group.load_versions(_add_other_versions)
+ releases_menu.setEnabled(True)
+ else:
+ releases_menu.setEnabled(False)
if self.config.setting["enable_ratings"] and \
len(self.window.selected_objects) == 1 and isinstance(obj, Track):
@@ -306,6 +316,12 @@ class BaseTreeView(QtGui.QTreeWidget):
menu.addAction(action)
menu.addSeparator()
+ selected_albums = [a for a in self.window.selected_objects if type(a) == Album]
+ if selected_albums:
+ if not bottom_separator:
+ menu.addSeparator()
+ menu.addMenu(CollectionMenu(selected_albums, _("Collections"), menu))
+
if plugin_actions:
plugin_menu = QtGui.QMenu(_("&Plugins"), menu)
plugin_menu.setIcon(self.panel.icon_plugins)
@@ -324,6 +340,7 @@ class BaseTreeView(QtGui.QTreeWidget):
action_menu.addAction(action)
if isinstance(obj, Cluster) or isinstance(obj, ClusterList) or isinstance(obj, Album):
+ menu.addSeparator()
menu.addAction(self.expand_all_action)
menu.addAction(self.collapse_all_action)
@@ -453,7 +470,9 @@ class BaseTreeView(QtGui.QTreeWidget):
def activate_item(self, index):
obj = self.itemFromIndex(index).obj
- if obj.can_view_info():
+ # Double-clicking albums should expand them. The album info can be
+ # viewed by using the toolbar button.
+ if not isinstance(obj, Album) and obj.can_view_info():
self.window.view_info()
def add_cluster(self, cluster, parent_item=None):
diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py
index 5f457122c..64b8e889f 100644
--- a/picard/ui/mainwindow.py
+++ b/picard/ui/mainwindow.py
@@ -24,6 +24,7 @@ import os.path
from picard.file import File
from picard.track import Track
+from picard.album import Album
from picard.config import Option, BoolOption, TextOption
from picard.formats import supported_formats
from picard.ui.coverartbox import CoverArtBox
@@ -32,7 +33,7 @@ from picard.ui.metadatabox import MetadataBox
from picard.ui.filebrowser import FileBrowser
from picard.ui.tagsfromfilenames import TagsFromFileNamesDialog
from picard.ui.options.dialog import OptionsDialog
-from picard.ui.infodialog import InfoDialog
+from picard.ui.infodialog import FileInfoDialog, AlbumInfoDialog
from picard.ui.passworddialog import PasswordDialog
from picard.util import icontheme, webbrowser2, find_existing_path
from picard.util.cdrom import get_cdrom_drives
@@ -676,8 +677,12 @@ been merged with that of single artist albums."""),
return ret == QtGui.QMessageBox.Yes
def view_info(self):
- file = self.tagger.get_files_from_objects(self.selected_objects)[0]
- dialog = InfoDialog(file, self)
+ if isinstance(self.selected_objects[0], Album):
+ album = self.selected_objects[0]
+ dialog = AlbumInfoDialog(album, self)
+ else:
+ file = self.tagger.get_files_from_objects(self.selected_objects)[0]
+ dialog = FileInfoDialog(file, self)
dialog.exec_()
def cluster(self):
diff --git a/picard/ui/options/cover.py b/picard/ui/options/cover.py
index 036b9a3d7..bc4a07953 100644
--- a/picard/ui/options/cover.py
+++ b/picard/ui/options/cover.py
@@ -21,6 +21,45 @@ from PyQt4 import QtCore, QtGui
from picard.config import BoolOption, IntOption, TextOption
from picard.ui.options import OptionsPage, register_options_page
from picard.ui.ui_options_cover import Ui_CoverOptionsPage
+from picard.coverartarchive import CAA_TYPES, CAA_TYPES_SEPARATOR
+
+
+class CAATypesSelector(object):
+ def __init__(self, widget, enabled_types=''):
+ self.widget = widget
+ self._enabled_types = enabled_types.split(CAA_TYPES_SEPARATOR)
+ self._items = {}
+ self._populate()
+
+ def _populate(self):
+ for caa_type in CAA_TYPES:
+ enabled = caa_type["name"] in self._enabled_types
+ self._add_item(caa_type, enabled=enabled)
+
+ def _add_item(self, typ, enabled=False):
+ item = QtGui.QListWidgetItem(self.widget)
+ item.setText(typ['title'])
+ tooltip = u"CAA: %(name)s" % typ
+ item.setToolTip(tooltip)
+ if enabled:
+ state = QtCore.Qt.Checked
+ else:
+ state = QtCore.Qt.Unchecked
+ item.setCheckState(state)
+ self._items[item] = typ
+
+ def get_selected_types(self):
+ types = []
+ for item, typ in self._items.iteritems():
+ if item.checkState() == QtCore.Qt.Checked:
+ types.append(typ['name'])
+ return types
+
+ def get_selected_types_as_string(self):
+ return CAA_TYPES_SEPARATOR.join(self.get_selected_types())
+
+ def __str__(self):
+ return self.get_selected_types_as_string()
class CoverOptionsPage(OptionsPage):
@@ -67,7 +106,10 @@ class CoverOptionsPage(OptionsPage):
self.ui.gb_caa.setEnabled(self.config.setting["ca_provider_use_caa"])
self.ui.cb_image_size.setCurrentIndex(self.config.setting["caa_image_size"])
- self.ui.le_image_types.setText(self.config.setting["caa_image_types"])
+ widget = self.ui.caa_types_selector_1
+ self._selector = CAATypesSelector(widget, self.config.setting["caa_image_types"])
+ self.config.setting["caa_image_types"] = \
+ self._selector.get_selected_types_as_string()
self.ui.cb_approved_only.setChecked(self.config.setting["caa_approved_only"])
self.ui.cb_type_as_filename.setChecked(self.config.setting["caa_image_type_as_filename"])
self.connect(self.ui.caprovider_caa, QtCore.SIGNAL("toggled(bool)"),
@@ -88,7 +130,8 @@ class CoverOptionsPage(OptionsPage):
self.ui.caprovider_whitelist.isChecked()
self.config.setting["caa_image_size"] =\
self.ui.cb_image_size.currentIndex()
- self.config.setting["caa_image_types"] = self.ui.le_image_types.text()
+ self.config.setting["caa_image_types"] = \
+ self._selector.get_selected_types_as_string()
self.config.setting["caa_approved_only"] =\
self.ui.cb_approved_only.isChecked()
self.config.setting["caa_image_type_as_filename"] = \
diff --git a/picard/ui/ui_options_cover.py b/picard/ui/ui_options_cover.py
index 677a687a7..522f106bd 100644
--- a/picard/ui/ui_options_cover.py
+++ b/picard/ui/ui_options_cover.py
@@ -2,8 +2,8 @@
# Form implementation generated from reading ui file 'ui/options_cover.ui'
#
-# Created: Tue Nov 20 18:25:21 2012
-# by: PyQt4 UI code generator 4.9.5
+# Created: Tue Jan 22 12:56:46 2013
+# by: PyQt4 UI code generator 4.9.6
#
# WARNING! All changes made in this file will be lost!
@@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
- _fromUtf8 = lambda s: s
+ def _fromUtf8(s):
+ return s
+
+try:
+ _encoding = QtGui.QApplication.UnicodeUTF8
+ def _translate(context, text, disambig):
+ return QtGui.QApplication.translate(context, text, disambig, _encoding)
+except AttributeError:
+ def _translate(context, text, disambig):
+ return QtGui.QApplication.translate(context, text, disambig)
class Ui_CoverOptionsPage(object):
def setupUi(self, CoverOptionsPage):
@@ -89,12 +98,22 @@ class Ui_CoverOptionsPage(object):
self.label_2 = QtGui.QLabel(self.gb_caa)
self.label_2.setObjectName(_fromUtf8("label_2"))
self.verticalLayout_3.addWidget(self.label_2)
- self.le_image_types = QtGui.QLineEdit(self.gb_caa)
- self.le_image_types.setObjectName(_fromUtf8("le_image_types"))
- self.verticalLayout_3.addWidget(self.le_image_types)
- self.caa_types_help = QtGui.QLabel(self.gb_caa)
- self.caa_types_help.setObjectName(_fromUtf8("caa_types_help"))
- self.verticalLayout_3.addWidget(self.caa_types_help)
+ self.caa_types_selector_1 = QtGui.QListWidget(self.gb_caa)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.caa_types_selector_1.sizePolicy().hasHeightForWidth())
+ self.caa_types_selector_1.setSizePolicy(sizePolicy)
+ self.caa_types_selector_1.setMaximumSize(QtCore.QSize(16777215, 80))
+ self.caa_types_selector_1.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+ self.caa_types_selector_1.setTabKeyNavigation(True)
+ self.caa_types_selector_1.setProperty("showDropIndicator", False)
+ self.caa_types_selector_1.setAlternatingRowColors(True)
+ self.caa_types_selector_1.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
+ self.caa_types_selector_1.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+ self.caa_types_selector_1.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
+ self.caa_types_selector_1.setObjectName(_fromUtf8("caa_types_selector_1"))
+ self.verticalLayout_3.addWidget(self.caa_types_selector_1)
self.cb_approved_only = QtGui.QCheckBox(self.gb_caa)
self.cb_approved_only.setObjectName(_fromUtf8("cb_approved_only"))
self.verticalLayout_3.addWidget(self.cb_approved_only)
@@ -113,8 +132,18 @@ class Ui_CoverOptionsPage(object):
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_tags, self.cb_embed_front_only)
+ CoverOptionsPage.setTabOrder(self.cb_embed_front_only, self.save_images_to_files)
CoverOptionsPage.setTabOrder(self.save_images_to_files, self.cover_image_filename)
+ CoverOptionsPage.setTabOrder(self.cover_image_filename, self.save_images_overwrite)
+ CoverOptionsPage.setTabOrder(self.save_images_overwrite, self.caprovider_amazon)
+ CoverOptionsPage.setTabOrder(self.caprovider_amazon, self.caprovider_cdbaby)
+ CoverOptionsPage.setTabOrder(self.caprovider_cdbaby, self.caprovider_caa)
+ CoverOptionsPage.setTabOrder(self.caprovider_caa, self.caprovider_whitelist)
+ CoverOptionsPage.setTabOrder(self.caprovider_whitelist, self.cb_image_size)
+ CoverOptionsPage.setTabOrder(self.cb_image_size, self.caa_types_selector_1)
+ CoverOptionsPage.setTabOrder(self.caa_types_selector_1, self.cb_approved_only)
+ CoverOptionsPage.setTabOrder(self.cb_approved_only, self.cb_type_as_filename)
def retranslateUi(self, CoverOptionsPage):
self.rename_files.setTitle(_("Location"))
@@ -134,7 +163,6 @@ class Ui_CoverOptionsPage(object):
self.cb_image_size.setItemText(1, _("500 px"))
self.cb_image_size.setItemText(2, _("Full size"))
self.label_2.setText(_("Download only images of the following types:"))
- self.caa_types_help.setText(_("Types are separated by spaces, and are not case-sensitive."))
self.cb_approved_only.setText(_("Download only approved images"))
self.cb_type_as_filename.setText(_("Use the first image type as the filename. This will not change the filename of front images."))
diff --git a/picard/webservice.py b/picard/webservice.py
index 97a4d018d..362f5bd6d 100644
--- a/picard/webservice.py
+++ b/picard/webservice.py
@@ -26,16 +26,18 @@ Asynchronous XML web service.
import sys
import re
import time
+import os.path
from collections import deque, defaultdict
from PyQt4 import QtCore, QtNetwork, QtXml
+from PyQt4.QtGui import QDesktopServices
from PyQt4.QtCore import QUrl
from picard import version_string
from picard.util import partial
-from picard.const import ACOUSTID_KEY
+from picard.const import ACOUSTID_KEY, ACOUSTID_HOST
REQUEST_DELAY = defaultdict(lambda: 1000)
-REQUEST_DELAY[('api.acoustid.org', 80)] = 333
+REQUEST_DELAY[(ACOUSTID_HOST, 80)] = 333
REQUEST_DELAY[("coverartarchive.org", 80)] = 0
USER_AGENT_STRING = 'MusicBrainz%%20Picard-%s' % version_string
@@ -111,6 +113,7 @@ class XmlWebService(QtCore.QObject):
def __init__(self, parent=None):
QtCore.QObject.__init__(self, parent)
self.manager = QtNetwork.QNetworkAccessManager()
+ self.set_cache()
self.setup_proxy()
self.manager.finished.connect(self._process_reply)
self._last_request_times = {}
@@ -128,6 +131,16 @@ class XmlWebService(QtCore.QObject):
"DELETE": self.manager.deleteResource
}
+ def set_cache(self, cache_size_in_mb=100):
+ cache = QtNetwork.QNetworkDiskCache()
+ location = QDesktopServices.storageLocation(QDesktopServices.CacheLocation)
+ cache.setCacheDirectory(os.path.join(unicode(location), u'picard'))
+ cache.setMaximumCacheSize(cache_size_in_mb * 1024 * 1024)
+ self.manager.setCache(cache)
+ self.log.debug("NetworkDiskCache dir: %s", cache.cacheDirectory())
+ self.log.debug("NetworkDiskCache size: %s / %s", cache.cacheSize(),
+ cache.maximumCacheSize())
+
def setup_proxy(self):
self.proxy = QtNetwork.QNetworkProxy()
if self.config.setting["use_proxy"]:
@@ -138,13 +151,17 @@ class XmlWebService(QtCore.QObject):
self.proxy.setPassword(self.config.setting["proxy_password"])
self.manager.setProxy(self.proxy)
- def _start_request(self, method, host, port, path, data, handler, xml, mblogin=False):
+ def _start_request(self, method, host, port, path, data, handler, xml,
+ mblogin=False, cacheloadcontrol=None):
self.log.debug("%s http://%s:%d%s", method, host, port, path)
url = QUrl.fromEncoded("http://%s:%d%s" % (host, port, path))
if mblogin:
url.setUserName(self.config.setting["username"])
url.setPassword(self.config.setting["password"])
request = QtNetwork.QNetworkRequest(url)
+ if cacheloadcontrol is not None:
+ request.setAttribute(QtNetwork.QNetworkRequest.CacheLoadControlAttribute,
+ cacheloadcontrol)
request.setRawHeader("User-Agent", "MusicBrainz-Picard/%s" % version_string)
if data is not None:
if method == "POST" and host == self.config.setting["server_host"]:
@@ -176,10 +193,14 @@ class XmlWebService(QtCore.QObject):
return
error = int(reply.error())
redirect = reply.attribute(QtNetwork.QNetworkRequest.RedirectionTargetAttribute).toUrl()
- self.log.debug("Received reply for %s: HTTP %d (%s)",
+ fromCache = reply.attribute(QtNetwork.QNetworkRequest.SourceIsFromCacheAttribute).toBool()
+ cached = ' (CACHED)' if fromCache else ''
+ self.log.debug("Received reply for %s: HTTP %d (%s) %s",
reply.request().url().toString(),
reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute).toInt()[0],
- reply.attribute(QtNetwork.QNetworkRequest.HttpReasonPhraseAttribute).toString())
+ reply.attribute(QtNetwork.QNetworkRequest.HttpReasonPhraseAttribute).toString(),
+ cached
+ )
if handler is not None:
if error:
self.log.error("Network request error for %s: %s (QT code %d, HTTP code %d)",
@@ -210,7 +231,8 @@ class XmlWebService(QtCore.QObject):
redirect_port,
# retain path, query string and anchors from redirect URL
redirect.toString(QUrl.FormattingOption(QUrl.RemoveAuthority | QUrl.RemoveScheme)),
- handler, xml, priority=True, important=True)
+ handler, xml, priority=True, important=True,
+ cacheloadcontrol=request.attribute(QtNetwork.QNetworkRequest.CacheLoadControlAttribute))
elif xml:
xml_handler = XmlHandler()
xml_handler.init()
@@ -223,8 +245,10 @@ class XmlWebService(QtCore.QObject):
handler(str(reply.readAll()), reply, error)
reply.close()
- def get(self, host, port, path, handler, xml=True, priority=False, important=False, mblogin=False):
- func = partial(self._start_request, "GET", host, port, path, None, handler, xml, mblogin)
+ def get(self, host, port, path, handler, xml=True, priority=False,
+ important=False, mblogin=False, cacheloadcontrol=None):
+ func = partial(self._start_request, "GET", host, port, path, None,
+ handler, xml, mblogin, cacheloadcontrol=cacheloadcontrol)
return self.add_task(func, host, port, priority, important=important)
def post(self, host, port, path, data, handler, xml=True, priority=True, important=True, mblogin=True):
@@ -369,7 +393,7 @@ class XmlWebService(QtCore.QObject):
return '&'.join(filters)
def query_acoustid(self, handler, **args):
- host, port = 'api.acoustid.org', 80
+ host, port = ACOUSTID_HOST, 80
body = self._encode_acoustid_args(args)
return self.post(host, port, '/v2/lookup', body, handler, mblogin=False)
@@ -381,10 +405,38 @@ class XmlWebService(QtCore.QObject):
args['mbid.%d' % i] = str(submission.trackid)
if submission.puid:
args['puid.%d' % i] = str(submission.puid)
- host, port = 'api.acoustid.org', 80
+ host, port = ACOUSTID_HOST, 80
body = self._encode_acoustid_args(args)
return self.post(host, port, '/v2/submit', body, handler, mblogin=False)
- def download(self, host, port, path, handler, priority=False, important=False):
- return self.get(host, port, path, handler, xml=False, priority=priority, important=important)
+ def download(self, host, port, path, handler, priority=False,
+ important=False, cacheloadcontrol=None):
+ return self.get(host, port, path, handler, xml=False, priority=priority,
+ important=important, cacheloadcontrol=cacheloadcontrol)
+ def get_collection(self, id, handler, limit=100, offset=0):
+ host, port = self.config.setting['server_host'], self.config.setting['server_port']
+ path = "/ws/2/collection"
+ if id is not None:
+ inc = ["releases", "artist-credits", "media"]
+ path += "/%s/releases?inc=%s&limit=%d&offset=%d" % (id, "+".join(inc), limit, offset)
+ return self.get(host, port, path, handler, priority=True, important=True, mblogin=True)
+
+ def get_collection_list(self, handler):
+ return self.get_collection(None, handler)
+
+ def _collection_request(self, id, releases):
+ while releases:
+ ids = ";".join(releases if len(releases) <= 400 else releases[:400])
+ releases = releases[400:]
+ yield "/ws/2/collection/%s/releases/%s?client=%s" % (id, ids, USER_AGENT_STRING)
+
+ def put_to_collection(self, id, releases, handler):
+ host, port = self.config.setting['server_host'], self.config.setting['server_port']
+ for path in self._collection_request(id, releases):
+ self.put(host, port, path, "", handler)
+
+ def delete_from_collection(self, id, releases, handler):
+ host, port = self.config.setting['server_host'], self.config.setting['server_port']
+ for path in self._collection_request(id, releases):
+ self.delete(host, port, path, handler)
diff --git a/setup.py b/setup.py
index 3ff8c7aae..77b0aa9d3 100755
--- a/setup.py
+++ b/setup.py
@@ -250,9 +250,14 @@ class picard_build_ui(Command):
def run(self):
from PyQt4 import uic
- _translate_re = re.compile(
- r'QtGui\.QApplication.translate\(.*?, (.*?), None, '
- r'QtGui\.QApplication\.UnicodeUTF8\)')
+ _translate_re = (
+ re.compile(
+ r'QtGui\.QApplication.translate\(.*?, (.*?), None, '
+ r'QtGui\.QApplication\.UnicodeUTF8\)'),
+ re.compile(
+ r'\b_translate\(.*?, (.*?), None\)')
+ )
+
for uifile in glob.glob("ui/*.ui"):
pyfile = "ui_%s.py" % os.path.splitext(os.path.basename(uifile))[0]
pyfile = os.path.join("picard", "ui", pyfile)
@@ -260,7 +265,9 @@ class picard_build_ui(Command):
log.info("compiling %s -> %s", uifile, pyfile)
tmp = StringIO()
uic.compileUi(uifile, tmp)
- source = _translate_re.sub(r'_(\1)', tmp.getvalue())
+ source = tmp.getvalue()
+ for r in list(_translate_re):
+ source = r.sub(r'_(\1)', source)
f = open(pyfile, "w")
f.write(source)
f.close()
diff --git a/ui/options_cover.ui b/ui/options_cover.ui
index 04fb78dec..352de2df0 100644
--- a/ui/options_cover.ui
+++ b/ui/options_cover.ui
@@ -167,14 +167,41 @@
-
-
-
- -
-
-
- Types are separated by spaces, and are not case-sensitive.
+
+
+
+ 0
+ 0
+
-
+
+
+ 16777215
+ 80
+
+
+
+ Qt::ScrollBarAsNeeded
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ QAbstractItemView::NoSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
-
@@ -216,8 +243,18 @@
save_images_to_tags
+ cb_embed_front_only
save_images_to_files
cover_image_filename
+ save_images_overwrite
+ caprovider_amazon
+ caprovider_cdbaby
+ caprovider_caa
+ caprovider_whitelist
+ cb_image_size
+ caa_types_selector_1
+ cb_approved_only
+ cb_type_as_filename