Merge branch 'master' of github.com:musicbrainz/picard into mwiencek-master

This commit is contained in:
Michael Wiencek
2013-02-17 14:36:54 -06:00
22 changed files with 651 additions and 123 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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):

103
picard/collection.py Normal file
View File

@@ -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))

View File

@@ -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

View File

@@ -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)))

36
picard/coverartarchive.py Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()
# =======================================================================

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 = '<br/>'.join(map(lambda i: '<b>%s</b><br/>%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)

View File

@@ -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):

View File

@@ -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):

View File

@@ -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"] = \

View File

@@ -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."))

View File

@@ -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)

View File

@@ -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()

View File

@@ -167,14 +167,41 @@
</widget>
</item>
<item>
<widget class="QLineEdit" name="le_image_types"/>
</item>
<item>
<widget class="QLabel" name="caa_types_help">
<property name="text">
<string>Types are separated by spaces, and are not case-sensitive.</string>
<widget class="QListWidget" name="caa_types_selector_1">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>80</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="tabKeyNavigation">
<bool>true</bool>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cb_approved_only">
@@ -216,8 +243,18 @@
</widget>
<tabstops>
<tabstop>save_images_to_tags</tabstop>
<tabstop>cb_embed_front_only</tabstop>
<tabstop>save_images_to_files</tabstop>
<tabstop>cover_image_filename</tabstop>
<tabstop>save_images_overwrite</tabstop>
<tabstop>caprovider_amazon</tabstop>
<tabstop>caprovider_cdbaby</tabstop>
<tabstop>caprovider_caa</tabstop>
<tabstop>caprovider_whitelist</tabstop>
<tabstop>cb_image_size</tabstop>
<tabstop>caa_types_selector_1</tabstop>
<tabstop>cb_approved_only</tabstop>
<tabstop>cb_type_as_filename</tabstop>
</tabstops>
<resources/>
<connections>