From 1605adbd7b6b1512e3b406d459db851c3c6f006b Mon Sep 17 00:00:00 2001 From: Sophist Date: Wed, 22 May 2013 13:08:07 +0100 Subject: [PATCH 01/75] Handle Performer tags stored in ID3v2.3 Performer tags are stored as part of the IPLS tag in ID3v2.3 and this is put into the TIPL tag when read by mutagen. This fix adds unknown TIPL roles as Performer tags. --- picard/formats/id3.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 165b730e1..dfc508c9e 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -184,9 +184,13 @@ class ID3File(File): if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": + # If file is ID3v2.3, TIPL tag could contain TMCL + # so we will test for TMCL values and add to TIPL if not TMCL for role, name in frame.people: if role in self.__tipl_roles and name: metadata.add(self.__tipl_roles[role], name) + else: + metadata.add('performer:%s' % role, name) elif frameid == 'TXXX': name = frame.desc if name in self.__translate_freetext: From 738fbd94d5c470bb0de35ae5d0a7e7a66f97bfda Mon Sep 17 00:00:00 2001 From: Sophist Date: Mon, 3 Jun 2013 08:17:53 +0100 Subject: [PATCH 02/75] Remove sorting for displayed multi-value tags and provide a sort tags plugin Picard stores tags unsorted but displays them sorted and this is inconsistent. This commit removes display sorting of multi-value tags from Picard improves consistency by displaying what will be / is actually saved. This commit also provides a plug-in to allow user to sort tags both displayed and stored in the file. --- contrib/plugins/sort_multivalue_tags.py | 34 +++++++++++++++++++++++++ picard/ui/metadatabox.py | 2 -- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 contrib/plugins/sort_multivalue_tags.py diff --git a/contrib/plugins/sort_multivalue_tags.py b/contrib/plugins/sort_multivalue_tags.py new file mode 100644 index 000000000..e5d052d0c --- /dev/null +++ b/contrib/plugins/sort_multivalue_tags.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# This is the Sort Multivalue Tags plugin for MusicBrainz Picard. +# Copyright (C) 2013 Sophist +# +# 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. + +PLUGIN_NAME = u"Sort Multi-Value Tags" +PLUGIN_AUTHOR = u"Sophist" +PLUGIN_DESCRIPTION = u'Sort Multi-Value Tags e.g. Release Type, Lyrics alphabetically.' +PLUGIN_VERSION = "0.1" +PLUGIN_API_VERSIONS = ["0.15"] + +from picard.metadata import register_track_metadata_processor + +# Define and register the Track Metadata function +def sort_multivalue_tags(tagger, metadata, track, release): + + for tag in filter(lambda x: len(metadata[x]) > 1, metadata.keys()): + data = metadata.getall(tag) + if len(data) > 1: + sorted_data = sorted(data) + if data != sorted_data: + metadata.set(tag,sorted_data) + +register_track_metadata_processor(sort_multivalue_tags) diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index 5433bc105..1a2f38624 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -99,11 +99,9 @@ class TagDiff: def add(self, tag, orig_values, new_values, removable): if orig_values: - orig_values = sorted(orig_values) self.orig.add(tag, orig_values) if new_values: - new_values = sorted(new_values) self.new.add(tag, new_values) if orig_values and not new_values: From 2ad20b68a99268e3367d7617ce9c16f39637b58c Mon Sep 17 00:00:00 2001 From: Sophist Date: Mon, 3 Jun 2013 16:04:46 +0100 Subject: [PATCH 03/75] Add option for user to set concatenation character(s) for id3v23 multi-values --- picard/formats/id3.py | 2 +- picard/formats/mutagenext/compatid3.py | 4 +- picard/resources.py | 2 +- picard/ui/options/tags.py | 7 ++ picard/ui/ui_options_tags.py | 111 ++++++++++++++++--------- ui/options_tags.ui | 83 ++++++++++++++++-- 6 files changed, 161 insertions(+), 48 deletions(-) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index dfc508c9e..7aacb673e 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -378,7 +378,7 @@ class ID3File(File): tags.add(tipl) if settings['write_id3v23']: - tags.update_to_v23() + tags.update_to_v23(join_with=settings['id3v23_join_with']) tags.save(encode_filename(filename), v2=3, v1=v1) else: tags.update_to_v24() diff --git a/picard/formats/mutagenext/compatid3.py b/picard/formats/mutagenext/compatid3.py index 8ca98a6bc..62dae8bba 100644 --- a/picard/formats/mutagenext/compatid3.py +++ b/picard/formats/mutagenext/compatid3.py @@ -153,7 +153,7 @@ class CompatID3(ID3): header = pack('>4s4sH', type(frame).__name__, datasize, flags) return header + framedata - def update_to_v23(self): + def update_to_v23(self,join_with="/"): """Convert older (and newer) tags into an ID3v2.3 tag. This updates incompatible ID3v2 frames to ID3v2.3 ones. If you @@ -232,6 +232,6 @@ class CompatID3(ID3): # ID3v2.3 doesn't support multiple values if isinstance(frame, mutagen.id3.TextFrame): try: - frame.text = ["/".join(frame.text)] + frame.text = [join_with.join(frame.text)] except TypeError: frame.text = frame.text[:1] diff --git a/picard/resources.py b/picard/resources.py index 2e97c868f..b96677568 100644 --- a/picard/resources.py +++ b/picard/resources.py @@ -2,7 +2,7 @@ # Resource object code # -# Created: Wed 22. May 11:33:39 2013 +# Created: Mon 3. Jun 11:55:41 2013 # by: The Resource Compiler for PyQt (Qt v4.8.4) # # WARNING! All changes made in this file will be lost! diff --git a/picard/ui/options/tags.py b/picard/ui/options/tags.py index 6eb8587d6..4afe07461 100644 --- a/picard/ui/options/tags.py +++ b/picard/ui/options/tags.py @@ -38,6 +38,7 @@ class TagsOptionsPage(OptionsPage): BoolOption("setting", "write_id3v1", True), BoolOption("setting", "write_id3v23", True), TextOption("setting", "id3v2_encoding", "utf-16"), + TextOption("setting", "id3v23_join_with", "/"), BoolOption("setting", "remove_id3_from_flac", False), BoolOption("setting", "remove_ape_from_mp3", False), BoolOption("setting", "tpe2_albumartist", False), @@ -69,6 +70,7 @@ class TagsOptionsPage(OptionsPage): self.ui.enc_utf16.setChecked(True) else: self.ui.enc_utf8.setChecked(True) + self.ui.id3v23_join_with.setEditText(self.config.setting["id3v23_join_with"]) self.ui.remove_ape_from_mp3.setChecked(self.config.setting["remove_ape_from_mp3"]) self.ui.remove_id3_from_flac.setChecked(self.config.setting["remove_id3_from_flac"]) self.ui.preserved_tags.setText(self.config.setting["preserved_tags"]) @@ -83,6 +85,7 @@ class TagsOptionsPage(OptionsPage): self.tagger.window.metadata_box.update() self.config.setting["write_id3v1"] = self.ui.write_id3v1.isChecked() self.config.setting["write_id3v23"] = self.ui.write_id3v23.isChecked() + self.config.setting["id3v23_join_with"] = unicode(self.ui.id3v23_join_with.currentText()) if self.ui.enc_iso88591.isChecked(): self.config.setting["id3v2_encoding"] = "iso-8859-1" elif self.ui.enc_utf16.isChecked(): @@ -99,8 +102,12 @@ class TagsOptionsPage(OptionsPage): if self.ui.enc_utf8.isChecked(): self.ui.enc_utf16.setChecked(True) self.ui.enc_utf8.setEnabled(False) + self.ui.label_id3v23_join_with.setEnabled(True) + self.ui.id3v23_join_with.setEnabled(True) else: self.ui.enc_utf8.setEnabled(True) + self.ui.label_id3v23_join_with.setEnabled(False) + self.ui.id3v23_join_with.setEnabled(False) def preserved_tags_edited(self, text): prefix = unicode(text)[:self.ui.preserved_tags.cursorPosition()].split(" ")[-1] diff --git a/picard/ui/ui_options_tags.py b/picard/ui/ui_options_tags.py index 027486a23..9490f520b 100644 --- a/picard/ui/ui_options_tags.py +++ b/picard/ui/ui_options_tags.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'ui/options_tags.ui' +# Form implementation generated from reading ui file 'ui\options_tags.ui' # -# Created: Fri May 24 14:58:44 2013 +# Created: Mon Jun 03 12:50:08 2013 # by: PyQt4 UI code generator 4.10.1 # # WARNING! All changes made in this file will be lost! @@ -35,84 +35,114 @@ class Ui_TagsOptionsPage(object): self.preserve_timestamps = QtGui.QCheckBox(TagsOptionsPage) self.preserve_timestamps.setObjectName(_fromUtf8("preserve_timestamps")) self.vboxlayout.addWidget(self.preserve_timestamps) - self.rename_files = QtGui.QGroupBox(TagsOptionsPage) - self.rename_files.setObjectName(_fromUtf8("rename_files")) - self.vboxlayout1 = QtGui.QVBoxLayout(self.rename_files) + self.before_tagging = QtGui.QGroupBox(TagsOptionsPage) + self.before_tagging.setObjectName(_fromUtf8("before_tagging")) + self.vboxlayout1 = QtGui.QVBoxLayout(self.before_tagging) self.vboxlayout1.setSpacing(2) + self.vboxlayout1.setContentsMargins(-1, 6, -1, 7) self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1")) - self.clear_existing_tags = QtGui.QCheckBox(self.rename_files) + self.clear_existing_tags = QtGui.QCheckBox(self.before_tagging) self.clear_existing_tags.setObjectName(_fromUtf8("clear_existing_tags")) self.vboxlayout1.addWidget(self.clear_existing_tags) - self.remove_id3_from_flac = QtGui.QCheckBox(self.rename_files) + self.remove_id3_from_flac = QtGui.QCheckBox(self.before_tagging) self.remove_id3_from_flac.setObjectName(_fromUtf8("remove_id3_from_flac")) self.vboxlayout1.addWidget(self.remove_id3_from_flac) - self.remove_ape_from_mp3 = QtGui.QCheckBox(self.rename_files) + self.remove_ape_from_mp3 = QtGui.QCheckBox(self.before_tagging) self.remove_ape_from_mp3.setObjectName(_fromUtf8("remove_ape_from_mp3")) self.vboxlayout1.addWidget(self.remove_ape_from_mp3) spacerItem = QtGui.QSpacerItem(20, 6, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) self.vboxlayout1.addItem(spacerItem) - self.preserved_tags_label = QtGui.QLabel(self.rename_files) + self.preserved_tags_label = QtGui.QLabel(self.before_tagging) self.preserved_tags_label.setObjectName(_fromUtf8("preserved_tags_label")) self.vboxlayout1.addWidget(self.preserved_tags_label) - self.preserved_tags = QtGui.QLineEdit(self.rename_files) + self.preserved_tags = QtGui.QLineEdit(self.before_tagging) self.preserved_tags.setObjectName(_fromUtf8("preserved_tags")) self.vboxlayout1.addWidget(self.preserved_tags) - self.preserved_tags_help = QtGui.QLabel(self.rename_files) + self.preserved_tags_help = QtGui.QLabel(self.before_tagging) self.preserved_tags_help.setObjectName(_fromUtf8("preserved_tags_help")) self.vboxlayout1.addWidget(self.preserved_tags_help) - self.vboxlayout.addWidget(self.rename_files) - self.rename_files_2 = QtGui.QGroupBox(TagsOptionsPage) - self.rename_files_2.setObjectName(_fromUtf8("rename_files_2")) - self.vboxlayout2 = QtGui.QVBoxLayout(self.rename_files_2) + self.vboxlayout.addWidget(self.before_tagging) + self.tag_compatibility = QtGui.QGroupBox(TagsOptionsPage) + self.tag_compatibility.setObjectName(_fromUtf8("tag_compatibility")) + self.vboxlayout2 = QtGui.QVBoxLayout(self.tag_compatibility) self.vboxlayout2.setSpacing(2) + self.vboxlayout2.setContentsMargins(-1, 6, -1, 7) self.vboxlayout2.setObjectName(_fromUtf8("vboxlayout2")) - self.groupBox = QtGui.QGroupBox(self.rename_files_2) - self.groupBox.setFlat(False) - self.groupBox.setCheckable(False) - self.groupBox.setObjectName(_fromUtf8("groupBox")) - self.horizontalLayout = QtGui.QHBoxLayout(self.groupBox) + self.id3v2_version = QtGui.QGroupBox(self.tag_compatibility) + self.id3v2_version.setFlat(False) + self.id3v2_version.setCheckable(False) + self.id3v2_version.setObjectName(_fromUtf8("id3v2_version")) + self.horizontalLayout = QtGui.QHBoxLayout(self.id3v2_version) + self.horizontalLayout.setContentsMargins(-1, 6, -1, 7) self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) - self.write_id3v24 = QtGui.QRadioButton(self.groupBox) + self.write_id3v24 = QtGui.QRadioButton(self.id3v2_version) self.write_id3v24.setChecked(True) self.write_id3v24.setObjectName(_fromUtf8("write_id3v24")) self.horizontalLayout.addWidget(self.write_id3v24) - self.write_id3v23 = QtGui.QRadioButton(self.groupBox) + self.write_id3v23 = QtGui.QRadioButton(self.id3v2_version) self.write_id3v23.setChecked(False) self.write_id3v23.setObjectName(_fromUtf8("write_id3v23")) self.horizontalLayout.addWidget(self.write_id3v23) - self.label = QtGui.QLabel(self.groupBox) + self.label = QtGui.QLabel(self.id3v2_version) self.label.setText(_fromUtf8("")) self.label.setWordWrap(True) self.label.setObjectName(_fromUtf8("label")) self.horizontalLayout.addWidget(self.label) spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem1) - self.vboxlayout2.addWidget(self.groupBox) - self.groupBox_2 = QtGui.QGroupBox(self.rename_files_2) - self.groupBox_2.setObjectName(_fromUtf8("groupBox_2")) - self.horizontalLayout_2 = QtGui.QHBoxLayout(self.groupBox_2) + self.vboxlayout2.addWidget(self.id3v2_version) + self.id3v2_text_encoding = QtGui.QGroupBox(self.tag_compatibility) + self.id3v2_text_encoding.setObjectName(_fromUtf8("id3v2_text_encoding")) + self.horizontalLayout_2 = QtGui.QHBoxLayout(self.id3v2_text_encoding) + self.horizontalLayout_2.setContentsMargins(-1, 6, -1, 7) self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) - self.enc_utf8 = QtGui.QRadioButton(self.groupBox_2) + self.enc_utf8 = QtGui.QRadioButton(self.id3v2_text_encoding) self.enc_utf8.setObjectName(_fromUtf8("enc_utf8")) self.horizontalLayout_2.addWidget(self.enc_utf8) - self.enc_utf16 = QtGui.QRadioButton(self.groupBox_2) + self.enc_utf16 = QtGui.QRadioButton(self.id3v2_text_encoding) self.enc_utf16.setObjectName(_fromUtf8("enc_utf16")) self.horizontalLayout_2.addWidget(self.enc_utf16) - self.enc_iso88591 = QtGui.QRadioButton(self.groupBox_2) + self.enc_iso88591 = QtGui.QRadioButton(self.id3v2_text_encoding) self.enc_iso88591.setObjectName(_fromUtf8("enc_iso88591")) self.horizontalLayout_2.addWidget(self.enc_iso88591) spacerItem2 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacerItem2) - self.label_2 = QtGui.QLabel(self.groupBox_2) + self.label_2 = QtGui.QLabel(self.id3v2_text_encoding) self.label_2.setText(_fromUtf8("")) self.label_2.setWordWrap(True) self.label_2.setObjectName(_fromUtf8("label_2")) self.horizontalLayout_2.addWidget(self.label_2) - self.vboxlayout2.addWidget(self.groupBox_2) - self.write_id3v1 = QtGui.QCheckBox(self.rename_files_2) + self.vboxlayout2.addWidget(self.id3v2_text_encoding) + self.hbox_id3v23_join_with = QtGui.QHBoxLayout() + self.hbox_id3v23_join_with.setObjectName(_fromUtf8("hbox_id3v23_join_with")) + self.label_id3v23_join_with = QtGui.QLabel(self.tag_compatibility) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(4) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_id3v23_join_with.sizePolicy().hasHeightForWidth()) + self.label_id3v23_join_with.setSizePolicy(sizePolicy) + self.label_id3v23_join_with.setObjectName(_fromUtf8("label_id3v23_join_with")) + self.hbox_id3v23_join_with.addWidget(self.label_id3v23_join_with) + self.id3v23_join_with = QtGui.QComboBox(self.tag_compatibility) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.id3v23_join_with.sizePolicy().hasHeightForWidth()) + self.id3v23_join_with.setSizePolicy(sizePolicy) + self.id3v23_join_with.setEditable(True) + self.id3v23_join_with.setObjectName(_fromUtf8("id3v23_join_with")) + self.id3v23_join_with.addItem(_fromUtf8("")) + self.id3v23_join_with.setItemText(0, _fromUtf8("/")) + self.id3v23_join_with.addItem(_fromUtf8("")) + self.id3v23_join_with.setItemText(1, _fromUtf8("; ")) + self.id3v23_join_with.addItem(_fromUtf8("")) + self.id3v23_join_with.setItemText(2, _fromUtf8(" / ")) + self.hbox_id3v23_join_with.addWidget(self.id3v23_join_with) + self.vboxlayout2.addLayout(self.hbox_id3v23_join_with) + self.write_id3v1 = QtGui.QCheckBox(self.tag_compatibility) self.write_id3v1.setObjectName(_fromUtf8("write_id3v1")) self.vboxlayout2.addWidget(self.write_id3v1) - self.vboxlayout.addWidget(self.rename_files_2) + self.vboxlayout.addWidget(self.tag_compatibility) spacerItem3 = QtGui.QSpacerItem(274, 41, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) self.vboxlayout.addItem(spacerItem3) @@ -128,24 +158,27 @@ class Ui_TagsOptionsPage(object): TagsOptionsPage.setTabOrder(self.write_id3v23, self.enc_utf8) TagsOptionsPage.setTabOrder(self.enc_utf8, self.enc_utf16) TagsOptionsPage.setTabOrder(self.enc_utf16, self.enc_iso88591) - TagsOptionsPage.setTabOrder(self.enc_iso88591, self.write_id3v1) + TagsOptionsPage.setTabOrder(self.enc_iso88591, self.id3v23_join_with) + TagsOptionsPage.setTabOrder(self.id3v23_join_with, self.write_id3v1) def retranslateUi(self, TagsOptionsPage): self.write_tags.setText(_("Write tags to files")) self.preserve_timestamps.setText(_("Preserve timestamps of tagged files")) - self.rename_files.setTitle(_("Before tagging")) + self.before_tagging.setTitle(_("Before tagging")) self.clear_existing_tags.setText(_("Clear existing tags")) self.remove_id3_from_flac.setText(_("Remove ID3 tags from FLAC files")) self.remove_ape_from_mp3.setText(_("Remove APEv2 tags from MP3 files")) self.preserved_tags_label.setText(_("Preserve these tags from being cleared or overwritten with MusicBrainz data:")) self.preserved_tags_help.setText(_("Tags are separated by spaces, and are case-sensitive.")) - self.rename_files_2.setTitle(_("Tag compatibility")) - self.groupBox.setTitle(_("ID3v2 version")) + self.tag_compatibility.setTitle(_("Tag compatibility")) + self.id3v2_version.setTitle(_("ID3v2 version")) self.write_id3v24.setText(_("2.4")) self.write_id3v23.setText(_("2.3")) - self.groupBox_2.setTitle(_("ID3v2 text encoding")) + self.id3v2_text_encoding.setTitle(_("ID3v2 text encoding")) self.enc_utf8.setText(_("UTF-8")) self.enc_utf16.setText(_("UTF-16")) self.enc_iso88591.setText(_("ISO-8859-1")) + self.label_id3v23_join_with.setText(_("Join multiple ID3v2.3 tags with:")) + self.id3v23_join_with.setToolTip(_("

Default is \'/\' to maintain compatibility with previous Picard releases.

New alternatives are \';_\' or \'_/_\' or type your own.

")) self.write_id3v1.setText(_("Also include ID3v1 tags in the files")) diff --git a/ui/options_tags.ui b/ui/options_tags.ui index 9dcbc1a9b..720b7cfa9 100644 --- a/ui/options_tags.ui +++ b/ui/options_tags.ui @@ -26,7 +26,7 @@ - + Before tagging @@ -34,6 +34,12 @@ 2 + + 6 + + + 7 + @@ -92,7 +98,7 @@ - + Tag compatibility @@ -100,8 +106,14 @@ 2 + + 6 + + + 7 + - + ID3v2 version @@ -112,6 +124,12 @@ false + + 6 + + + 7 + @@ -159,11 +177,17 @@ - + ID3v2 text encoding + + 6 + + + 7 + @@ -211,6 +235,54 @@ + + + + + + + 4 + 0 + + + + Join multiple ID3v2.3 tags with: + + + + + + + + 1 + 0 + + + + <html><head/><body><p>Default is '/' to maintain compatibility with previous Picard releases.</p><p>New alternatives are ';_' or '_/_' or type your own. </p></body></html> + + + true + + + + / + + + + + ; + + + + + / + + + + + + @@ -222,7 +294,7 @@ - + Qt::Vertical @@ -248,6 +320,7 @@ enc_utf8 enc_utf16 enc_iso88591 + id3v23_join_with write_id3v1 From 74121d709d81bb130967ac6ae93b965d6fc562cf Mon Sep 17 00:00:00 2001 From: Sophist Date: Mon, 3 Jun 2013 16:11:36 +0100 Subject: [PATCH 04/75] Improve displayed values and comparison when saving id3v23 --- picard/file.py | 2 +- picard/formats/id3.py | 7 ++++++- picard/metadata.py | 26 ++++++++++++++++++++++++++ picard/ui/metadatabox.py | 5 +++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/picard/file.py b/picard/file.py index bb607cac8..389161e73 100644 --- a/picard/file.py +++ b/picard/file.py @@ -123,7 +123,7 @@ class File(QtCore.QObject, Item): else: metadata['tracknumber'] = str(tracknumber) self.orig_metadata.copy(metadata) - self.metadata = metadata + self.metadata.copy(metadata) _default_preserved_tags = [ "~bitrate", "~bits_per_sample", "~format", "~channels", "~filename", diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 7aacb673e..a5a26926f 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -22,7 +22,7 @@ import mutagen.mp3 import mutagen.trueaudio from collections import defaultdict from mutagen import id3 -from picard.metadata import Metadata +from picard.metadata import Metadata, ID3Metadata from picard.file import File from picard.formats.mutagenext import compatid3 from picard.util import encode_filename, sanitize_date @@ -155,6 +155,11 @@ class ID3File(File): __other_supported_tags = ("discnumber", "tracknumber", "totaldiscs", "totaltracks") + def __init__(self, filename): + super(ID3File, self).__init__(filename) + self.metadata = ID3Metadata() + self.metadata.tipl_roles = self.__rtipl_roles + def _load(self, filename): self.log.debug("Loading file %r", filename) file = self._File(encode_filename(filename), ID3=compatid3.CompatID3) diff --git a/picard/metadata.py b/picard/metadata.py index 8a05b7a19..8301c5aef 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -324,3 +324,29 @@ def run_album_metadata_processors(tagger, metadata, release): def run_track_metadata_processors(tagger, metadata, release, track): for processor in _track_metadata_processors: processor(tagger, metadata, track, release) + + +class ID3Metadata(Metadata): + """Subclass of Metadata to return New values in id3v23 format if Picard is set to write ID3v23.""" + + def __init__(self): + super(ID3Metadata, self).__init__() + self.tipl_roles = dict() + + def getall(self, name): + values=super(ID3Metadata, self).getall(name) + setting = QObject.config.setting + if setting["write_id3v23"] and len(values)>1 and not name in self.tipl_roles and not name.startswith("performer:"): + return [setting["id3v23_join_with"].join(values)] + else: + return values + + def get(self, name, default=None): + values = dict.get(self, name, None) + if not values: + values = default + setting = QObject.config.setting + if setting["write_id3v23"]: + return setting["id3v23_join_with"].join(values) + else: + return MULTI_VALUED_JOINER.join(values) diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index 1a2f38624..0ad8ccb88 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -28,6 +28,7 @@ from picard.config import TextOption, BoolOption from picard.util import partial from picard.util.tags import display_tag_name from picard.ui.edittagdialog import EditTagDialog +from picard.metadata import MULTI_VALUED_JOINER COMMON_TAGS = [ @@ -82,7 +83,7 @@ class TagCounter(dict): if tag in self.different: return (ungettext("(different across %d item)", "(different across %d items)", count) % count, True) else: - msg = "; ".join(self[tag]) + msg = MULTI_VALUED_JOINER.join(self[tag]) if count > 0 and missing > 0: return (msg + " " + (ungettext("(missing from %d item)", "(missing from %d items)", missing) % missing), True) else: @@ -189,7 +190,7 @@ class MetadataBox(QtGui.QTableWidget): else: self.editing = True self.itemChanged.disconnect(self.item_changed) - item.setText("; ".join(values)) + item.setText(MULTI_VALUED_JOINER.join(values)) self.itemChanged.connect(self.item_changed) return QtGui.QTableWidget.edit(self, index, trigger, event) return False From 9835cab8de43cb2f2f2316b975b90f4fe32ec01e Mon Sep 17 00:00:00 2001 From: Sophist Date: Mon, 3 Jun 2013 16:37:09 +0100 Subject: [PATCH 05/75] Fix original values metadata class after save --- picard/file.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/picard/file.py b/picard/file.py index 389161e73..33ebf2a68 100644 --- a/picard/file.py +++ b/picard/file.py @@ -222,9 +222,13 @@ class File(QtCore.QObject, Item): for info in ('~bitrate', '~sample_rate', '~channels', '~bits_per_sample', '~format'): temp_info[info] = self.orig_metadata[info] + # handle save of id3v23 to correct metadata class if self.config.setting["clear_existing_tags"]: - self.orig_metadata.copy(self.metadata) + self.orig_metadata = self.metadata else: + self.old_orig_metadata = self.orig_metadata + self.orig_metadata = self.metadata + self.orig_metadata.copy(self.old_orig_metadata) self.orig_metadata.update(self.metadata) self.orig_metadata.length = length self.orig_metadata['~length'] = format_time(length) From 9e675e4973c5fb9925760a47e8272ec70cab4957 Mon Sep 17 00:00:00 2001 From: Sophist Date: Mon, 3 Jun 2013 17:34:59 +0100 Subject: [PATCH 06/75] Handle TypeError on flattening --- picard/metadata.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/picard/metadata.py b/picard/metadata.py index 8301c5aef..f7b17fa04 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -347,6 +347,10 @@ class ID3Metadata(Metadata): values = default setting = QObject.config.setting if setting["write_id3v23"]: - return setting["id3v23_join_with"].join(values) + try: + return setting["id3v23_join_with"].join(values) + except TypeError: + self.log.warning("TypeError handled in ID3Metadata:get -",name,values) + return values[:1] else: return MULTI_VALUED_JOINER.join(values) From 9a11aded5bf5403afc0fb24e166ed24f0f302659 Mon Sep 17 00:00:00 2001 From: Sophist Date: Wed, 5 Jun 2013 11:21:44 +0100 Subject: [PATCH 07/75] Address peer review comments Improved object handling on file-save. Improved code for identification of id3v23 true multi-value fields. etc. --- NEWS.txt | 5 ++++- picard/file.py | 7 ++----- picard/formats/id3.py | 45 +++++++++++++++++++++++++++++++++++-------- picard/metadata.py | 39 +++---------------------------------- 4 files changed, 46 insertions(+), 50 deletions(-) diff --git a/NEWS.txt b/NEWS.txt index 81d1c8650..2a731a1cb 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,5 +1,8 @@ -Version 1.3 - xxxx-xx-xx +Version 1.3 - xxxx-xx-xx * The "About" window now displays the versions of libraries used by Picard + * Picard now correctly handles matching of collections saved in ID3v23 (which is the version that Microsoft Windows recognises) much better. + * A sort tags plugin is now provided as tag data is no longer displayed sorted by default. + Note: You may need to resave your tags once to get them to match in future. Version 1.2 - 2013-03-30 * Picard now requires at least Python 2.6 diff --git a/picard/file.py b/picard/file.py index 33ebf2a68..7c9182ddc 100644 --- a/picard/file.py +++ b/picard/file.py @@ -122,7 +122,7 @@ class File(QtCore.QObject, Item): pass else: metadata['tracknumber'] = str(tracknumber) - self.orig_metadata.copy(metadata) + self.orig_metadata = metadata self.metadata.copy(metadata) _default_preserved_tags = [ @@ -224,11 +224,8 @@ class File(QtCore.QObject, Item): temp_info[info] = self.orig_metadata[info] # handle save of id3v23 to correct metadata class if self.config.setting["clear_existing_tags"]: - self.orig_metadata = self.metadata + self.orig_metadata.copy(self.metadata) else: - self.old_orig_metadata = self.orig_metadata - self.orig_metadata = self.metadata - self.orig_metadata.copy(self.old_orig_metadata) self.orig_metadata.update(self.metadata) self.orig_metadata.length = length self.orig_metadata['~length'] = format_time(length) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index a5a26926f..7d69e865e 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -22,11 +22,12 @@ import mutagen.mp3 import mutagen.trueaudio from collections import defaultdict from mutagen import id3 -from picard.metadata import Metadata, ID3Metadata +from picard.metadata import Metadata from picard.file import File from picard.formats.mutagenext import compatid3 from picard.util import encode_filename, sanitize_date from urlparse import urlparse +from PyQt4.QtCore import QObject # Ugly, but... I need to save the text in ISO-8859-1 even if it contains @@ -143,14 +144,14 @@ class ID3File(File): } __rtranslate_freetext = dict([(v, k) for k, v in __translate_freetext.iteritems()]) - __tipl_roles = { + _tipl_roles = { 'engineer': 'engineer', 'arranger': 'arranger', 'producer': 'producer', 'DJ-mix': 'djmixer', 'mix': 'mixer', } - __rtipl_roles = dict([(v, k) for k, v in __tipl_roles.iteritems()]) + _rtipl_roles = dict([(v, k) for k, v in _tipl_roles.iteritems()]) __other_supported_tags = ("discnumber", "tracknumber", "totaldiscs", "totaltracks") @@ -158,7 +159,6 @@ class ID3File(File): def __init__(self, filename): super(ID3File, self).__init__(filename) self.metadata = ID3Metadata() - self.metadata.tipl_roles = self.__rtipl_roles def _load(self, filename): self.log.debug("Loading file %r", filename) @@ -192,8 +192,8 @@ class ID3File(File): # If file is ID3v2.3, TIPL tag could contain TMCL # so we will test for TMCL values and add to TIPL if not TMCL for role, name in frame.people: - if role in self.__tipl_roles and name: - metadata.add(self.__tipl_roles[role], name) + if role in self._tipl_roles and name: + metadata.add(self._tipl_roles[role], name) else: metadata.add('performer:%s' % role, name) elif frameid == 'TXXX': @@ -325,9 +325,9 @@ class ID3File(File): desc = '' for value in values: tags.add(id3.USLT(encoding=encoding, desc=desc, text=value)) - elif name in self.__rtipl_roles: + elif name in self._rtipl_roles: for value in values: - tipl.people.append([self.__rtipl_roles[name], value]) + tipl.people.append([self._rtipl_roles[name], value]) elif name == 'musicbrainz_trackid': tags.add(id3.UFID(owner='http://musicbrainz.org', data=str(values[0]))) elif name == '~rating': @@ -418,3 +418,32 @@ class TrueAudioFile(ID3File): def _info(self, metadata, file): super(TrueAudioFile, self)._info(metadata, file) metadata['~format'] = self.NAME + + +class ID3Metadata(Metadata): + """Subclass of Metadata to return New values in id3v23 format if Picard is set to write ID3v23.""" + + def getall(self, name): + values=super(ID3Metadata, self).getall(name) + setting = QObject.config.setting + if (setting["write_id3v23"] and len(values)>1 and + not name in ID3File._rtipl_roles and + not name.startswith("performer:")): + + return [setting["id3v23_join_with"].join(values)] + else: + return values + + def get(self, name, default=None): + values = dict.get(self, name, None) + if not values: + return default + setting = QObject.config.setting + if setting["write_id3v23"]: + try: + return setting["id3v23_join_with"].join(values) + except TypeError: + self.log.warning("TypeError handled in ID3Metadata:get -",name,values) + return values[:1] + else: + return MULTI_VALUED_JOINER.join(values) diff --git a/picard/metadata.py b/picard/metadata.py index f7b17fa04..70f161a47 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -211,14 +211,11 @@ class Metadata(dict): def copy(self, other): self.clear() - for key, values in other.rawitems(): - self.set(key, values[:]) - self.images = other.images[:] - self.length = other.length + self.update(other) def update(self, other): - for name, values in other.rawitems(): - self.set(name, values[:]) + for key in other.iterkeys(): + self.set(key, other.getall(key)) if other.images: self.images = other.images[:] if other.length: @@ -324,33 +321,3 @@ def run_album_metadata_processors(tagger, metadata, release): def run_track_metadata_processors(tagger, metadata, release, track): for processor in _track_metadata_processors: processor(tagger, metadata, track, release) - - -class ID3Metadata(Metadata): - """Subclass of Metadata to return New values in id3v23 format if Picard is set to write ID3v23.""" - - def __init__(self): - super(ID3Metadata, self).__init__() - self.tipl_roles = dict() - - def getall(self, name): - values=super(ID3Metadata, self).getall(name) - setting = QObject.config.setting - if setting["write_id3v23"] and len(values)>1 and not name in self.tipl_roles and not name.startswith("performer:"): - return [setting["id3v23_join_with"].join(values)] - else: - return values - - def get(self, name, default=None): - values = dict.get(self, name, None) - if not values: - values = default - setting = QObject.config.setting - if setting["write_id3v23"]: - try: - return setting["id3v23_join_with"].join(values) - except TypeError: - self.log.warning("TypeError handled in ID3Metadata:get -",name,values) - return values[:1] - else: - return MULTI_VALUED_JOINER.join(values) From 4b0c7d0a07521e769019b7076cd6e635c4363942 Mon Sep 17 00:00:00 2001 From: Sophist Date: Wed, 5 Jun 2013 21:23:39 +0100 Subject: [PATCH 08/75] Remove spurious exception handler. --- picard/formats/id3.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 7d69e865e..7fa03e8dc 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -440,10 +440,6 @@ class ID3Metadata(Metadata): return default setting = QObject.config.setting if setting["write_id3v23"]: - try: - return setting["id3v23_join_with"].join(values) - except TypeError: - self.log.warning("TypeError handled in ID3Metadata:get -",name,values) - return values[:1] + return setting["id3v23_join_with"].join(values) else: return MULTI_VALUED_JOINER.join(values) From ea89cd5a003d79a7aa47e2481d99a4ee9ea39eea Mon Sep 17 00:00:00 2001 From: Sophist Date: Wed, 5 Jun 2013 21:26:32 +0100 Subject: [PATCH 09/75] Resequence change notes. --- NEWS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.txt b/NEWS.txt index 2a731a1cb..aabd86abc 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,8 +1,8 @@ Version 1.3 - xxxx-xx-xx * The "About" window now displays the versions of libraries used by Picard * Picard now correctly handles matching of collections saved in ID3v23 (which is the version that Microsoft Windows recognises) much better. - * A sort tags plugin is now provided as tag data is no longer displayed sorted by default. Note: You may need to resave your tags once to get them to match in future. + * A sort tags plugin is now provided as tag data is no longer displayed sorted by default. Version 1.2 - 2013-03-30 * Picard now requires at least Python 2.6 From 550b82f80eaf342072695e212ec0adf000c83f8f Mon Sep 17 00:00:00 2001 From: Sophist Date: Wed, 5 Jun 2013 21:28:57 +0100 Subject: [PATCH 10/75] Fix terminology. --- NEWS.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/NEWS.txt b/NEWS.txt index aabd86abc..0ff751259 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,7 +1,8 @@ Version 1.3 - xxxx-xx-xx * The "About" window now displays the versions of libraries used by Picard - * Picard now correctly handles matching of collections saved in ID3v23 (which is the version that Microsoft Windows recognises) much better. - Note: You may need to resave your tags once to get them to match in future. + * Picard now correctly handles matching of collections saved in ID3v2.3 tags + (which is the version that Microsoft Windows recognises) much better. + Note: You may need to resave your tags once to get them to match in future. * A sort tags plugin is now provided as tag data is no longer displayed sorted by default. Version 1.2 - 2013-03-30 From 4a74a220843c14391114987cd7c5a424f1cee170 Mon Sep 17 00:00:00 2001 From: Sophist Date: Wed, 5 Jun 2013 21:32:12 +0100 Subject: [PATCH 11/75] Another minor terminology tweak in News.txt --- NEWS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.txt b/NEWS.txt index 0ff751259..fbd22a5dc 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,6 +1,6 @@ Version 1.3 - xxxx-xx-xx * The "About" window now displays the versions of libraries used by Picard - * Picard now correctly handles matching of collections saved in ID3v2.3 tags + * Picard now correctly handles matching of MP3 files saved in ID3v2.3 tags (which is the version that Microsoft Windows recognises) much better. Note: You may need to resave your tags once to get them to match in future. * A sort tags plugin is now provided as tag data is no longer displayed sorted by default. From 29d967e40632f7626ac7c08212ea1606d371f0ab Mon Sep 17 00:00:00 2001 From: Sophist Date: Wed, 5 Jun 2013 21:35:17 +0100 Subject: [PATCH 12/75] Remove incorrect comment. --- picard/file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/picard/file.py b/picard/file.py index 7c9182ddc..09fe4a155 100644 --- a/picard/file.py +++ b/picard/file.py @@ -222,7 +222,6 @@ class File(QtCore.QObject, Item): for info in ('~bitrate', '~sample_rate', '~channels', '~bits_per_sample', '~format'): temp_info[info] = self.orig_metadata[info] - # handle save of id3v23 to correct metadata class if self.config.setting["clear_existing_tags"]: self.orig_metadata.copy(self.metadata) else: From c809968d1cbe7abab5bb38d33ab6fa01da39e11c Mon Sep 17 00:00:00 2001 From: Sophist Date: Thu, 6 Jun 2013 10:06:39 +0100 Subject: [PATCH 13/75] Fix function and white space --- contrib/plugins/sort_multivalue_tags.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/plugins/sort_multivalue_tags.py b/contrib/plugins/sort_multivalue_tags.py index e5d052d0c..e61f2e985 100644 --- a/contrib/plugins/sort_multivalue_tags.py +++ b/contrib/plugins/sort_multivalue_tags.py @@ -24,11 +24,11 @@ from picard.metadata import register_track_metadata_processor # Define and register the Track Metadata function def sort_multivalue_tags(tagger, metadata, track, release): - for tag in filter(lambda x: len(metadata[x]) > 1, metadata.keys()): - data = metadata.getall(tag) - if len(data) > 1: - sorted_data = sorted(data) - if data != sorted_data: + for tag in metadata.keys(): + data = metadata.getall(tag) + if len(data) > 1: + sorted_data = sorted(data) + if data != sorted_data: metadata.set(tag,sorted_data) register_track_metadata_processor(sort_multivalue_tags) From 69c73f2527f964eaf407eeeff7e6dec80b3bc7b6 Mon Sep 17 00:00:00 2001 From: Sophist Date: Sat, 15 Jun 2013 19:23:42 +0100 Subject: [PATCH 14/75] Handle id3v23 date / original date in yyyy only For id3 files where picard is set to write id3v23 tags, truncate dates provided by MB to yyyy (from yyyy-mm-dd). --- picard/formats/id3.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 7fa03e8dc..5d5914aa8 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -424,8 +424,9 @@ class ID3Metadata(Metadata): """Subclass of Metadata to return New values in id3v23 format if Picard is set to write ID3v23.""" def getall(self, name): - values=super(ID3Metadata, self).getall(name) + values = super(ID3Metadata, self).getall(name) setting = QObject.config.setting + values = self.__id3v23_date(setting,name,values) if (setting["write_id3v23"] and len(values)>1 and not name in ID3File._rtipl_roles and not name.startswith("performer:")): @@ -439,7 +440,16 @@ class ID3Metadata(Metadata): if not values: return default setting = QObject.config.setting + values = self.__id3v23_date(setting,name,values) if setting["write_id3v23"]: return setting["id3v23_join_with"].join(values) else: return MULTI_VALUED_JOINER.join(values) + + def __id3v23_date(self,setting,name,values): + # id3v23 can only save dates in yyyy format (cf. id3v24 and MB who provides dates in yyyy-mm-dd format) + if (setting["write_id3v23"] and + name in ("date","originaldate")): + for i, v in enumerate(values): + values[i] = v[:4] + return values From 7a7c255c6e4a73879c5c4e0ba844a269909f664e Mon Sep 17 00:00:00 2001 From: Sophist Date: Sun, 16 Jun 2013 12:39:13 +0100 Subject: [PATCH 15/75] Fix white-space issues as pep8 --- picard/formats/id3.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 5d5914aa8..226617be6 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -426,7 +426,7 @@ class ID3Metadata(Metadata): def getall(self, name): values = super(ID3Metadata, self).getall(name) setting = QObject.config.setting - values = self.__id3v23_date(setting,name,values) + values = self.__id3v23_date(setting, name, values) if (setting["write_id3v23"] and len(values)>1 and not name in ID3File._rtipl_roles and not name.startswith("performer:")): @@ -440,16 +440,16 @@ class ID3Metadata(Metadata): if not values: return default setting = QObject.config.setting - values = self.__id3v23_date(setting,name,values) + values = self.__id3v23_date(setting, name, values) if setting["write_id3v23"]: return setting["id3v23_join_with"].join(values) else: return MULTI_VALUED_JOINER.join(values) - def __id3v23_date(self,setting,name,values): + def __id3v23_date(self, setting, name, values): # id3v23 can only save dates in yyyy format (cf. id3v24 and MB who provides dates in yyyy-mm-dd format) if (setting["write_id3v23"] and - name in ("date","originaldate")): + name in ("date", "originaldate")): for i, v in enumerate(values): values[i] = v[:4] return values From 26b310d04bfaa96df0fde8faaf8c3b8513bd70de Mon Sep 17 00:00:00 2001 From: Sophist Date: Mon, 17 Jun 2013 07:48:45 +0100 Subject: [PATCH 16/75] Minor white-space tweaks --- contrib/plugins/sort_multivalue_tags.py | 2 +- picard/formats/mutagenext/compatid3.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/plugins/sort_multivalue_tags.py b/contrib/plugins/sort_multivalue_tags.py index e61f2e985..e8b68bdcf 100644 --- a/contrib/plugins/sort_multivalue_tags.py +++ b/contrib/plugins/sort_multivalue_tags.py @@ -29,6 +29,6 @@ def sort_multivalue_tags(tagger, metadata, track, release): if len(data) > 1: sorted_data = sorted(data) if data != sorted_data: - metadata.set(tag,sorted_data) + metadata.set(tag, sorted_data) register_track_metadata_processor(sort_multivalue_tags) diff --git a/picard/formats/mutagenext/compatid3.py b/picard/formats/mutagenext/compatid3.py index 62dae8bba..78782b0ba 100644 --- a/picard/formats/mutagenext/compatid3.py +++ b/picard/formats/mutagenext/compatid3.py @@ -153,7 +153,7 @@ class CompatID3(ID3): header = pack('>4s4sH', type(frame).__name__, datasize, flags) return header + framedata - def update_to_v23(self,join_with="/"): + def update_to_v23(self, join_with="/"): """Convert older (and newer) tags into an ID3v2.3 tag. This updates incompatible ID3v2 frames to ID3v2.3 ones. If you From da1c2239b30907184b5c86d0d8b89ec87d02a97e Mon Sep 17 00:00:00 2001 From: Sophist Date: Mon, 17 Jun 2013 09:15:48 +0100 Subject: [PATCH 17/75] Fix review comments from luks --- picard/file.py | 1 + picard/formats/id3.py | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/picard/file.py b/picard/file.py index 09fe4a155..b7b072c45 100644 --- a/picard/file.py +++ b/picard/file.py @@ -222,6 +222,7 @@ class File(QtCore.QObject, Item): for info in ('~bitrate', '~sample_rate', '~channels', '~bits_per_sample', '~format'): temp_info[info] = self.orig_metadata[info] + # Data is copied from New to Original because New may be a subclass to handle id3v23 if self.config.setting["clear_existing_tags"]: self.orig_metadata.copy(self.metadata) else: diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 226617be6..329dd5d05 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -426,30 +426,30 @@ class ID3Metadata(Metadata): def getall(self, name): values = super(ID3Metadata, self).getall(name) setting = QObject.config.setting - values = self.__id3v23_date(setting, name, values) + vals = self.__id3v23_date(setting, name, values) + # if this is a multi-value field then it needs to be flattened + # unless it is TIPL or TMCL which can still be multi-value. if (setting["write_id3v23"] and len(values)>1 and - not name in ID3File._rtipl_roles and - not name.startswith("performer:")): - - return [setting["id3v23_join_with"].join(values)] + not name in ID3File._rtipl_roles and + not name.startswith("performer:")): + return [setting["id3v23_join_with"].join(vals)] else: - return values + return vals def get(self, name, default=None): values = dict.get(self, name, None) if not values: return default setting = QObject.config.setting - values = self.__id3v23_date(setting, name, values) + vals = self.__id3v23_date(setting, name, values) if setting["write_id3v23"]: - return setting["id3v23_join_with"].join(values) + return setting["id3v23_join_with"].join(vals) else: - return MULTI_VALUED_JOINER.join(values) + return MULTI_VALUED_JOINER.join(vals) def __id3v23_date(self, setting, name, values): - # id3v23 can only save dates in yyyy format (cf. id3v24 and MB who provides dates in yyyy-mm-dd format) - if (setting["write_id3v23"] and - name in ("date", "originaldate")): - for i, v in enumerate(values): - values[i] = v[:4] - return values + # id3v23 can only save TDOR dates in yyyy format (cf. id3v24 and MB who provides dates in yyyy-mm-dd format) + if (setting["write_id3v23"] and name == "originaldate"): + return [v[:4] for v in values] + else: + return values From fdbed3f6c1113cc20fcd1c3396983e330ced5051 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Tue, 18 Jun 2013 12:24:09 +0200 Subject: [PATCH 18/75] Remove fr_FR locale test, too much a requirement. --- test/po/fr.po | 69 ---------------------------------------- test/test_bytes2human.py | 14 -------- 2 files changed, 83 deletions(-) delete mode 100644 test/po/fr.po diff --git a/test/po/fr.po b/test/po/fr.po deleted file mode 100644 index 1f58b585b..000000000 --- a/test/po/fr.po +++ /dev/null @@ -1,69 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: MusicBrainz\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2013-01-20 13:23+0100\n" -"PO-Revision-Date: 2013-01-20 13:26+0100\n" -"Last-Translator: Laurent Monin \n" -"Language-Team: French (http://www.transifex.com/projects/p/musicbrainz/language/fr/)\n" -"Language: fr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#: picard/util/bytes2human.py:47 -#, python-format -msgid "%s B" -msgstr "%s o" - -#: picard/util/bytes2human.py:48 -#, python-format -msgid "%s kB" -msgstr "%s ko" - -#: picard/util/bytes2human.py:49 -#, python-format -msgid "%s KiB" -msgstr "%s Kio" - -#: picard/util/bytes2human.py:50 -#, python-format -msgid "%s MB" -msgstr "%s Mo" - -#: picard/util/bytes2human.py:51 -#, python-format -msgid "%s MiB" -msgstr "%s Mio" - -#: picard/util/bytes2human.py:52 -#, python-format -msgid "%s GB" -msgstr "%s Go" - -#: picard/util/bytes2human.py:53 -#, python-format -msgid "%s GiB" -msgstr "%s Gio" - -#: picard/util/bytes2human.py:54 -#, python-format -msgid "%s TB" -msgstr "%s To" - -#: picard/util/bytes2human.py:55 -#, python-format -msgid "%s TiB" -msgstr "%s Tio" - -#: picard/util/bytes2human.py:56 -#, python-format -msgid "%s PB" -msgstr "%s Po" - -#: picard/util/bytes2human.py:57 -#, python-format -msgid "%s PiB" -msgstr "%s Pio" diff --git a/test/test_bytes2human.py b/test/test_bytes2human.py index 06038bb41..5da804f62 100644 --- a/test/test_bytes2human.py +++ b/test/test_bytes2human.py @@ -17,13 +17,6 @@ class Testbytes2human(unittest.TestCase): self.addCleanup(shutil.rmtree, self.tmp_path) self.localedir = os.path.join(self.tmp_path, 'locale') - test_locales = [('picard', 'fr', 'test/po/fr.po')] - for domain, locale, po in test_locales: - path = os.path.join(self.localedir, locale, 'LC_MESSAGES') - os.makedirs(path) - mo = os.path.join(path, '%s.mo' % domain) - assert(subprocess.call(['msgfmt', '-o', mo, po]) == 0) - def tearDown(self): if sys.hexversion < 0x020700F0: shutil.rmtree(self.tmp_path) @@ -48,13 +41,6 @@ class Testbytes2human(unittest.TestCase): except Exception as e: self.fail('Unexpected exception: %s' % e) - def test_05(self): - # testing with french locale and translation - # 1.5 MiB -> 1,5 Mio - lang = 'fr_FR.UTF-8' - setup_gettext(self.localedir, lang) - self.run_test(lang) - def run_test(self, lang = 'C', create_test_data=False): """ Compare data generated with sample files From 4fc84a428b84ac4e44ca2552a8677aff0e1b77b2 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Wed, 19 Jun 2013 19:27:10 +0200 Subject: [PATCH 19/75] Remove (now) useless data test file. --- test/data/b2h_test_fr_FR.UTF-8.dat | 74 ------------------------------ 1 file changed, 74 deletions(-) delete mode 100644 test/data/b2h_test_fr_FR.UTF-8.dat diff --git a/test/data/b2h_test_fr_FR.UTF-8.dat b/test/data/b2h_test_fr_FR.UTF-8.dat deleted file mode 100644 index f781da84e..000000000 --- a/test/data/b2h_test_fr_FR.UTF-8.dat +++ /dev/null @@ -1,74 +0,0 @@ -0;0 o;0 o;0 o -1;1 o;1 o;1 o -100;100 o;100 o;100 o -102;102 o;102 o;102 o -500;500 o;500 o;500 o -512;512 o;512 o;512 o -990;990 o;990 o;990 o -999;999 o;999 o;999 o -1000;1 ko;1000 o;1000 o -1013;1 ko;1013 o;1013 o -1023;1 ko;1023 o;1023 o -1024;1 ko;1 Kio;1 Kio -1500;1,5 ko;1,5 Kio;1,46 Kio -1536;1,5 ko;1,5 Kio;1,50 Kio -100000;100 ko;97,7 Kio;97,66 Kio -104857;104,9 ko;102,4 Kio;102,40 Kio -500000;500 ko;488,3 Kio;488,28 Kio -524288;524,3 ko;512 Kio;512 Kio -990000;990 ko;966,8 Kio;966,80 Kio -999900;999,9 ko;976,5 Kio;976,46 Kio -1000000;1 Mo;976,6 Kio;976,56 Kio -1038090;1 Mo;1013,8 Kio;1013,76 Kio -1048471;1 Mo;1023,9 Kio;1023,90 Kio -1048576;1 Mo;1 Mio;1 Mio -1500000;1,5 Mo;1,4 Mio;1,43 Mio -1572864;1,6 Mo;1,5 Mio;1,50 Mio -100000000;100 Mo;95,4 Mio;95,37 Mio -107374182;107,4 Mo;102,4 Mio;102,40 Mio -500000000;500 Mo;476,8 Mio;476,84 Mio -536870912;536,9 Mo;512 Mio;512 Mio -990000000;990 Mo;944,1 Mio;944,14 Mio -999900000;999,9 Mo;953,6 Mio;953,58 Mio -1000000000;1 Go;953,7 Mio;953,67 Mio -1063004405;1,1 Go;1013,8 Mio;1013,76 Mio -1073634449;1,1 Go;1023,9 Mio;1023,90 Mio -1073741824;1,1 Go;1 Gio;1 Gio -1500000000;1,5 Go;1,4 Gio;1,40 Gio -1610612736;1,6 Go;1,5 Gio;1,50 Gio -100000000000;100 Go;93,1 Gio;93,13 Gio -109951162777;110,0 Go;102,4 Gio;102,40 Gio -500000000000;500 Go;465,7 Gio;465,66 Gio -549755813888;549,8 Go;512 Gio;512 Gio -990000000000;990 Go;922 Gio;922,01 Gio -999900000000;999,9 Go;931,2 Gio;931,23 Gio -1000000000000;1 To;931,3 Gio;931,32 Gio -1088516511498;1,1 To;1013,8 Gio;1013,76 Gio -1099401676613;1,1 To;1023,9 Gio;1023,90 Gio -1099511627776;1,1 To;1 Tio;1 Tio -1500000000000;1,5 To;1,4 Tio;1,36 Tio -1649267441664;1,6 To;1,5 Tio;1,50 Tio -100000000000000;100 To;90,9 Tio;90,95 Tio -112589990684262;112,6 To;102,4 Tio;102,40 Tio -500000000000000;500 To;454,7 Tio;454,75 Tio -562949953421312;562,9 To;512 Tio;512 Tio -990000000000000;990 To;900,4 Tio;900,40 Tio -999900000000000;999,9 To;909,4 Tio;909,40 Tio -1000000000000000;1 Po;909,5 Tio;909,49 Tio -1114640907774197;1,1 Po;1013,8 Tio;1013,76 Tio -1125787316851939;1,1 Po;1023,9 Tio;1023,90 Tio -1125899906842624;1,1 Po;1 Pio;1 Pio -1500000000000000;1,5 Po;1,3 Pio;1,33 Pio -1688849860263936;1,7 Po;1,5 Pio;1,50 Pio -100000000000000000;100 Po;88,8 Pio;88,82 Pio -115292150460684704;115,3 Po;102,4 Pio;102,40 Pio -500000000000000000;500 Po;444,1 Pio;444,09 Pio -576460752303423488;576,5 Po;512 Pio;512 Pio -990000000000000000;990 Po;879,3 Pio;879,30 Pio -999900000000000000;999,9 Po;888,1 Pio;888,09 Pio -1000000000000000000;1000 Po;888,2 Pio;888,18 Pio -1141392289560778496;1141,4 Po;1013,8 Pio;1013,76 Pio -1152806212456386304;1152,8 Po;1023,9 Pio;1023,90 Pio -1152921504606846976;1152,9 Po;1024 Pio;1024 Pio -1500000000000000000;1500 Po;1332,3 Pio;1332,27 Pio -1729382256910270464;1729,4 Po;1536 Pio;1536 Pio From 46d47f6edc238e3c5c99b4ba4da6e50d282328ad Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Fri, 21 Jun 2013 13:30:03 +0200 Subject: [PATCH 20/75] Move duplicated amazon asin regex to const.py --- picard/const.py | 5 ++++- picard/coverart.py | 2 +- picard/mbxml.py | 4 +--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/picard/const.py b/picard/const.py index c36edb276..8006b88f1 100644 --- a/picard/const.py +++ b/picard/const.py @@ -17,7 +17,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -import os, sys +import os, sys, re # Install gettext "noop" function in case const.py gets imported directly. import __builtin__ @@ -43,6 +43,9 @@ FPCALC_NAMES = ['fpcalc', 'pyfpcalc'] # Various Artists MBID VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' +# Amazon asin url +AMAZON_ASIN_URL_REGEX = re.compile(r'^http://(?:www.)?(.*?)(?:\:[0-9]+)?/.*/([0-9B][0-9A-Z]{9})(?:[^0-9A-Z]|$)') + # Release formats RELEASE_FORMATS = { u'CD': N_('CD'), diff --git a/picard/coverart.py b/picard/coverart.py index b2e507ad0..1a571ace4 100644 --- a/picard/coverart.py +++ b/picard/coverart.py @@ -26,6 +26,7 @@ import traceback import picard.webservice from picard import config, log +from picard.const import AMAZON_ASIN_URL_REGEX from picard.metadata import Metadata, is_front_image from picard.util import partial, mimetype from PyQt4.QtCore import QUrl, QObject @@ -81,7 +82,6 @@ AMAZON_SERVER = { } AMAZON_IMAGE_PATH = '/images/P/%s.%s.%sZZZZZZZ.jpg' -AMAZON_ASIN_URL_REGEX = re.compile(r'^http://(?:www.)?(.*?)(?:\:[0-9]+)?/.*/([0-9B][0-9A-Z]{9})(?:[^0-9A-Z]|$)') def _coverart_downloaded(album, metadata, release, try_list, coverinfos, data, http, error): diff --git a/picard/mbxml.py b/picard/mbxml.py index 705690623..bf3405ae5 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -20,11 +20,9 @@ import re from picard import config from picard.util import format_time, translate_from_sortname -from picard.const import RELEASE_FORMATS +from picard.const import RELEASE_FORMATS, AMAZON_ASIN_URL_REGEX -AMAZON_ASIN_URL_REGEX = re.compile(r'^http://(?:www.)?(.*?)(?:\:[0-9]+)?/.*/([0-9B][0-9A-Z]{9})(?:[^0-9A-Z]|$)') - _artist_rel_types = { "composer": "composer", "writer": "writer", From 9446b03d2e3dc4752d35821f5e301136b5f8756a Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 23 Jun 2013 11:49:23 -0500 Subject: [PATCH 21/75] Use config module in ID3Metadata --- picard/formats/id3.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 9304c3233..fe1d3d97c 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -28,7 +28,6 @@ from picard.file import File from picard.formats.mutagenext import compatid3 from picard.util import encode_filename, sanitize_date from urlparse import urlparse -from PyQt4.QtCore import QObject # Ugly, but... I need to save the text in ISO-8859-1 even if it contains @@ -437,14 +436,13 @@ class ID3Metadata(Metadata): def getall(self, name): values = super(ID3Metadata, self).getall(name) - setting = QObject.config.setting - vals = self.__id3v23_date(setting, name, values) + vals = self.__id3v23_date(name, values) # if this is a multi-value field then it needs to be flattened # unless it is TIPL or TMCL which can still be multi-value. - if (setting["write_id3v23"] and len(values)>1 and + if (config.setting["write_id3v23"] and len(values)>1 and not name in ID3File._rtipl_roles and not name.startswith("performer:")): - return [setting["id3v23_join_with"].join(vals)] + return [config.setting["id3v23_join_with"].join(vals)] else: return vals @@ -452,16 +450,15 @@ class ID3Metadata(Metadata): values = dict.get(self, name, None) if not values: return default - setting = QObject.config.setting - vals = self.__id3v23_date(setting, name, values) - if setting["write_id3v23"]: - return setting["id3v23_join_with"].join(vals) + vals = self.__id3v23_date(name, values) + if config.setting["write_id3v23"]: + return config.setting["id3v23_join_with"].join(vals) else: return MULTI_VALUED_JOINER.join(vals) - def __id3v23_date(self, setting, name, values): + def __id3v23_date(self, name, values): # id3v23 can only save TDOR dates in yyyy format (cf. id3v24 and MB who provides dates in yyyy-mm-dd format) - if (setting["write_id3v23"] and name == "originaldate"): + if (config.setting["write_id3v23"] and name == "originaldate"): return [v[:4] for v in values] else: return values From d8b1b79d02af964ad16610a711c8b7136dac272e Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 23 Jun 2013 12:03:32 -0500 Subject: [PATCH 22/75] Add missing import --- picard/formats/id3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index fe1d3d97c..3be35273c 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -23,7 +23,7 @@ import mutagen.trueaudio from collections import defaultdict from mutagen import id3 from picard import config, log -from picard.metadata import Metadata, save_this_image_to_tags +from picard.metadata import Metadata, save_this_image_to_tags, MULTI_VALUED_JOINER from picard.file import File from picard.formats.mutagenext import compatid3 from picard.util import encode_filename, sanitize_date @@ -439,7 +439,7 @@ class ID3Metadata(Metadata): vals = self.__id3v23_date(name, values) # if this is a multi-value field then it needs to be flattened # unless it is TIPL or TMCL which can still be multi-value. - if (config.setting["write_id3v23"] and len(values)>1 and + if (config.setting["write_id3v23"] and len(values) > 1 and not name in ID3File._rtipl_roles and not name.startswith("performer:")): return [config.setting["id3v23_join_with"].join(vals)] From 78df904c776c4bbdcc45f5ed517569366db06f65 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Thu, 27 Jun 2013 18:02:55 +0200 Subject: [PATCH 23/75] Use icons for counters in status bar --- picard/ui/infostatus.py | 81 ++++++++++++++++++ picard/ui/mainwindow.py | 20 ++--- picard/ui/ui_infostatus.py | 101 ++++++++++++++++++++++ ui/infostatus.ui | 167 +++++++++++++++++++++++++++++++++++++ 4 files changed, 356 insertions(+), 13 deletions(-) create mode 100644 picard/ui/infostatus.py create mode 100644 picard/ui/ui_infostatus.py create mode 100644 ui/infostatus.ui diff --git a/picard/ui/infostatus.py b/picard/ui/infostatus.py new file mode 100644 index 000000000..73c827152 --- /dev/null +++ b/picard/ui/infostatus.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2008 Philipp Wolfer +# +# 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 PyQt4.QtGui import QIcon +from picard import config +from picard.util import icontheme +from picard.ui.ui_infostatus import Ui_InfoStatus + + +class InfoStatus(QtGui.QWidget, Ui_InfoStatus): + + def __init__(self, parent): + QtGui.QWidget.__init__(self, parent) + Ui_InfoStatus.__init__(self) + self.setupUi(self) + + self._size = QtCore.QSize(16, 16) + self._create_icons() + self._init_labels() + + def _init_labels(self): + size = self._size + self.label1.setPixmap(self.icon_file.pixmap(size)) + self.label2.setPixmap(self.icon_cd.pixmap(size)) + self.label3.setPixmap(self.icon_file_pending.pixmap(size)) + self.label4.setPixmap(self.icon_web.pixmap(size, QIcon.Disabled)) + self._init_tooltips() + + def _create_icons(self): + self.icon_cd = icontheme.lookup('media-optical') + self.icon_file = QtGui.QIcon(":/images/file.png") + self.icon_file_pending = QtGui.QIcon(":/images/file-pending.png") + self.icon_web = icontheme.lookup('lookup-musicbrainz') + + def _init_tooltips(self): + t1 = _("Files") + t2 = _("Albums") + t3 = _("Pending files") + t4 = _("Pending requests") + self.val1.setToolTip(t1) + self.label1.setToolTip(t1) + self.val2.setToolTip(t2) + self.label2.setToolTip(t2) + self.val3.setToolTip(t3) + self.label3.setToolTip(t3) + self.val4.setToolTip(t4) + self.label4.setToolTip(t4) + + def setFiles(self, num): + self.val1.setText(unicode(num)) + + def setAlbums(self, num): + self.val2.setText(unicode(num)) + + def setPendingFiles(self, num): + self.val3.setText(unicode(num)) + + def setPendingRequests(self, num): + if num <= 0: + enabled = QIcon.Disabled + else: + enabled = QIcon.Normal + self.label4.setPixmap(self.icon_web.pixmap(self._size, enabled)) + self.val4.setText(unicode(num)) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index bb8332581..95bd3fe08 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -34,6 +34,7 @@ from picard.ui.filebrowser import FileBrowser from picard.ui.tagsfromfilenames import TagsFromFileNamesDialog from picard.ui.options.dialog import OptionsDialog from picard.ui.infodialog import FileInfoDialog, AlbumInfoDialog +from picard.ui.infostatus import InfoStatus from picard.ui.passworddialog import PasswordDialog from picard.util import icontheme, webbrowser2, find_existing_path from picard.util.cdrom import get_cdrom_drives @@ -208,12 +209,12 @@ class MainWindow(QtGui.QMainWindow): def create_statusbar(self): """Creates a new status bar.""" self.statusBar().showMessage(_("Ready")) - self.tagger_counts_label = QtGui.QLabel() + self.infostatus = InfoStatus(self) self.listening_label = QtGui.QLabel() self.listening_label.setVisible(False) self.listening_label.setToolTip(_("Picard listens on a port to integrate with your browser and downloads release" " information when you click the \"Tagger\" buttons on the MusicBrainz website")) - self.statusBar().addPermanentWidget(self.tagger_counts_label) + self.statusBar().addPermanentWidget(self.infostatus) self.statusBar().addPermanentWidget(self.listening_label) self.tagger.tagger_stats_changed.connect(self.update_statusbar_stats) self.tagger.listen_port_changed.connect(self.update_statusbar_listen_port) @@ -221,17 +222,10 @@ class MainWindow(QtGui.QMainWindow): def update_statusbar_stats(self): """Updates the status bar information.""" - self.tagger_counts_label.setText(_( - " Files: %(files)d, " - "Albums: %(albums)d, " - "Pending files: %(pfiles)d, " - "Pending web lookups: %(web)d ") - % { - "files": len(self.tagger.files), - "pfiles": File.num_pending_files, - "albums": len(self.tagger.albums), - "web": self.tagger.xmlws.num_pending_web_requests, - }) + self.infostatus.setFiles(len(self.tagger.files)) + self.infostatus.setAlbums(len(self.tagger.albums)) + self.infostatus.setPendingFiles(File.num_pending_files) + self.infostatus.setPendingRequests(self.tagger.xmlws.num_pending_web_requests) def update_statusbar_listen_port(self, listen_port): self.listening_label.setVisible(True) diff --git a/picard/ui/ui_infostatus.py b/picard/ui/ui_infostatus.py new file mode 100644 index 000000000..9f21705da --- /dev/null +++ b/picard/ui/ui_infostatus.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui/infostatus.ui' +# +# Created: Thu Jun 27 18:02:21 2013 +# by: PyQt4 UI code generator 4.9.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_InfoStatus(object): + def setupUi(self, InfoStatus): + InfoStatus.setObjectName(_fromUtf8("InfoStatus")) + InfoStatus.resize(350, 24) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(InfoStatus.sizePolicy().hasHeightForWidth()) + InfoStatus.setSizePolicy(sizePolicy) + InfoStatus.setMinimumSize(QtCore.QSize(0, 0)) + self.horizontalLayout = QtGui.QHBoxLayout(InfoStatus) + self.horizontalLayout.setSpacing(2) + self.horizontalLayout.setMargin(0) + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.val1 = QtGui.QLabel(InfoStatus) + self.val1.setMinimumSize(QtCore.QSize(50, 0)) + self.val1.setText(_fromUtf8("")) + self.val1.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.val1.setObjectName(_fromUtf8("val1")) + self.horizontalLayout.addWidget(self.val1) + self.label1 = QtGui.QLabel(InfoStatus) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label1.sizePolicy().hasHeightForWidth()) + self.label1.setSizePolicy(sizePolicy) + self.label1.setFrameShape(QtGui.QFrame.NoFrame) + self.label1.setTextFormat(QtCore.Qt.AutoText) + self.label1.setScaledContents(False) + self.label1.setMargin(1) + self.label1.setObjectName(_fromUtf8("label1")) + self.horizontalLayout.addWidget(self.label1) + self.val2 = QtGui.QLabel(InfoStatus) + self.val2.setMinimumSize(QtCore.QSize(50, 0)) + self.val2.setText(_fromUtf8("")) + self.val2.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.val2.setObjectName(_fromUtf8("val2")) + self.horizontalLayout.addWidget(self.val2) + self.label2 = QtGui.QLabel(InfoStatus) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label2.sizePolicy().hasHeightForWidth()) + self.label2.setSizePolicy(sizePolicy) + self.label2.setText(_fromUtf8("")) + self.label2.setObjectName(_fromUtf8("label2")) + self.horizontalLayout.addWidget(self.label2) + self.val3 = QtGui.QLabel(InfoStatus) + self.val3.setMinimumSize(QtCore.QSize(50, 0)) + self.val3.setText(_fromUtf8("")) + self.val3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.val3.setObjectName(_fromUtf8("val3")) + self.horizontalLayout.addWidget(self.val3) + self.label3 = QtGui.QLabel(InfoStatus) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label3.sizePolicy().hasHeightForWidth()) + self.label3.setSizePolicy(sizePolicy) + self.label3.setText(_fromUtf8("")) + self.label3.setObjectName(_fromUtf8("label3")) + self.horizontalLayout.addWidget(self.label3) + self.val4 = QtGui.QLabel(InfoStatus) + self.val4.setMinimumSize(QtCore.QSize(50, 0)) + self.val4.setText(_fromUtf8("")) + self.val4.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.val4.setObjectName(_fromUtf8("val4")) + self.horizontalLayout.addWidget(self.val4) + self.label4 = QtGui.QLabel(InfoStatus) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label4.sizePolicy().hasHeightForWidth()) + self.label4.setSizePolicy(sizePolicy) + self.label4.setText(_fromUtf8("")) + self.label4.setScaledContents(False) + self.label4.setObjectName(_fromUtf8("label4")) + self.horizontalLayout.addWidget(self.label4) + + self.retranslateUi(InfoStatus) + QtCore.QMetaObject.connectSlotsByName(InfoStatus) + + def retranslateUi(self, InfoStatus): + InfoStatus.setWindowTitle(_("Form")) + diff --git a/ui/infostatus.ui b/ui/infostatus.ui new file mode 100644 index 000000000..4cfb5d165 --- /dev/null +++ b/ui/infostatus.ui @@ -0,0 +1,167 @@ + + + InfoStatus + + + + 0 + 0 + 350 + 24 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Form + + + + 2 + + + 0 + + + + + + 50 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + Qt::AutoText + + + false + + + 1 + + + + + + + + 50 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + + + + 50 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + + + + 50 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + false + + + + + + + + From 559ec4e74ba47ce50d8ecdfde52ac57cbebea6fd Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Thu, 27 Jun 2013 18:17:42 +0200 Subject: [PATCH 24/75] Include active requests in the pending requests count. Counter was 0 if request queue was empty but Picard was still waiting for replies, with this change, counter is set to 0 only if no active request left. --- picard/ui/mainwindow.py | 4 +++- picard/webservice.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 95bd3fe08..1c4b05c0b 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -225,7 +225,9 @@ class MainWindow(QtGui.QMainWindow): self.infostatus.setFiles(len(self.tagger.files)) self.infostatus.setAlbums(len(self.tagger.albums)) self.infostatus.setPendingFiles(File.num_pending_files) - self.infostatus.setPendingRequests(self.tagger.xmlws.num_pending_web_requests) + ws = self.tagger.xmlws + self.infostatus.setPendingRequests(max(ws.num_pending_web_requests, + ws.num_active_requests)) def update_statusbar_listen_port(self, listen_port): self.listening_label.setVisible(True) diff --git a/picard/webservice.py b/picard/webservice.py index a78b2bc32..b734da840 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -139,6 +139,7 @@ class XmlWebService(QtCore.QObject): "DELETE": self.manager.deleteResource } self.num_pending_web_requests = 0 + self.num_active_requests = 0 def set_cache(self, cache_size_in_mb=100): cache = QtNetwork.QNetworkDiskCache() @@ -182,6 +183,7 @@ class XmlWebService(QtCore.QObject): key = (host, port) self._last_request_times[key] = time.time() self._active_requests[reply] = (request, handler, xml) + self.num_active_requests += 1 return True @staticmethod @@ -195,6 +197,7 @@ class XmlWebService(QtCore.QObject): leftUrl.toString(QUrl.RemovePort) == rightUrl.toString(QUrl.RemovePort) def _process_reply(self, reply): + self.num_active_requests -= 1 try: request, handler, xml = self._active_requests.pop(reply) except KeyError: From 4aa4cd411e7a6e4059e720e844600effbc7139c8 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Thu, 27 Jun 2013 19:18:59 +0200 Subject: [PATCH 25/75] Reduce spaces between counters. --- picard/ui/ui_infostatus.py | 10 +++++----- ui/infostatus.ui | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/picard/ui/ui_infostatus.py b/picard/ui/ui_infostatus.py index 9f21705da..92dc6c4e2 100644 --- a/picard/ui/ui_infostatus.py +++ b/picard/ui/ui_infostatus.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'ui/infostatus.ui' # -# Created: Thu Jun 27 18:02:21 2013 +# Created: Thu Jun 27 19:18:14 2013 # by: PyQt4 UI code generator 4.9.3 # # WARNING! All changes made in this file will be lost! @@ -29,7 +29,7 @@ class Ui_InfoStatus(object): self.horizontalLayout.setMargin(0) self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) self.val1 = QtGui.QLabel(InfoStatus) - self.val1.setMinimumSize(QtCore.QSize(50, 0)) + self.val1.setMinimumSize(QtCore.QSize(40, 0)) self.val1.setText(_fromUtf8("")) self.val1.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.val1.setObjectName(_fromUtf8("val1")) @@ -47,7 +47,7 @@ class Ui_InfoStatus(object): self.label1.setObjectName(_fromUtf8("label1")) self.horizontalLayout.addWidget(self.label1) self.val2 = QtGui.QLabel(InfoStatus) - self.val2.setMinimumSize(QtCore.QSize(50, 0)) + self.val2.setMinimumSize(QtCore.QSize(40, 0)) self.val2.setText(_fromUtf8("")) self.val2.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.val2.setObjectName(_fromUtf8("val2")) @@ -62,7 +62,7 @@ class Ui_InfoStatus(object): self.label2.setObjectName(_fromUtf8("label2")) self.horizontalLayout.addWidget(self.label2) self.val3 = QtGui.QLabel(InfoStatus) - self.val3.setMinimumSize(QtCore.QSize(50, 0)) + self.val3.setMinimumSize(QtCore.QSize(40, 0)) self.val3.setText(_fromUtf8("")) self.val3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.val3.setObjectName(_fromUtf8("val3")) @@ -77,7 +77,7 @@ class Ui_InfoStatus(object): self.label3.setObjectName(_fromUtf8("label3")) self.horizontalLayout.addWidget(self.label3) self.val4 = QtGui.QLabel(InfoStatus) - self.val4.setMinimumSize(QtCore.QSize(50, 0)) + self.val4.setMinimumSize(QtCore.QSize(40, 0)) self.val4.setText(_fromUtf8("")) self.val4.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.val4.setObjectName(_fromUtf8("val4")) diff --git a/ui/infostatus.ui b/ui/infostatus.ui index 4cfb5d165..433e86b02 100644 --- a/ui/infostatus.ui +++ b/ui/infostatus.ui @@ -36,7 +36,7 @@ - 50 + 40 0 @@ -74,7 +74,7 @@ - 50 + 40 0 @@ -103,7 +103,7 @@ - 50 + 40 0 @@ -132,7 +132,7 @@ - 50 + 40 0 From d2baf4449dc55d308fd5259d549643051e23e0a8 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Thu, 27 Jun 2013 21:26:11 +0200 Subject: [PATCH 26/75] Fix a TypeError, str vs unicode. E: 139836956702464 21:25:09 Traceback (most recent call last): File "./picard/album.py", line 133, in _parse_release run_album_metadata_processors(self, m, release_node) File "./picard/metadata.py", line 334, in run_album_metadata_processors processor(tagger, metadata, release) File "./picard/coverart.py", line 178, in coverart config.setting["caa_image_types"].split()) TypeError: descriptor 'lower' requires a 'unicode' object but received a 'str' --- picard/ui/options/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/options/cover.py b/picard/ui/options/cover.py index ea521ffda..ae7c6fe2a 100644 --- a/picard/ui/options/cover.py +++ b/picard/ui/options/cover.py @@ -83,7 +83,7 @@ class CoverOptionsPage(OptionsPage): config.BoolOption("setting", "caa_approved_only", False), config.BoolOption("setting", "caa_image_type_as_filename", False), config.IntOption("setting", "caa_image_size", 2), - config.TextOption("setting", "caa_image_types", "front"), + config.TextOption("setting", "caa_image_types", u"front"), ] def __init__(self, parent=None): From 22d99966dc4ede49814918ce3c1c843af98b612a Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Fri, 28 Jun 2013 12:31:58 +0200 Subject: [PATCH 27/75] Move source images (.psd, .svg) from images to img-src directory --- resources/{images => img-src}/analyze.psd | Bin resources/{images => img-src}/remove.psd | Bin resources/{images => img-src}/tag.svg | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename resources/{images => img-src}/analyze.psd (100%) rename resources/{images => img-src}/remove.psd (100%) rename resources/{images => img-src}/tag.svg (100%) diff --git a/resources/images/analyze.psd b/resources/img-src/analyze.psd similarity index 100% rename from resources/images/analyze.psd rename to resources/img-src/analyze.psd diff --git a/resources/images/remove.psd b/resources/img-src/remove.psd similarity index 100% rename from resources/images/remove.psd rename to resources/img-src/remove.psd diff --git a/resources/images/tag.svg b/resources/img-src/tag.svg similarity index 100% rename from resources/images/tag.svg rename to resources/img-src/tag.svg From 5f5afa7d021a71ca81a78774f7dede8f69bd5089 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Fri, 28 Jun 2013 12:32:52 +0200 Subject: [PATCH 28/75] Add script to generate picard.qrc from png images in images directory --- resources/makeqrc.py | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 resources/makeqrc.py diff --git a/resources/makeqrc.py b/resources/makeqrc.py new file mode 100644 index 000000000..c9aa8614e --- /dev/null +++ b/resources/makeqrc.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +import os, fnmatch +import re +from distutils import log +from distutils.dep_util import newer + +"""Build a Qt resources file with all png images found under images/ +It will update qrc file only if images newer than it are found +""" + +def tryint(s): + try: + return int(s) + except: + return s + +def natsort_key(s): + return [ tryint(c) for c in re.split('(\d+)', s) ] + + +def find_files(topdir, directory, pattern): + tdir = os.path.join(topdir, directory) + for root, dirs, files in os.walk(tdir): + for basename in files: + if fnmatch.fnmatch(basename, pattern): + filepath = os.path.join(root, basename) + filename = os.path.relpath(filepath, topdir) + yield filename + +def main(): + scriptdir = os.path.dirname(os.path.abspath(__file__)) + topdir = os.path.abspath(os.path.join(scriptdir, "..")) + resourcesdir = os.path.join(topdir, "resources") + qrcfile = os.path.join(resourcesdir, "picard.qrc") + images = [i for i in find_files(resourcesdir, 'images', '*.png')] + newimages = 0 + for filename in images: + filepath = os.path.join(resourcesdir, filename) + if newer(filepath, qrcfile): + newimages += 1 + if newimages: + log.info("%d images newer than %s found" % (newimages, qrcfile)) + with open(qrcfile, 'wb+') as f: + f.write('\r\n\r\n') + n = 0 + for filename in sorted(images, key=natsort_key): + f.write(' %s\r\n' % filename) + n += 1 + f.write('\r\n\r\n') + log.info("File %s written, %d images" % (qrcfile, n)) + + +if __name__ == "__main__": + log.set_verbosity(1) + main() From b974a9cf6354dd7c9b40e1bfad3a5d4cef999883 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Fri, 28 Jun 2013 12:33:41 +0200 Subject: [PATCH 29/75] Make compile.py runnable from anywhere and importable. --- resources/compile.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/resources/compile.py b/resources/compile.py index 04a98849e..2dc8a3bf3 100644 --- a/resources/compile.py +++ b/resources/compile.py @@ -1,7 +1,18 @@ #!/usr/bin/env python import os.path +from distutils import log +from distutils.dep_util import newer -pyfile = os.path.join("..", "picard", "resources.py") -os.system("pyrcc4 picard.qrc -o %s" % pyfile) +def main(): + scriptdir = os.path.dirname(os.path.abspath(__file__)) + topdir = os.path.abspath(os.path.join(scriptdir, "..")) + pyfile = os.path.join(topdir, "picard", "resources.py") + qrcfile = os.path.join(topdir, "resources", "picard.qrc") + if newer(qrcfile, pyfile): + log.info("compiling %s -> %s", qrcfile, pyfile) + os.system("pyrcc4 %s -o %s" % (qrcfile, pyfile)) +if __name__ == "__main__": + log.set_verbosity(1) + main() From 26f595a068288a2610e957be3f5765b2fbd78c94 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Fri, 28 Jun 2013 12:35:40 +0200 Subject: [PATCH 30/75] Mark compile.py and makeqrc.py as executables, they have python bang path. --- resources/compile.py | 0 resources/makeqrc.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 resources/compile.py mode change 100644 => 100755 resources/makeqrc.py diff --git a/resources/compile.py b/resources/compile.py old mode 100644 new mode 100755 diff --git a/resources/makeqrc.py b/resources/makeqrc.py old mode 100644 new mode 100755 From 746fa31286b3d403bceded45f256a209b3d53cb1 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Fri, 28 Jun 2013 12:37:05 +0200 Subject: [PATCH 31/75] Use resources makeqrc and compile code, reducing code redundancy. An empty __init__.py file was added so functions can be imported. --- resources/__init__.py | 0 setup.py | 15 +++------------ 2 files changed, 3 insertions(+), 12 deletions(-) create mode 100644 resources/__init__.py diff --git a/resources/__init__.py b/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/setup.py b/setup.py index 8a0aae97e..5bba12dfc 100755 --- a/setup.py +++ b/setup.py @@ -275,18 +275,9 @@ class picard_build_ui(Command): f = open(pyfile, "w") f.write(source) f.close() - qrcfile = os.path.join("resources", "picard.qrc") - pyfile = os.path.join("picard", "resources.py") - build_resources = False - if newer("resources/picard.qrc", pyfile): - build_resources = True - for datafile in glob.glob("resources/images/*.*"): - if newer(datafile, pyfile): - build_resources = True - break - if build_resources: - log.info("compiling %s -> %s", qrcfile, pyfile) - os.system("pyrcc4 %s -o %s" % (qrcfile, pyfile)) + from resources import compile, makeqrc + makeqrc.main() + compile.main() class picard_clean_ui(Command): From 0661491f373025481e28f7013858f03143ee2307 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Sun, 30 Jun 2013 05:02:45 +0200 Subject: [PATCH 32/75] Use len() instead of a counter. --- resources/makeqrc.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/makeqrc.py b/resources/makeqrc.py index c9aa8614e..8b3f90ffe 100755 --- a/resources/makeqrc.py +++ b/resources/makeqrc.py @@ -43,12 +43,10 @@ def main(): log.info("%d images newer than %s found" % (newimages, qrcfile)) with open(qrcfile, 'wb+') as f: f.write('\r\n\r\n') - n = 0 for filename in sorted(images, key=natsort_key): f.write(' %s\r\n' % filename) - n += 1 f.write('\r\n\r\n') - log.info("File %s written, %d images" % (qrcfile, n)) + log.info("File %s written, %d images" % (qrcfile, len(images))) if __name__ == "__main__": From c8af60e21a5b3498c1e64feba06b234f7f2ca569 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Sun, 30 Jun 2013 05:04:47 +0200 Subject: [PATCH 33/75] Use unix eol in generated ressource file. --- resources/makeqrc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/makeqrc.py b/resources/makeqrc.py index 8b3f90ffe..d6d0635fa 100755 --- a/resources/makeqrc.py +++ b/resources/makeqrc.py @@ -42,10 +42,10 @@ def main(): if newimages: log.info("%d images newer than %s found" % (newimages, qrcfile)) with open(qrcfile, 'wb+') as f: - f.write('\r\n\r\n') + f.write('\n\n') for filename in sorted(images, key=natsort_key): - f.write(' %s\r\n' % filename) - f.write('\r\n\r\n') + f.write(' %s\n' % filename) + f.write('\n\n') log.info("File %s written, %d images" % (qrcfile, len(images))) From f8924449988f7960f5e38fa5fa8580889eca32fe Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Sun, 30 Jun 2013 17:25:15 +0200 Subject: [PATCH 34/75] Create a function to parse amazon urls and reduce code redundancy. A test for amazon url parsing was added. --- picard/const.py | 3 --- picard/coverart.py | 17 +++++++---------- picard/mbxml.py | 15 +++++++-------- picard/util/__init__.py | 11 +++++++++++ test/test_amazon_urls.py | 39 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 test/test_amazon_urls.py diff --git a/picard/const.py b/picard/const.py index 8006b88f1..399a32188 100644 --- a/picard/const.py +++ b/picard/const.py @@ -43,9 +43,6 @@ FPCALC_NAMES = ['fpcalc', 'pyfpcalc'] # Various Artists MBID VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' -# Amazon asin url -AMAZON_ASIN_URL_REGEX = re.compile(r'^http://(?:www.)?(.*?)(?:\:[0-9]+)?/.*/([0-9B][0-9A-Z]{9})(?:[^0-9A-Z]|$)') - # Release formats RELEASE_FORMATS = { u'CD': N_('CD'), diff --git a/picard/coverart.py b/picard/coverart.py index 1a571ace4..c2c37001a 100644 --- a/picard/coverart.py +++ b/picard/coverart.py @@ -26,9 +26,8 @@ import traceback import picard.webservice from picard import config, log -from picard.const import AMAZON_ASIN_URL_REGEX from picard.metadata import Metadata, is_front_image -from picard.util import partial, mimetype +from picard.util import partial, mimetype, parse_amazon_url from PyQt4.QtCore import QUrl, QObject # data transliterated from the perl stuff used to find cover art for the @@ -282,17 +281,15 @@ def _process_url_relation(try_list, relation): return False def _process_asin_relation(try_list, relation): - match = AMAZON_ASIN_URL_REGEX.match(relation.target[0].text) - if match is not None: - asinHost = match.group(1) - asin = match.group(2) - if asinHost in AMAZON_SERVER: - serverInfo = AMAZON_SERVER[asinHost] + amz = parse_amazon_url(relation.target[0].text) + if amz is not None: + if amz['host'] in AMAZON_SERVER: + serverInfo = AMAZON_SERVER[amz['host']] else: serverInfo = AMAZON_SERVER['amazon.com'] host = serverInfo['server'] - path_l = AMAZON_IMAGE_PATH % (asin, serverInfo['id'], 'L') - path_m = AMAZON_IMAGE_PATH % (asin, serverInfo['id'], 'M') + path_l = AMAZON_IMAGE_PATH % (amz['asin'], serverInfo['id'], 'L') + path_m = AMAZON_IMAGE_PATH % (amz['asin'], serverInfo['id'], 'M') _try_list_append_image_url(try_list, QUrl("http://%s:%s" % (host, path_l))) _try_list_append_image_url(try_list, QUrl("http://%s:%s" % (host, path_m))) diff --git a/picard/mbxml.py b/picard/mbxml.py index bf3405ae5..bb08b91c2 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -19,8 +19,8 @@ import re from picard import config -from picard.util import format_time, translate_from_sortname -from picard.const import RELEASE_FORMATS, AMAZON_ASIN_URL_REGEX +from picard.util import format_time, translate_from_sortname, parse_amazon_url +from picard.const import RELEASE_FORMATS _artist_rel_types = { @@ -98,12 +98,11 @@ def _relations_to_metadata(relation_lists, m): work_to_metadata(relation.work[0], m) elif relation_list.target_type == 'url': for relation in relation_list.relation: - if relation.type == 'amazon asin': - url = relation.target[0].text - match = AMAZON_ASIN_URL_REGEX.match(url) - if match is not None and 'asin' not in m: - m['asin'] = match.group(2) - if relation.type == 'license': + if relation.type == 'amazon asin' and 'asin' not in m: + amz = parse_amazon_url(relation.target[0].text) + if amz is not None: + m['asin'] = amz['asin'] + elif relation.type == 'license': url = relation.target[0].text m.add('license', url) diff --git a/picard/util/__init__.py b/picard/util/__init__.py index 19495e4f5..8851e0113 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -325,3 +325,14 @@ def load_release_type_scores(setting): def save_release_type_scores(scores): return " ".join(["%s %.2f" % v for v in scores.iteritems()]) + + +def parse_amazon_url(url): + """Extract host and asin from an amazon url. + It returns a dict with host and asin keys on success, None else + """ + r = re.compile(r'^http://(?:www.)?(?P.*?)(?:\:[0-9]+)?/.*/(?P[0-9B][0-9A-Z]{9})(?:[^0-9A-Z]|$)') + match = r.match(url) + if match is not None: + return match.groupdict() + return None diff --git a/test/test_amazon_urls.py b/test/test_amazon_urls.py new file mode 100644 index 000000000..de19306b6 --- /dev/null +++ b/test/test_amazon_urls.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +import unittest +from picard.util import parse_amazon_url + + +class ParseAmazonUrlTest(unittest.TestCase): + + def test_1(self): + url = 'http://www.amazon.com/dp/020530902X' + expected = {'asin': '020530902X', 'host': 'amazon.com'} + r = parse_amazon_url(url) + self.failUnlessEqual(r, expected) + + def test_2(self): + url = 'http://ec1.amazon.co.jp/gp/product/020530902X' + expected = {'asin': '020530902X', 'host': 'ec1.amazon.co.jp'} + r = parse_amazon_url(url) + self.failUnlessEqual(r, expected) + + def test_3(self): + url = 'http://amazon.com/Dark-Side-Moon-Pink-Floyd/dp/B004ZN9RWK/ref=sr_1_1?s=music&ie=UTF8&qid=1372605047&sr=1-1&keywords=pink+floyd+dark+side+of+the+moon' + expected = {'asin': 'B004ZN9RWK', 'host': 'amazon.com'} + r = parse_amazon_url(url) + self.failUnlessEqual(r, expected) + + def test_4(self): + #incorrect ASIN + url = 'http://www.amazon.com/dp/A20530902X' + expected = None + r = parse_amazon_url(url) + self.failUnlessEqual(r, expected) + + def test_5(self): + #incorrect ASIN + url = 'http://www.amazon.com/dp/020530902x' + expected = None + r = parse_amazon_url(url) + self.failUnlessEqual(r, expected) From 748502419a00d3fd654a32d18f88bec30037841a Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Sun, 30 Jun 2013 21:12:54 +0200 Subject: [PATCH 35/75] Remove incorrect author from header. --- picard/ui/infostatus.py | 1 - 1 file changed, 1 deletion(-) diff --git a/picard/ui/infostatus.py b/picard/ui/infostatus.py index 73c827152..3c8fbad9b 100644 --- a/picard/ui/infostatus.py +++ b/picard/ui/infostatus.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # # Picard, the next-generation MusicBrainz tagger -# Copyright (C) 2008 Philipp Wolfer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License From 1802208d6904cd609c65a43492c9e0bb0adc1777 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Sun, 30 Jun 2013 22:01:21 +0200 Subject: [PATCH 36/75] Use a green down arrow as download icon instead of globe. In 16x16 globe is too complex, arrow is much more legible. --- picard/resources.py | 739 +++++++++++-------- picard/ui/infostatus.py | 6 +- resources/images/16x16/action-go-down-16.png | Bin 0 -> 683 bytes resources/picard.qrc | 61 +- 4 files changed, 454 insertions(+), 352 deletions(-) create mode 100644 resources/images/16x16/action-go-down-16.png diff --git a/picard/resources.py b/picard/resources.py index b96677568..00a7d37db 100644 --- a/picard/resources.py +++ b/picard/resources.py @@ -2,8 +2,8 @@ # Resource object code # -# Created: Mon 3. Jun 11:55:41 2013 -# by: The Resource Compiler for PyQt (Qt v4.8.4) +# Created: dim. juin 30 21:56:31 2013 +# by: The Resource Compiler for PyQt (Qt v4.8.2) # # WARNING! All changes made in this file will be lost! @@ -36,123 +36,6 @@ qt_resource_data = "\ \x9b\x3c\x71\x95\x28\x7f\xf3\x9d\xed\x85\x7d\x96\xfa\xc9\xc9\x79\ \xf7\x20\x68\x4d\x1f\x7a\xed\x1f\x2d\x49\x58\xba\x4e\x19\x3c\x81\ \x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x07\x22\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ -\x00\x00\x03\x1e\x69\x43\x43\x50\x49\x43\x43\x20\x50\x72\x6f\x66\ -\x69\x6c\x65\x00\x00\x78\x01\x85\x54\xdf\x6b\xd3\x50\x14\xfe\xda\ -\x65\x9d\xb0\xe1\x8b\x3a\x67\x11\x09\x3e\x68\x91\x6e\x64\x53\x74\ -\x43\x9c\xb6\x6b\x57\xba\xcd\x5a\xea\x36\xb7\x21\x48\x9b\xa6\x6d\ -\x5c\x9a\xc6\x24\xed\x7e\xb0\x07\xd9\x8b\x6f\x3a\xc5\x77\xf1\x07\ -\x3e\xf9\x07\x0c\xd9\x83\x6f\x7b\x92\x0d\xc6\x14\x61\xf8\xac\x88\ -\x22\x4c\xf6\x22\xb3\x9e\x9b\x34\x4d\x27\x53\x03\xb9\xf7\xbb\xdf\ -\xf9\xee\x39\x27\xe7\xe4\x5e\xa0\xf9\x71\x5a\xd3\x14\x2f\x0f\x14\ -\x55\x53\x4f\xc5\xc2\xfc\xc4\xe4\x14\xdf\xf2\x01\x5e\x1c\x43\x2b\ -\xfc\x68\x4d\x8b\x86\x16\x4a\x26\x47\x40\x0f\xd3\xb2\x79\xef\xb3\ -\xf3\x0e\x1e\xc6\x6c\x74\xee\x6f\xdf\xab\xfe\x63\xd5\x9a\x95\x0c\ -\x11\xf0\x1c\x20\xbe\x94\x35\xc4\x22\xe1\x59\xa0\x69\x5c\xd4\x74\ -\x13\xe0\xd6\x89\xef\x9d\x31\x35\xc2\xcd\x4c\x73\x58\xa7\x04\x09\ -\x1f\x67\x38\x6f\x63\x81\xe1\x8c\x8d\x23\x96\x66\x34\x35\x40\x9a\ -\x09\xc2\x07\xc5\x42\x3a\x4b\xb8\x40\x38\x98\x69\xe0\xf3\x0d\xd8\ -\xce\x81\x14\xe4\x27\x26\xa9\x92\x2e\x8b\x3c\xab\x45\x52\x2f\xe5\ -\x64\x45\xb2\x0c\xf6\xf0\x1f\x73\x83\xf2\x5f\xb0\xa8\x94\xe9\x9b\ -\xad\xe7\x10\x8d\x6d\x9a\x19\x4e\xd1\x7c\x8a\xde\x1f\x39\x7d\x70\ -\x8c\xe6\x00\xd5\xc1\x3f\x5f\x18\xbd\x41\xb8\x9d\x70\x58\x36\xe3\ -\xa3\x35\x7e\x42\xcd\x24\xae\x11\x26\xbd\xe7\xee\x74\x69\x98\xed\ -\x65\x9a\x97\x59\x29\x12\x25\x1c\x24\xbc\x62\x54\xae\x33\x6c\x69\ -\xe6\x0b\x03\x89\x9a\xe6\xd3\xed\xf4\x50\x92\xb0\x9f\x34\xbf\x34\ -\x33\x59\xf3\xe3\xed\x50\x95\x04\xeb\x31\xc5\xf5\xf6\x4b\x46\xf4\ -\xba\xbd\xd7\xdb\x91\x93\x07\xe3\x35\x3e\xa7\x29\xd6\x7f\x40\xfe\ -\xbd\xf7\xf5\x72\x8a\xe5\x79\x92\xf0\xeb\xb4\x1e\x8d\xd5\xf4\x5b\ -\x92\x3a\x56\xdb\xdb\xe4\xcd\xa6\x23\xc3\xc4\x77\x51\x3f\x03\x48\ -\x42\x82\x8e\x1c\x64\x28\xe0\x91\x42\x0c\x61\x9a\x63\xc4\xaa\xf8\ -\x4c\x16\x19\x22\x4a\xa4\xd2\x69\x74\x54\x79\xb2\x38\xd6\x3b\x28\ -\x93\x96\xed\x1c\x47\x78\xc9\x5f\x0e\xb8\x5e\x16\xf5\x5b\xb2\xb8\ -\xf6\xe0\xfb\x9e\xdd\x25\xd7\x8e\xbc\x15\x85\xc5\xb7\xa3\xd8\x51\ -\xed\xb5\x81\xe9\xba\xb2\x13\x9a\x1b\x7f\x75\x61\xa5\xa3\x6e\xe1\ -\x37\xb9\xe5\x9b\x1b\x6d\xab\x0b\x08\x51\xfe\x8a\xe5\xb1\x48\x5e\ -\x65\xca\x4f\x82\x51\xd7\x75\x36\xe6\x90\x53\x97\xfc\x75\x0b\xcf\ -\x32\x94\xee\x25\x76\x12\x58\x0c\xba\xac\xf0\x5e\xf8\x2a\x6c\x0a\ -\x4f\x85\x17\xc2\x97\xbf\xd4\xc8\xce\xde\xad\x11\xcb\x80\x71\x2c\ -\x3e\xab\x9e\x53\xcd\xc6\xec\x25\xd2\x4c\xd2\xeb\x64\xb8\xbf\x8a\ -\xf5\x42\xc6\x18\xf9\x90\x31\x43\x5a\x9d\xbe\x24\x4d\x9c\x8a\x39\ -\xf2\xda\x50\x0b\x27\x06\x77\x82\xeb\xe6\xe2\x5c\x2f\xd7\x07\x9e\ -\xbb\xcc\x5d\xe1\xfa\xb9\x08\xad\x2e\x72\x23\x8e\xc2\x17\xf5\x45\ -\x7c\x21\xf0\xbe\x33\xbe\x3e\x5f\xb7\x6f\x88\x61\xa7\xdb\xbe\xd3\ -\x64\xeb\xa3\x31\x5a\xeb\xbb\xd3\x91\xba\xa2\xb1\x7a\x94\x8f\xdb\ -\x27\xf6\x3d\x8e\xaa\x13\x19\xb2\xb1\xbe\xb1\x7e\x56\x08\x2b\xb4\ -\xa2\x63\x6a\x4a\xb3\x74\x4f\x00\x03\x25\x6d\x4e\x97\xf3\x05\x93\ -\xef\x11\x84\x0b\x7c\x88\xae\x2d\x89\x8f\xab\x62\x57\x90\x4f\x2b\ -\x0a\x6f\x99\x0c\x5e\x97\x0c\x49\xaf\x48\xd9\x2e\xb0\x3b\x8f\xed\ -\x03\xb6\x53\xd6\x5d\xe6\x69\x5f\x73\x39\xf3\x2a\x70\xe9\x1b\xfd\ -\xc3\xeb\x2e\x37\x55\x06\x5e\x19\xc0\xd1\x73\x2e\x17\xa0\x33\x75\ -\xe4\x09\xb0\x7c\x5e\x2c\xeb\x15\xdb\x1f\x3c\x9e\xb7\x80\x91\x3b\ -\xdb\x63\xad\x3d\x6d\x61\xba\x8b\x3e\x56\xab\xdb\x74\x2e\x5b\x1e\ -\x01\xbb\x0f\xab\xd5\x9f\xcf\xaa\xd5\xdd\xe7\xe4\x7f\x0b\x78\xa3\ -\xfc\x06\xa9\x23\x0a\xd6\xc2\xa1\x5f\x32\x00\x00\x00\x09\x70\x48\ -\x59\x73\x00\x00\x04\x9d\x00\x00\x04\x9d\x01\x7c\x34\x6b\xa1\x00\ -\x00\x01\x64\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\x61\ -\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x78\x3a\ -\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\x6d\x6c\x6e\x73\x3a\x78\x3d\ -\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\x3a\x6d\x65\x74\x61\x2f\x22\ -\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\x22\x58\x4d\x50\x20\x43\x6f\ -\x72\x65\x20\x34\x2e\x34\x2e\x30\x22\x3e\x0a\x20\x20\x20\x3c\x72\ -\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\x73\x3a\x72\x64\x66\ -\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ -\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\x32\x2d\x72\ -\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\x22\x3e\x0a\ -\x20\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\x72\ -\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\ -\x3d\x22\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ -\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ -\x61\x70\x2f\x31\x2e\x30\x2f\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\ -\x20\x20\x20\x3c\x78\x6d\x70\x3a\x43\x72\x65\x61\x74\x6f\x72\x54\ -\x6f\x6f\x6c\x3e\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\ -\x2e\x6f\x72\x67\x3c\x2f\x78\x6d\x70\x3a\x43\x72\x65\x61\x74\x6f\ -\x72\x54\x6f\x6f\x6c\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x2f\x72\ -\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ -\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\ -\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x3e\x0a\xad\x12\xec\x34\x00\ -\x00\x02\x3a\x49\x44\x41\x54\x38\x11\xa5\x52\xcf\x6b\x13\x41\x14\ -\x7e\xf3\x63\xb3\x9b\x34\xbf\xda\x34\x39\xd8\x42\xa1\x92\x56\x6d\ -\x69\x63\x44\xa9\x91\x50\x44\x2f\x1a\x28\x52\xaa\x3d\x88\xf7\x9a\ -\x93\x9e\xfc\x1f\x14\x4f\xa5\xe8\xc1\x73\x4b\x23\x55\x04\x45\xec\ -\x45\x2a\x08\x16\x63\x52\x53\x45\x8a\x54\x4b\x3d\x08\x26\x4d\x62\ -\xd3\x68\xb2\x99\x9d\x75\x26\xb0\x25\xc1\x26\x08\xbe\x65\x78\x6f\ -\xde\x7e\xdf\xb7\xef\xdb\x19\x64\x9a\x26\xfc\x4f\xe0\x76\xe4\xb7\ -\x0b\x23\xe3\x72\xb5\xc3\xd0\x76\x2f\x83\xe1\x2b\xb3\x60\x8a\x07\ -\x60\xb4\x25\x4e\x5a\x38\x68\xa5\x13\xa1\x18\x2f\x2e\x33\x5e\x7c\ -\xc6\x64\x7d\x10\x46\xf6\x5a\x5a\xe8\x1b\x9e\xb8\x83\x78\x91\x80\ -\x58\xbd\xc7\x2e\xdc\x6e\x35\x01\x92\x2a\x56\x3c\x9f\x0d\xfa\x5d\ -\x0a\x1d\xb2\x77\x75\x4c\x86\xce\xdd\x8c\x83\x91\x13\x02\x3a\x70\ -\xe4\x31\x3e\xac\xdc\x9f\x2b\x15\x2a\x4b\x25\x4e\xd7\x63\xd7\x33\ -\x05\x8b\x83\x56\xe6\x82\x47\x83\x63\x17\x1f\x77\x06\x8e\xf8\x08\ -\xa6\x4e\xa2\xa9\x1a\x46\x18\x6a\x95\x6f\x60\x1a\x65\x61\x4f\x07\ -\x40\x00\x54\x09\x00\x37\x74\x60\x7a\xa5\x6c\x30\xf6\xb3\xf8\x63\ -\x7b\xe7\xcb\xda\xea\x74\x7d\x82\x8d\xa7\x67\x67\x0e\x9f\xba\x76\ -\x97\xa0\x5d\x07\x17\x24\x83\xfd\x02\xce\x04\x99\x57\x84\x88\x0e\ -\x08\x71\x51\xef\x02\x11\x99\x2a\x2a\x30\x08\x96\xb6\x32\x2f\x6f\ -\x0d\x4e\xbc\xbe\xb7\x6f\x61\xe3\xc9\xd8\x54\x5f\x78\xea\x81\x82\ -\xb3\x1e\x56\xdb\xab\x8b\x18\xb5\x32\x70\x29\xc2\xf2\x80\x4c\x03\ -\xa8\xcd\x06\x06\x1e\xc8\x7f\x5d\x4f\xc5\x47\x2f\xbf\x5b\x94\x36\ -\xf6\x05\xe4\x26\x3d\x3f\x72\x7e\x30\x72\x69\x91\xf0\xad\x2e\x26\ -\xa6\xa8\x55\x4b\x60\x54\x0b\xd2\x81\xf8\x32\x01\xb0\x0d\xe5\xb6\ -\x33\xc9\xab\xc3\xd3\x6b\xcb\x12\x2f\xa3\x49\x40\x36\x0a\x99\x99\ -\x4d\x55\xd9\xec\x67\x35\x61\xc3\x60\x62\x74\x00\x4c\x10\x60\x4c\ -\xe0\x77\xb5\xe7\x73\xe0\x64\x62\x40\xe2\xac\x68\x3a\x46\x71\x0a\ -\x6e\x8c\x4a\xdd\x18\x74\xe1\x17\x81\x62\xb3\x83\x4d\xd3\x80\x12\ -\x0a\x84\x12\x50\x54\xe4\x97\x18\x8b\x2c\x73\xd3\x4d\xf4\xb9\xb5\ -\xa8\xc3\x65\x77\x03\x73\x80\x49\xfc\x95\x7c\x76\x2f\x69\x22\x44\ -\x7c\xdd\xae\x30\xc5\xdf\x55\xa2\x69\x5e\x9f\x53\x3d\x2d\x78\x2f\ -\x2c\x91\xa6\x09\x3c\x01\x6f\x14\xd3\x43\x7a\x2e\xef\x7e\xf5\x69\ -\x35\x15\xeb\x39\xf3\x28\xda\x1b\x59\x8a\x7c\x4c\xa6\x26\xb3\x3b\ -\x9d\x6f\x88\xda\x5f\x73\x7a\x3b\xa2\x16\xb9\x9e\x1b\xaf\xe8\xfb\ -\x87\x27\xe2\x99\xc4\xf1\xf1\xc6\x5e\x63\x9d\x5e\x08\xc5\xd2\xf3\ -\xa1\x1b\x8d\xbd\xbf\x7e\x62\x93\xfa\x3f\x6c\xfe\x00\x30\x2a\x2f\ -\x9b\x6a\x8a\xe5\x15\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ -\x82\ \x00\x00\x40\x8c\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -1188,6 +1071,123 @@ qt_resource_data = "\ \x90\x61\x2b\x02\x00\xc6\x14\xf8\xdb\x5f\xbe\x4a\x7f\x08\x49\x83\ \x2d\xcf\x38\x15\x42\xfc\xff\x17\x69\x09\x93\x93\x7f\x7e\xdc\x00\ \x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x07\x22\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x03\x1e\x69\x43\x43\x50\x49\x43\x43\x20\x50\x72\x6f\x66\ +\x69\x6c\x65\x00\x00\x78\x01\x85\x54\xdf\x6b\xd3\x50\x14\xfe\xda\ +\x65\x9d\xb0\xe1\x8b\x3a\x67\x11\x09\x3e\x68\x91\x6e\x64\x53\x74\ +\x43\x9c\xb6\x6b\x57\xba\xcd\x5a\xea\x36\xb7\x21\x48\x9b\xa6\x6d\ +\x5c\x9a\xc6\x24\xed\x7e\xb0\x07\xd9\x8b\x6f\x3a\xc5\x77\xf1\x07\ +\x3e\xf9\x07\x0c\xd9\x83\x6f\x7b\x92\x0d\xc6\x14\x61\xf8\xac\x88\ +\x22\x4c\xf6\x22\xb3\x9e\x9b\x34\x4d\x27\x53\x03\xb9\xf7\xbb\xdf\ +\xf9\xee\x39\x27\xe7\xe4\x5e\xa0\xf9\x71\x5a\xd3\x14\x2f\x0f\x14\ +\x55\x53\x4f\xc5\xc2\xfc\xc4\xe4\x14\xdf\xf2\x01\x5e\x1c\x43\x2b\ +\xfc\x68\x4d\x8b\x86\x16\x4a\x26\x47\x40\x0f\xd3\xb2\x79\xef\xb3\ +\xf3\x0e\x1e\xc6\x6c\x74\xee\x6f\xdf\xab\xfe\x63\xd5\x9a\x95\x0c\ +\x11\xf0\x1c\x20\xbe\x94\x35\xc4\x22\xe1\x59\xa0\x69\x5c\xd4\x74\ +\x13\xe0\xd6\x89\xef\x9d\x31\x35\xc2\xcd\x4c\x73\x58\xa7\x04\x09\ +\x1f\x67\x38\x6f\x63\x81\xe1\x8c\x8d\x23\x96\x66\x34\x35\x40\x9a\ +\x09\xc2\x07\xc5\x42\x3a\x4b\xb8\x40\x38\x98\x69\xe0\xf3\x0d\xd8\ +\xce\x81\x14\xe4\x27\x26\xa9\x92\x2e\x8b\x3c\xab\x45\x52\x2f\xe5\ +\x64\x45\xb2\x0c\xf6\xf0\x1f\x73\x83\xf2\x5f\xb0\xa8\x94\xe9\x9b\ +\xad\xe7\x10\x8d\x6d\x9a\x19\x4e\xd1\x7c\x8a\xde\x1f\x39\x7d\x70\ +\x8c\xe6\x00\xd5\xc1\x3f\x5f\x18\xbd\x41\xb8\x9d\x70\x58\x36\xe3\ +\xa3\x35\x7e\x42\xcd\x24\xae\x11\x26\xbd\xe7\xee\x74\x69\x98\xed\ +\x65\x9a\x97\x59\x29\x12\x25\x1c\x24\xbc\x62\x54\xae\x33\x6c\x69\ +\xe6\x0b\x03\x89\x9a\xe6\xd3\xed\xf4\x50\x92\xb0\x9f\x34\xbf\x34\ +\x33\x59\xf3\xe3\xed\x50\x95\x04\xeb\x31\xc5\xf5\xf6\x4b\x46\xf4\ +\xba\xbd\xd7\xdb\x91\x93\x07\xe3\x35\x3e\xa7\x29\xd6\x7f\x40\xfe\ +\xbd\xf7\xf5\x72\x8a\xe5\x79\x92\xf0\xeb\xb4\x1e\x8d\xd5\xf4\x5b\ +\x92\x3a\x56\xdb\xdb\xe4\xcd\xa6\x23\xc3\xc4\x77\x51\x3f\x03\x48\ +\x42\x82\x8e\x1c\x64\x28\xe0\x91\x42\x0c\x61\x9a\x63\xc4\xaa\xf8\ +\x4c\x16\x19\x22\x4a\xa4\xd2\x69\x74\x54\x79\xb2\x38\xd6\x3b\x28\ +\x93\x96\xed\x1c\x47\x78\xc9\x5f\x0e\xb8\x5e\x16\xf5\x5b\xb2\xb8\ +\xf6\xe0\xfb\x9e\xdd\x25\xd7\x8e\xbc\x15\x85\xc5\xb7\xa3\xd8\x51\ +\xed\xb5\x81\xe9\xba\xb2\x13\x9a\x1b\x7f\x75\x61\xa5\xa3\x6e\xe1\ +\x37\xb9\xe5\x9b\x1b\x6d\xab\x0b\x08\x51\xfe\x8a\xe5\xb1\x48\x5e\ +\x65\xca\x4f\x82\x51\xd7\x75\x36\xe6\x90\x53\x97\xfc\x75\x0b\xcf\ +\x32\x94\xee\x25\x76\x12\x58\x0c\xba\xac\xf0\x5e\xf8\x2a\x6c\x0a\ +\x4f\x85\x17\xc2\x97\xbf\xd4\xc8\xce\xde\xad\x11\xcb\x80\x71\x2c\ +\x3e\xab\x9e\x53\xcd\xc6\xec\x25\xd2\x4c\xd2\xeb\x64\xb8\xbf\x8a\ +\xf5\x42\xc6\x18\xf9\x90\x31\x43\x5a\x9d\xbe\x24\x4d\x9c\x8a\x39\ +\xf2\xda\x50\x0b\x27\x06\x77\x82\xeb\xe6\xe2\x5c\x2f\xd7\x07\x9e\ +\xbb\xcc\x5d\xe1\xfa\xb9\x08\xad\x2e\x72\x23\x8e\xc2\x17\xf5\x45\ +\x7c\x21\xf0\xbe\x33\xbe\x3e\x5f\xb7\x6f\x88\x61\xa7\xdb\xbe\xd3\ +\x64\xeb\xa3\x31\x5a\xeb\xbb\xd3\x91\xba\xa2\xb1\x7a\x94\x8f\xdb\ +\x27\xf6\x3d\x8e\xaa\x13\x19\xb2\xb1\xbe\xb1\x7e\x56\x08\x2b\xb4\ +\xa2\x63\x6a\x4a\xb3\x74\x4f\x00\x03\x25\x6d\x4e\x97\xf3\x05\x93\ +\xef\x11\x84\x0b\x7c\x88\xae\x2d\x89\x8f\xab\x62\x57\x90\x4f\x2b\ +\x0a\x6f\x99\x0c\x5e\x97\x0c\x49\xaf\x48\xd9\x2e\xb0\x3b\x8f\xed\ +\x03\xb6\x53\xd6\x5d\xe6\x69\x5f\x73\x39\xf3\x2a\x70\xe9\x1b\xfd\ +\xc3\xeb\x2e\x37\x55\x06\x5e\x19\xc0\xd1\x73\x2e\x17\xa0\x33\x75\ +\xe4\x09\xb0\x7c\x5e\x2c\xeb\x15\xdb\x1f\x3c\x9e\xb7\x80\x91\x3b\ +\xdb\x63\xad\x3d\x6d\x61\xba\x8b\x3e\x56\xab\xdb\x74\x2e\x5b\x1e\ +\x01\xbb\x0f\xab\xd5\x9f\xcf\xaa\xd5\xdd\xe7\xe4\x7f\x0b\x78\xa3\ +\xfc\x06\xa9\x23\x0a\xd6\xc2\xa1\x5f\x32\x00\x00\x00\x09\x70\x48\ +\x59\x73\x00\x00\x04\x9d\x00\x00\x04\x9d\x01\x7c\x34\x6b\xa1\x00\ +\x00\x01\x64\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\x61\ +\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x78\x3a\ +\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\x6d\x6c\x6e\x73\x3a\x78\x3d\ +\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\x3a\x6d\x65\x74\x61\x2f\x22\ +\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\x22\x58\x4d\x50\x20\x43\x6f\ +\x72\x65\x20\x34\x2e\x34\x2e\x30\x22\x3e\x0a\x20\x20\x20\x3c\x72\ +\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\x73\x3a\x72\x64\x66\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ +\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\x32\x2d\x72\ +\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\x22\x3e\x0a\ +\x20\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\x72\ +\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\ +\x3d\x22\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ +\x61\x70\x2f\x31\x2e\x30\x2f\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x3c\x78\x6d\x70\x3a\x43\x72\x65\x61\x74\x6f\x72\x54\ +\x6f\x6f\x6c\x3e\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x2e\x6f\x72\x67\x3c\x2f\x78\x6d\x70\x3a\x43\x72\x65\x61\x74\x6f\ +\x72\x54\x6f\x6f\x6c\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x2f\x72\ +\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\ +\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\ +\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x3e\x0a\xad\x12\xec\x34\x00\ +\x00\x02\x3a\x49\x44\x41\x54\x38\x11\xa5\x52\xcf\x6b\x13\x41\x14\ +\x7e\xf3\x63\xb3\x9b\x34\xbf\xda\x34\x39\xd8\x42\xa1\x92\x56\x6d\ +\x69\x63\x44\xa9\x91\x50\x44\x2f\x1a\x28\x52\xaa\x3d\x88\xf7\x9a\ +\x93\x9e\xfc\x1f\x14\x4f\xa5\xe8\xc1\x73\x4b\x23\x55\x04\x45\xec\ +\x45\x2a\x08\x16\x63\x52\x53\x45\x8a\x54\x4b\x3d\x08\x26\x4d\x62\ +\xd3\x68\xb2\x99\x9d\x75\x26\xb0\x25\xc1\x26\x08\xbe\x65\x78\x6f\ +\xde\x7e\xdf\xb7\xef\xdb\x19\x64\x9a\x26\xfc\x4f\xe0\x76\xe4\xb7\ +\x0b\x23\xe3\x72\xb5\xc3\xd0\x76\x2f\x83\xe1\x2b\xb3\x60\x8a\x07\ +\x60\xb4\x25\x4e\x5a\x38\x68\xa5\x13\xa1\x18\x2f\x2e\x33\x5e\x7c\ +\xc6\x64\x7d\x10\x46\xf6\x5a\x5a\xe8\x1b\x9e\xb8\x83\x78\x91\x80\ +\x58\xbd\xc7\x2e\xdc\x6e\x35\x01\x92\x2a\x56\x3c\x9f\x0d\xfa\x5d\ +\x0a\x1d\xb2\x77\x75\x4c\x86\xce\xdd\x8c\x83\x91\x13\x02\x3a\x70\ +\xe4\x31\x3e\xac\xdc\x9f\x2b\x15\x2a\x4b\x25\x4e\xd7\x63\xd7\x33\ +\x05\x8b\x83\x56\xe6\x82\x47\x83\x63\x17\x1f\x77\x06\x8e\xf8\x08\ +\xa6\x4e\xa2\xa9\x1a\x46\x18\x6a\x95\x6f\x60\x1a\x65\x61\x4f\x07\ +\x40\x00\x54\x09\x00\x37\x74\x60\x7a\xa5\x6c\x30\xf6\xb3\xf8\x63\ +\x7b\xe7\xcb\xda\xea\x74\x7d\x82\x8d\xa7\x67\x67\x0e\x9f\xba\x76\ +\x97\xa0\x5d\x07\x17\x24\x83\xfd\x02\xce\x04\x99\x57\x84\x88\x0e\ +\x08\x71\x51\xef\x02\x11\x99\x2a\x2a\x30\x08\x96\xb6\x32\x2f\x6f\ +\x0d\x4e\xbc\xbe\xb7\x6f\x61\xe3\xc9\xd8\x54\x5f\x78\xea\x81\x82\ +\xb3\x1e\x56\xdb\xab\x8b\x18\xb5\x32\x70\x29\xc2\xf2\x80\x4c\x03\ +\xa8\xcd\x06\x06\x1e\xc8\x7f\x5d\x4f\xc5\x47\x2f\xbf\x5b\x94\x36\ +\xf6\x05\xe4\x26\x3d\x3f\x72\x7e\x30\x72\x69\x91\xf0\xad\x2e\x26\ +\xa6\xa8\x55\x4b\x60\x54\x0b\xd2\x81\xf8\x32\x01\xb0\x0d\xe5\xb6\ +\x33\xc9\xab\xc3\xd3\x6b\xcb\x12\x2f\xa3\x49\x40\x36\x0a\x99\x99\ +\x4d\x55\xd9\xec\x67\x35\x61\xc3\x60\x62\x74\x00\x4c\x10\x60\x4c\ +\xe0\x77\xb5\xe7\x73\xe0\x64\x62\x40\xe2\xac\x68\x3a\x46\x71\x0a\ +\x6e\x8c\x4a\xdd\x18\x74\xe1\x17\x81\x62\xb3\x83\x4d\xd3\x80\x12\ +\x0a\x84\x12\x50\x54\xe4\x97\x18\x8b\x2c\x73\xd3\x4d\xf4\xb9\xb5\ +\xa8\xc3\x65\x77\x03\x73\x80\x49\xfc\x95\x7c\x76\x2f\x69\x22\x44\ +\x7c\xdd\xae\x30\xc5\xdf\x55\xa2\x69\x5e\x9f\x53\x3d\x2d\x78\x2f\ +\x2c\x91\xa6\x09\x3c\x01\x6f\x14\xd3\x43\x7a\x2e\xef\x7e\xf5\x69\ +\x35\x15\xeb\x39\xf3\x28\xda\x1b\x59\x8a\x7c\x4c\xa6\x26\xb3\x3b\ +\x9d\x6f\x88\xda\x5f\x73\x7a\x3b\xa2\x16\xb9\x9e\x1b\xaf\xe8\xfb\ +\x87\x27\xe2\x99\xc4\xf1\xf1\xc6\x5e\x63\x9d\x5e\x08\xc5\xd2\xf3\ +\xa1\x1b\x8d\xbd\xbf\x7e\x62\x93\xfa\x3f\x6c\xfe\x00\x30\x2a\x2f\ +\x9b\x6a\x8a\xe5\x15\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ +\x82\ \x00\x00\x00\xac\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -1914,99 +1914,95 @@ qt_resource_data = "\ \xbc\xfe\x92\x5f\x48\x9e\xe4\xec\x1d\xf1\xc4\xbe\x03\xd8\xc1\xbf\ \xce\x15\x14\xa7\xaa\x2a\x88\xcd\x00\x00\x00\x00\x49\x45\x4e\x44\ \xae\x42\x60\x82\ -\x00\x00\x03\x50\ +\x00\x00\x02\xab\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\ -\x01\x42\x28\x9b\x78\x00\x00\x03\x02\x49\x44\x41\x54\x78\x9c\x75\ -\x93\xdd\x4b\x9b\x67\x18\xc6\xdf\xb1\x8e\xf1\xec\xa0\x62\x07\x1b\ -\x2b\xb2\xff\x60\xc7\x61\x67\x3b\x19\x0c\x61\x2d\x8c\x8d\x95\x7d\ -\x48\x08\x41\x2c\xda\x67\x12\xa2\x0d\x21\x58\x41\x4a\xd7\xa7\xd0\ -\x4e\x02\x4b\x59\xdc\x3c\xe8\x41\x37\x3a\x26\xcc\x42\xf6\x55\xba\ -\xae\x6a\xac\xd6\xc5\x58\x35\xa6\xd5\x24\x26\x31\x7e\x24\x26\x31\ -\x9a\x2f\xf3\xe6\xfd\xed\x20\xcd\x3e\xda\xee\xe0\x3e\xb9\xb9\xef\ -\x1f\xf7\x7d\x71\x5d\x1a\xa0\xfd\xbb\x94\x14\x26\x25\x85\x47\x49\ -\x11\x52\x52\x54\x1e\x57\xe8\x71\xcf\xf4\xe4\xfc\x93\xcb\x9e\x91\ -\xc1\xb6\xe0\x61\xc4\x79\x9b\x62\x70\x8f\x7a\xc9\xa0\x5e\x32\x28\ -\x06\xf7\x2b\xab\x0e\xff\xc8\x60\x5b\x50\x49\xe1\x79\x26\x40\x49\ -\xe1\x4b\x4e\x5b\xaf\xa3\x17\x8c\x62\xd8\xce\x6e\xb0\x87\x74\xa0\ -\x9b\xfc\x62\x2f\xb5\xa8\x13\xb2\xe3\x50\x2f\x19\xf1\x29\x8b\x4f\ -\x49\xe1\xfb\x0f\x40\x49\xe1\x49\x4e\x5b\xbf\x25\xe7\xab\xaf\xdd\ -\xf9\x84\xd5\xdf\x3f\x66\x7d\xd2\xcc\xe6\x6c\x27\x99\xf9\x1e\x2a\ -\xab\x0e\xd8\x74\x43\xe6\x3a\x94\x23\xac\x4f\x9a\xef\x35\x2f\xd1\ -\x94\x14\xa6\x91\xc1\xb6\x20\x7a\xc1\xc8\x2e\x48\xe2\x53\x16\x1e\ -\xf8\xde\xe3\xcf\x1f\xdf\x25\x30\x7e\x82\xc5\x9f\xde\x67\x7b\xae\ -\x0b\x3d\xe6\x82\x6d\x2f\xe4\x7c\xa0\x17\xf0\x9e\x3b\xbe\xa4\xa4\ -\x30\x69\x4a\x0a\x4f\x65\xd5\xb1\xc0\xa6\x9b\xfc\x62\x2f\x09\xbf\ -\x85\xd8\x84\x99\xe4\xb4\x95\xfc\x62\x2f\xe5\x47\xfd\xb0\x71\x89\ -\x74\xa0\x1b\x23\x3e\x00\x89\x21\xc8\xdc\x60\xeb\x7e\xd7\x86\x92\ -\xc2\xa3\x29\x29\x42\x14\x97\x2a\x24\x86\x28\x3d\xec\x27\x7a\xb7\ -\x83\x74\xa0\x9b\xe9\xef\xdf\x61\xb8\xaf\x95\xe1\xbe\x56\x23\x35\ -\xd3\x09\xe9\x6b\x24\xfc\x16\x8a\x61\x7b\xe3\x9a\xd4\x95\xaa\x92\ -\x22\xa4\x29\x29\x2a\xd4\x4b\x06\xdb\x5e\x8c\xf8\x00\xb1\x09\x33\ -\x7a\xcc\xc5\x70\x5f\xab\xf1\xd5\xc0\x6b\x6f\x5c\xb6\x1d\x7d\xd9\ -\x7d\xf6\x58\x9d\x6a\x8a\xf9\x9b\x27\xd9\x5b\xea\xa5\xf4\xb0\x1f\ -\xd2\xd7\x0c\x25\x45\xa5\x09\x68\xa8\xbc\xe9\x26\x39\x6d\x85\x9d\ -\x51\xdc\x67\x8f\xe9\x4a\x8a\x16\x25\x45\xcb\x70\x5f\x6b\x99\x6a\ -\x8a\x99\x1f\xda\xd9\x0d\xf6\x50\x58\xb6\xc1\x96\x07\x25\x45\xb5\ -\xf1\x42\x29\x5c\xe3\x60\x0e\x32\x37\xd8\x0f\xd9\x20\x31\x44\x6a\ -\xa6\xd3\xf8\xc2\xde\x52\xbe\x6c\x3b\x9a\x89\xde\xed\xa8\x57\xd7\ -\x1c\xcc\xdf\x3c\xc9\xd6\xfd\x2e\xb2\x0b\x92\xea\x9a\x43\x57\x52\ -\xac\x68\x4a\x0a\x8f\x1e\x73\xed\x50\xcb\xc2\xc1\x1c\xec\x8e\x51\ -\x0c\xdb\xa1\x30\x09\x87\x3b\x70\xb8\x43\x2d\xea\xe4\xe7\xaf\xdf\ -\x24\x7c\xeb\x14\x09\xbf\x85\xc2\xb2\x8d\xf8\x94\x65\xbf\x29\xa2\ -\x69\x74\xe8\xf5\x08\xc6\x21\xd4\xb2\x50\x8e\x40\xfe\x57\x0e\x23\ -\x4e\x96\x7f\xf9\x80\xd9\xb1\x76\x02\xe3\x27\x08\xdf\x3a\xc5\xa3\ -\xdb\x1f\xb1\x3e\x69\x86\x6d\x2f\xde\x73\xc7\xd3\x4a\x0a\xd3\xdf\ -\x46\x4a\xf8\x2d\x49\xea\xa5\x7f\x20\xd9\x71\xea\xeb\x2e\x9a\xae\ -\xdc\xb8\x67\x65\xe5\xb7\x0f\xd1\x63\x2e\x66\xc7\xda\xf3\x17\xcf\ -\x88\xab\x4f\x59\x39\xf2\xc7\xa7\x39\xf4\x42\x03\x92\xf3\x41\xea\ -\x0a\xb5\xa8\x93\x83\x15\x7b\xe3\xad\x2d\x0f\x93\xdf\xbd\x5d\x6d\ -\x5a\x59\xd3\xb4\xe7\x9f\x0a\xd3\x97\xce\x57\x32\xb9\x07\xb2\xc6\ -\xb6\xb7\xa1\xc9\xde\x1d\x48\x5e\x20\x36\x61\xd6\xaf\xba\x5e\xdd\ -\xff\xdf\x30\x69\x9a\xf6\x9c\xa6\x69\x2f\x38\x3a\xb4\xb7\xce\x9f\ -\x3e\xf2\xcd\xe7\x3d\x2f\x46\x95\x14\xfa\xa5\xcf\x5e\xaa\x5f\x3c\ -\x23\xe2\xe7\x4f\x1f\x19\x7d\x56\x9c\xff\x02\x85\x5c\xd5\xd5\x28\ -\xd2\xd4\xa2\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x02\x31\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\ +\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\x72\ +\x67\x9b\xee\x3c\x1a\x00\x00\x02\x3d\x49\x44\x41\x54\x38\x8d\x85\ +\x93\x4d\x68\x13\x41\x18\x86\xdf\x99\xec\xe6\x0f\x6b\xc0\x60\xd2\ +\x43\xfc\xc1\xaa\xf5\x22\x1a\x8f\x49\xb4\xe0\xc1\x8b\xf1\x22\x14\ +\x2f\x1e\x2a\xe2\x41\x14\xea\xa1\x55\xf1\x2a\x08\xd2\x15\x3c\xa9\ +\x50\x84\x9c\x14\x21\xa2\x52\x53\x5b\x72\xb1\xd6\x55\x08\xb5\x2d\ +\x14\x7b\xa8\x55\xb4\x54\xcd\x9a\xae\xfd\x0b\xe9\x26\xb3\xb3\x9f\ +\x87\xd6\x1a\x9b\x18\xdf\xe3\x37\xdf\x3c\xf3\xbe\xf3\xcd\x30\x22\ +\x42\xb5\xe2\xdd\x6a\x1e\x84\x30\xea\x89\xc1\xd0\x7b\x44\x73\x75\ +\x49\xa9\x69\x22\x84\x6f\x5d\x48\xc3\x76\x04\x6c\x47\x60\xc9\x2a\ +\x60\xc9\x32\x01\x00\x77\x1f\xdd\xa8\x01\xd7\x02\x00\x30\xc6\x90\ +\x9b\x79\x8e\xb2\x5d\x42\xa1\x38\x83\x8f\x73\x63\x68\x3f\x78\xb5\ +\xae\x29\x5e\xaf\x58\x1d\xca\xa3\xf8\xeb\x6e\x6c\x08\x00\x11\x88\ +\x1c\x10\x11\x14\xee\x6e\x08\x50\x12\x97\x55\x9d\x1c\xc4\xd6\xed\ +\xbb\x60\x39\x24\xbd\x92\x6c\x48\xb2\xb1\x5c\x36\x51\xbd\x16\xef\ +\x52\xd7\x0d\x32\x8e\x37\x0a\x39\x48\x45\x42\x2d\xd1\x8b\x27\xaf\ +\xfb\x14\x97\x0a\x22\xc7\x2b\x49\x82\x98\x04\xb8\x44\x51\x16\xa0\ +\x7a\x39\xb8\xc2\x70\xfe\xd4\x35\x2f\x08\x10\xa2\x82\x74\x36\x55\ +\x9a\x5f\x9c\x4b\x31\x22\x42\xa2\x5b\x4d\xb7\x45\x8f\x27\x8f\x44\ +\x93\x9e\xf7\xf9\x57\x90\x5c\x40\xa2\x8c\x15\x7b\x11\xd3\xf3\x39\ +\x10\x68\x2d\x16\x47\xdb\xf6\xd3\x18\x9d\x18\x29\x4f\x4e\x8d\xf7\ +\xbd\xee\x11\xed\x7c\x2d\x72\xc7\xcb\xb1\x8c\xf1\xc5\x98\xa2\x48\ +\x70\x2f\x88\x57\x00\x97\x8d\x42\x65\x1a\xaa\x8f\xc1\xed\xe3\x50\ +\x7d\x2e\x44\x23\x47\x61\x2e\x98\x34\xf9\x61\xdc\x20\xc2\x99\xf5\ +\x4b\xd4\x35\x51\x04\x21\xf9\x70\xf0\x9e\xe5\xe5\x4d\x08\xf8\x83\ +\x28\xd1\x4f\x94\x5d\x8b\x70\x7b\x39\xdc\x3e\x17\xb6\x6d\xd9\x83\ +\x90\xaf\x05\xd9\xa1\x8c\x45\x0e\x92\xba\x26\x8a\x7f\x4d\x41\xd7\ +\xc4\x44\x45\xac\x74\x3e\x18\xbc\x53\xda\x11\x38\x00\x9b\x95\xa0\ +\x7a\x56\x4f\xde\xbc\x29\x80\xfd\xa1\x63\xe8\xcb\x3e\x2e\x09\x51\ +\xe9\xd4\x35\x31\x51\x77\x8c\xba\x26\x7a\x67\x8d\x4f\x2f\x86\xc7\ +\x06\x44\x6b\x30\x01\xae\x30\x28\x6e\x8e\x43\xe1\x13\xc8\xbd\x7b\ +\x5b\xf9\x61\x7e\xeb\xd7\x35\xd1\xdb\xf0\x1d\x90\x83\x8e\xa1\x91\ +\x81\xef\xa6\xb1\x40\xbb\x9b\x62\xd8\xe5\x8f\xc1\xfc\xba\x4c\xb9\ +\x51\x3d\xef\xc8\xd5\xdc\x0d\x01\xba\x26\x8a\x24\x91\xcc\x64\x9f\ +\x59\x5b\xb1\x0f\x41\xa7\x15\x4f\xfb\x9f\x58\x8e\xfc\x93\xbb\x5a\ +\x6c\xe3\x6f\xfc\xad\x78\x97\x7a\x2e\xd2\xbc\xf3\x36\x00\xcc\xe6\ +\x3f\x5f\xda\x68\xfd\xbf\x00\x00\x38\x7c\x45\xbd\x0f\x00\xc3\x37\ +\xc5\xd9\x7f\xf5\xfc\x02\x73\x4a\x12\x2a\x86\x68\x1c\xcb\x00\x00\ +\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x02\x97\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xd5\x0a\ -\x0e\x00\x1c\x15\xaf\xe2\xf1\x18\x00\x00\x01\xd0\x49\x44\x41\x54\ -\x38\xcb\x95\x93\xb1\x6b\x53\x51\x14\xc6\x7f\xf7\xbd\x34\x82\x24\ -\x31\x8d\xd4\x94\x0c\x85\x22\x06\x5c\x5c\xb4\x6a\x62\x04\xd1\x12\ -\x70\x76\xd0\x21\x38\x14\x45\x6d\x1d\xc4\xc5\x41\x97\x82\x38\x08\ -\x8e\xfa\x0f\x34\x83\xbb\x9b\xa8\x4b\xd5\x2e\xba\x38\x29\x91\x52\ -\xa8\xd1\x48\x42\x5e\x6f\x6c\xd0\x46\xef\x39\x0e\xa1\x31\x2f\x6d\ -\x20\x7e\xdb\xfd\xce\x3d\x3f\xce\xf9\xe0\x18\x06\x54\x2a\x95\x8c\ -\xef\x7b\xcf\x9d\x93\xd9\x7e\xdf\xf7\xbd\x17\xce\x49\xb1\x5c\x2e\ -\x6b\xbf\x6f\x00\x6e\xce\x9a\x2b\x06\x1e\x2a\x8c\x2b\x1e\x76\xf2\ -\x12\xb9\xdc\x11\x3c\xcf\x00\x20\xa2\xac\xac\x7c\x60\x5f\xed\x29\ -\x06\x01\x68\xaa\xea\xad\xc7\x2f\x59\x32\xf3\x67\x89\xf8\x9e\x09\ -\x2e\x9c\x39\x18\xfb\xd2\xc9\xf0\x2e\xc8\xd2\xb4\x1d\xa6\xa7\xa7\ -\x30\x66\x1b\x20\xac\xad\xad\x93\x4c\x44\x39\x9a\xfc\xcc\xa4\xbf\ -\xce\xb3\xd7\xab\x56\x54\x53\x11\x20\xe6\x79\x66\x2c\x1a\xf1\x79\ -\x5f\xcf\xb2\x78\xff\x11\x41\x10\x50\xa9\x54\x42\xab\xcd\xcd\x1d\ -\x22\x95\x4a\x71\xef\xee\x6d\x2e\x66\xbe\x21\xaa\x31\x20\x1a\xe9\ -\xff\xd4\xde\x82\x44\x22\x81\xaa\x30\x33\x73\x6c\x30\x1e\xe2\xf1\ -\x38\x5b\x7f\xc0\xa9\xe9\x79\x3d\x80\x6a\x28\x1b\x96\xdf\xbc\x0d\ -\xbd\x0b\xf9\x1c\xbb\x29\xc2\x10\x9d\x3e\x95\xe7\x1f\x5c\x10\x71\ -\xc3\x01\xba\x4b\x61\x7b\x02\x55\x45\x45\xc8\x9d\x3c\xfe\x7f\x13\ -\x14\xf2\x39\x44\x04\x11\x41\x55\xb0\xd6\x32\x31\x91\x1e\x0d\xd0\ -\xdf\x28\xd2\x6d\x6e\xfd\x68\x8d\x3e\x81\x88\xeb\x41\xaa\x5f\xab\ -\x34\x1a\xf5\x1d\x21\x87\x01\x03\x35\xe7\xba\x80\x56\xcb\xd2\xa8\ -\xd7\x29\x16\xcf\x0f\xdb\x14\x6f\xd8\x04\xd6\x6e\x50\xfb\x5e\x43\ -\x44\x7a\xfe\xf5\xf9\xab\x5c\x9e\x5a\x1e\xbe\xc2\xde\x31\xa5\x19\ -\x34\x49\xa7\x33\xa4\xd3\x19\xb2\xd9\xc3\x58\xbb\x81\xaa\x70\x63\ -\xe1\x5a\xaf\xd9\x37\x1a\x02\x6c\x8a\xe8\xef\xf6\x2f\xb7\xe7\x44\ -\xf2\x23\x0f\x16\xef\xf0\xd3\xed\x8c\xa6\xb0\xff\x13\xab\xed\x03\ -\x00\x24\xb5\x8a\x31\x66\x53\x55\x3b\x06\x60\xe1\x5c\xf7\x1a\x31\ -\x8c\x33\x82\x54\xbb\xd7\xf8\xe4\x15\x4b\x7f\x01\x4f\xe6\xe1\x98\ -\x78\x69\xc4\xda\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\ +\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\x72\ +\x67\x9b\xee\x3c\x1a\x00\x00\x02\x29\x49\x44\x41\x54\x38\x8d\x95\ +\x52\x3b\x68\x53\x61\x18\x3d\xff\x7d\x24\x37\x52\x2c\x44\x4d\x3a\ +\x24\x45\xa9\xaf\x45\xb0\x76\x32\xad\x08\x1d\x04\x25\x1d\x8a\x88\ +\x8b\x43\x1d\x45\xb0\x88\xd6\xc1\x5d\x10\x7a\x47\x07\xa1\x4b\xa6\ +\x0e\x52\x51\x09\x6d\x84\x4c\x3e\xae\x85\x50\x4b\x34\x34\x42\x8b\ +\x42\x87\x94\xa4\x35\x5a\x31\x4d\xef\xbd\xff\xe3\x73\x88\xc4\xd4\ +\xa4\x62\x0f\xfc\xcb\xe1\xe3\x7c\xff\x39\xdf\x61\x44\x84\x56\x0c\ +\x4e\x98\x65\x10\xa2\xe8\x04\x86\x8a\x33\xc9\x7b\x5a\x29\xa3\x6d\ +\x88\x10\xb5\x6f\x3e\x81\x50\x02\x42\xf9\x90\x8a\x43\x28\x0e\x2e\ +\x3d\x3c\x4c\xdd\x6e\x13\x6e\x17\x00\xc0\x98\x86\xdc\x6a\x1a\x9e\ +\xd8\x86\x2b\xb6\xe0\xf1\x2d\x9c\x3d\x32\xda\xf1\x53\x5a\x47\x16\ +\x0c\x8c\xfd\x7e\x60\x00\x63\x90\x8a\xef\x45\xa0\x91\x0b\x11\x81\ +\x40\x00\xd1\xae\x02\xc6\xd0\x3d\xd3\x21\x85\x44\x73\xb7\x0e\x97\ +\x88\x2c\xa5\x24\x88\x14\x88\x14\x7c\xb9\x0d\xae\x7c\x30\x1d\xee\ +\xe0\x5d\xb3\x99\x3a\xd3\xf0\xce\x20\x85\x54\x2c\xd2\xd7\x7f\xeb\ +\xf2\x83\x90\xa1\x07\x40\xa4\x2c\x49\x02\x8a\x49\x40\x17\x00\x04\ +\xca\x9b\xcb\x60\xfa\x25\xdc\xb8\x7a\xdf\x02\x01\x9c\xfb\x98\xc9\ +\xa6\xea\xdf\x7f\x7c\x4d\x31\x22\xc2\xd0\x84\x39\x73\xee\xf4\xc5\ +\xe4\xf0\xc0\x68\xb0\x58\x79\x03\xc9\x7c\x48\xf8\xf8\xe6\x96\xb0\ +\x56\xfb\x04\xae\x7c\x80\x08\x44\x1a\xce\xf7\x5e\xc3\x62\x61\xc1\ +\x2b\x2e\xe7\xd3\x6f\x27\xf9\x15\xad\xe1\x15\x63\xaf\xf3\x99\xca\ +\x97\xf2\x12\xc5\xc2\xc7\x41\x1a\xc7\xba\xb7\x82\x92\xfb\x11\x2c\ +\x28\x10\x08\x69\x30\x43\x3a\xfa\x63\xc3\xa8\x6e\x56\xa9\xb8\x92\ +\xaf\x10\xe1\x7a\x33\x44\xc7\xe6\x35\x10\x92\xd3\x2f\x1f\xbb\x06\ +\xb3\x60\x9a\x26\x36\xfc\xcf\x30\x2d\x1d\x01\x4b\x43\x20\xa4\x23\ +\x1e\x3e\x86\x48\xa8\x0f\xd9\x57\xb3\x2e\x29\x24\x1d\x9b\xd7\x76\ +\x5c\xc1\xb1\x79\x41\x08\x6f\x7c\x3a\xf3\xa8\x7e\x70\x5f\x1c\x5d\ +\xa1\x6e\x98\xc1\xc6\xe6\xfd\x5d\xdd\x38\x15\xb9\x80\x74\xf6\x69\ +\x9d\x73\x7f\xdc\xb1\x79\xa1\xe3\x19\x1d\x9b\x4f\x95\x36\x56\x33\ +\xb9\x0f\x8e\x3f\xd0\x33\x02\xcd\x60\x30\x02\x1a\xce\x44\x47\x90\ +\x7b\x3f\xef\xaf\x57\xd7\xe6\x1c\x9b\x4f\xed\x38\x63\x5b\x03\x14\ +\xc6\x16\x0a\xf3\x4b\xbd\xb1\xc3\xf1\xa3\xe1\x04\xd3\x0d\x0d\xd5\ +\xd2\x4f\xca\x2d\x3a\x65\x25\x1b\xbe\xff\x59\x24\xc7\xe6\x35\x92\ +\x48\xce\x66\x5f\xb8\x87\x70\x12\x07\xd4\x09\x3c\x9f\x7b\xe6\x2a\ +\xf9\xc7\x77\x2b\x58\xe2\x8e\x41\x7f\x93\x7b\xc1\x2e\x55\xfe\x7f\ +\xfc\x02\xaa\x26\x0d\x2a\x62\xcc\x1b\x11\x00\x00\x00\x00\x49\x45\ +\x4e\x44\xae\x42\x60\x82\ \x00\x00\x03\x8f\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -2066,6 +2062,99 @@ qt_resource_data = "\ \x78\x5d\x1c\xa5\x14\xf1\xba\x3a\x94\x52\x2c\x89\x46\x73\x96\x03\ \xa9\x8e\xe4\x19\xe0\x34\xff\xa7\xce\x5f\x93\x3b\x33\x4c\xd5\x22\ \x62\x6d\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x02\x31\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xd5\x0a\ +\x0e\x00\x1c\x15\xaf\xe2\xf1\x18\x00\x00\x01\xd0\x49\x44\x41\x54\ +\x38\xcb\x95\x93\xb1\x6b\x53\x51\x14\xc6\x7f\xf7\xbd\x34\x82\x24\ +\x31\x8d\xd4\x94\x0c\x85\x22\x06\x5c\x5c\xb4\x6a\x62\x04\xd1\x12\ +\x70\x76\xd0\x21\x38\x14\x45\x6d\x1d\xc4\xc5\x41\x97\x82\x38\x08\ +\x8e\xfa\x0f\x34\x83\xbb\x9b\xa8\x4b\xd5\x2e\xba\x38\x29\x91\x52\ +\xa8\xd1\x48\x42\x5e\x6f\x6c\xd0\x46\xef\x39\x0e\xa1\x31\x2f\x6d\ +\x20\x7e\xdb\xfd\xce\x3d\x3f\xce\xf9\xe0\x18\x06\x54\x2a\x95\x8c\ +\xef\x7b\xcf\x9d\x93\xd9\x7e\xdf\xf7\xbd\x17\xce\x49\xb1\x5c\x2e\ +\x6b\xbf\x6f\x00\x6e\xce\x9a\x2b\x06\x1e\x2a\x8c\x2b\x1e\x76\xf2\ +\x12\xb9\xdc\x11\x3c\xcf\x00\x20\xa2\xac\xac\x7c\x60\x5f\xed\x29\ +\x06\x01\x68\xaa\xea\xad\xc7\x2f\x59\x32\xf3\x67\x89\xf8\x9e\x09\ +\x2e\x9c\x39\x18\xfb\xd2\xc9\xf0\x2e\xc8\xd2\xb4\x1d\xa6\xa7\xa7\ +\x30\x66\x1b\x20\xac\xad\xad\x93\x4c\x44\x39\x9a\xfc\xcc\xa4\xbf\ +\xce\xb3\xd7\xab\x56\x54\x53\x11\x20\xe6\x79\x66\x2c\x1a\xf1\x79\ +\x5f\xcf\xb2\x78\xff\x11\x41\x10\x50\xa9\x54\x42\xab\xcd\xcd\x1d\ +\x22\x95\x4a\x71\xef\xee\x6d\x2e\x66\xbe\x21\xaa\x31\x20\x1a\xe9\ +\xff\xd4\xde\x82\x44\x22\x81\xaa\x30\x33\x73\x6c\x30\x1e\xe2\xf1\ +\x38\x5b\x7f\xc0\xa9\xe9\x79\x3d\x80\x6a\x28\x1b\x96\xdf\xbc\x0d\ +\xbd\x0b\xf9\x1c\xbb\x29\xc2\x10\x9d\x3e\x95\xe7\x1f\x5c\x10\x71\ +\xc3\x01\xba\x4b\x61\x7b\x02\x55\x45\x45\xc8\x9d\x3c\xfe\x7f\x13\ +\x14\xf2\x39\x44\x04\x11\x41\x55\xb0\xd6\x32\x31\x91\x1e\x0d\xd0\ +\xdf\x28\xd2\x6d\x6e\xfd\x68\x8d\x3e\x81\x88\xeb\x41\xaa\x5f\xab\ +\x34\x1a\xf5\x1d\x21\x87\x01\x03\x35\xe7\xba\x80\x56\xcb\xd2\xa8\ +\xd7\x29\x16\xcf\x0f\xdb\x14\x6f\xd8\x04\xd6\x6e\x50\xfb\x5e\x43\ +\x44\x7a\xfe\xf5\xf9\xab\x5c\x9e\x5a\x1e\xbe\xc2\xde\x31\xa5\x19\ +\x34\x49\xa7\x33\xa4\xd3\x19\xb2\xd9\xc3\x58\xbb\x81\xaa\x70\x63\ +\xe1\x5a\xaf\xd9\x37\x1a\x02\x6c\x8a\xe8\xef\xf6\x2f\xb7\xe7\x44\ +\xf2\x23\x0f\x16\xef\xf0\xd3\xed\x8c\xa6\xb0\xff\x13\xab\xed\x03\ +\x00\x24\xb5\x8a\x31\x66\x53\x55\x3b\x06\x60\xe1\x5c\xf7\x1a\x31\ +\x8c\x33\x82\x54\xbb\xd7\xf8\xe4\x15\x4b\x7f\x01\x4f\xe6\xe1\x98\ +\x78\x69\xc4\xda\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\ +\x00\x00\x03\x50\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\ +\x01\x42\x28\x9b\x78\x00\x00\x03\x02\x49\x44\x41\x54\x78\x9c\x75\ +\x93\xdd\x4b\x9b\x67\x18\xc6\xdf\xb1\x8e\xf1\xec\xa0\x62\x07\x1b\ +\x2b\xb2\xff\x60\xc7\x61\x67\x3b\x19\x0c\x61\x2d\x8c\x8d\x95\x7d\ +\x48\x08\x41\x2c\xda\x67\x12\xa2\x0d\x21\x58\x41\x4a\xd7\xa7\xd0\ +\x4e\x02\x4b\x59\xdc\x3c\xe8\x41\x37\x3a\x26\xcc\x42\xf6\x55\xba\ +\xae\x6a\xac\xd6\xc5\x58\x35\xa6\xd5\x24\x26\x31\x7e\x24\x26\x31\ +\x9a\x2f\xf3\xe6\xfd\xed\x20\xcd\x3e\xda\xee\xe0\x3e\xb9\xb9\xef\ +\x1f\xf7\x7d\x71\x5d\x1a\xa0\xfd\xbb\x94\x14\x26\x25\x85\x47\x49\ +\x11\x52\x52\x54\x1e\x57\xe8\x71\xcf\xf4\xe4\xfc\x93\xcb\x9e\x91\ +\xc1\xb6\xe0\x61\xc4\x79\x9b\x62\x70\x8f\x7a\xc9\xa0\x5e\x32\x28\ +\x06\xf7\x2b\xab\x0e\xff\xc8\x60\x5b\x50\x49\xe1\x79\x26\x40\x49\ +\xe1\x4b\x4e\x5b\xaf\xa3\x17\x8c\x62\xd8\xce\x6e\xb0\x87\x74\xa0\ +\x9b\xfc\x62\x2f\xb5\xa8\x13\xb2\xe3\x50\x2f\x19\xf1\x29\x8b\x4f\ +\x49\xe1\xfb\x0f\x40\x49\xe1\x49\x4e\x5b\xbf\x25\xe7\xab\xaf\xdd\ +\xf9\x84\xd5\xdf\x3f\x66\x7d\xd2\xcc\xe6\x6c\x27\x99\xf9\x1e\x2a\ +\xab\x0e\xd8\x74\x43\xe6\x3a\x94\x23\xac\x4f\x9a\xef\x35\x2f\xd1\ +\x94\x14\xa6\x91\xc1\xb6\x20\x7a\xc1\xc8\x2e\x48\xe2\x53\x16\x1e\ +\xf8\xde\xe3\xcf\x1f\xdf\x25\x30\x7e\x82\xc5\x9f\xde\x67\x7b\xae\ +\x0b\x3d\xe6\x82\x6d\x2f\xe4\x7c\xa0\x17\xf0\x9e\x3b\xbe\xa4\xa4\ +\x30\x69\x4a\x0a\x4f\x65\xd5\xb1\xc0\xa6\x9b\xfc\x62\x2f\x09\xbf\ +\x85\xd8\x84\x99\xe4\xb4\x95\xfc\x62\x2f\xe5\x47\xfd\xb0\x71\x89\ +\x74\xa0\x1b\x23\x3e\x00\x89\x21\xc8\xdc\x60\xeb\x7e\xd7\x86\x92\ +\xc2\xa3\x29\x29\x42\x14\x97\x2a\x24\x86\x28\x3d\xec\x27\x7a\xb7\ +\x83\x74\xa0\x9b\xe9\xef\xdf\x61\xb8\xaf\x95\xe1\xbe\x56\x23\x35\ +\xd3\x09\xe9\x6b\x24\xfc\x16\x8a\x61\x7b\xe3\x9a\xd4\x95\xaa\x92\ +\x22\xa4\x29\x29\x2a\xd4\x4b\x06\xdb\x5e\x8c\xf8\x00\xb1\x09\x33\ +\x7a\xcc\xc5\x70\x5f\xab\xf1\xd5\xc0\x6b\x6f\x5c\xb6\x1d\x7d\xd9\ +\x7d\xf6\x58\x9d\x6a\x8a\xf9\x9b\x27\xd9\x5b\xea\xa5\xf4\xb0\x1f\ +\xd2\xd7\x0c\x25\x45\xa5\x09\x68\xa8\xbc\xe9\x26\x39\x6d\x85\x9d\ +\x51\xdc\x67\x8f\xe9\x4a\x8a\x16\x25\x45\xcb\x70\x5f\x6b\x99\x6a\ +\x8a\x99\x1f\xda\xd9\x0d\xf6\x50\x58\xb6\xc1\x96\x07\x25\x45\xb5\ +\xf1\x42\x29\x5c\xe3\x60\x0e\x32\x37\xd8\x0f\xd9\x20\x31\x44\x6a\ +\xa6\xd3\xf8\xc2\xde\x52\xbe\x6c\x3b\x9a\x89\xde\xed\xa8\x57\xd7\ +\x1c\xcc\xdf\x3c\xc9\xd6\xfd\x2e\xb2\x0b\x92\xea\x9a\x43\x57\x52\ +\xac\x68\x4a\x0a\x8f\x1e\x73\xed\x50\xcb\xc2\xc1\x1c\xec\x8e\x51\ +\x0c\xdb\xa1\x30\x09\x87\x3b\x70\xb8\x43\x2d\xea\xe4\xe7\xaf\xdf\ +\x24\x7c\xeb\x14\x09\xbf\x85\xc2\xb2\x8d\xf8\x94\x65\xbf\x29\xa2\ +\x69\x74\xe8\xf5\x08\xc6\x21\xd4\xb2\x50\x8e\x40\xfe\x57\x0e\x23\ +\x4e\x96\x7f\xf9\x80\xd9\xb1\x76\x02\xe3\x27\x08\xdf\x3a\xc5\xa3\ +\xdb\x1f\xb1\x3e\x69\x86\x6d\x2f\xde\x73\xc7\xd3\x4a\x0a\xd3\xdf\ +\x46\x4a\xf8\x2d\x49\xea\xa5\x7f\x20\xd9\x71\xea\xeb\x2e\x9a\xae\ +\xdc\xb8\x67\x65\xe5\xb7\x0f\xd1\x63\x2e\x66\xc7\xda\xf3\x17\xcf\ +\x88\xab\x4f\x59\x39\xf2\xc7\xa7\x39\xf4\x42\x03\x92\xf3\x41\xea\ +\x0a\xb5\xa8\x93\x83\x15\x7b\xe3\xad\x2d\x0f\x93\xdf\xbd\x5d\x6d\ +\x5a\x59\xd3\xb4\xe7\x9f\x0a\xd3\x97\xce\x57\x32\xb9\x07\xb2\xc6\ +\xb6\xb7\xa1\xc9\xde\x1d\x48\x5e\x20\x36\x61\xd6\xaf\xba\x5e\xdd\ +\xff\xdf\x30\x69\x9a\xf6\x9c\xa6\x69\x2f\x38\x3a\xb4\xb7\xce\x9f\ +\x3e\xf2\xcd\xe7\x3d\x2f\x46\x95\x14\xfa\xa5\xcf\x5e\xaa\x5f\x3c\ +\x23\xe2\xe7\x4f\x1f\x19\x7d\x56\x9c\xff\x02\x85\x5c\xd5\xd5\x28\ +\xd2\xd4\xa2\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x03\x90\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -2974,6 +3063,80 @@ qt_resource_data = "\ \x7f\x82\x77\x0a\xd7\xf2\xe0\x46\xfd\x4c\x7f\x09\x30\x00\xef\x78\ \xf6\x1a\x18\x1d\xd4\xb7\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ \x60\x82\ +\x00\x00\x04\x7e\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x16\x00\x00\x00\x16\x08\x06\x00\x00\x00\xc4\xb4\x6c\x3b\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\ +\x0d\xd7\x01\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xd5\x0b\x0a\x0d\x34\x15\x48\x65\x72\x06\x00\x00\x04\x0b\x49\x44\ +\x41\x54\x38\xcb\xad\x95\x5d\x6c\x14\x55\x14\xc7\x7f\x33\xfb\x39\ +\x9d\x6d\xbb\xd3\x65\x97\xdd\xd6\x65\x59\x3e\x96\x22\x05\xda\x92\ +\x56\x8b\x1f\x58\x13\x1f\x30\x42\x34\x46\x7c\xd0\x17\xd3\xf8\x00\ +\x35\x48\x55\x52\x03\xfa\x20\x46\x02\x18\x8b\x69\x6c\x1f\x44\x1f\ +\x34\x8a\x92\xc8\x8b\x0f\x68\x0c\x31\x36\xc6\x88\x34\x41\x54\xac\ +\x25\xb4\xb5\x36\xc5\xd6\xdd\xb6\xd3\x76\xbf\x67\x67\x77\x7c\x68\ +\x77\x6c\xa9\x50\x4c\xfc\x27\x37\xe7\xe6\x9e\x73\xfe\xf7\x7f\xcf\ +\x3d\x33\x57\x60\x01\x9e\x7b\xa3\xfd\xfd\x3f\x12\x95\x8f\xc6\xa6\ +\xb4\x8a\x85\xeb\x92\xcd\x48\xfa\x5c\xf1\xbe\x95\xf2\x6f\x6d\x9d\ +\xaf\x7e\xf6\x1d\xb7\x01\xa1\x38\xe9\xfa\xa0\xad\xf6\xec\x8f\xe1\ +\x8b\xab\x23\x1b\xb3\xa1\x95\x3e\x31\x9e\x13\x9d\x55\x65\x4e\xf1\ +\xcf\xb8\xc6\x54\x3a\xcb\xf4\xd5\xf3\x43\xa9\x64\xc6\x11\x56\xfa\ +\xdb\xbb\x5f\x3b\xfd\xf1\x72\xc4\x96\x22\xe9\xf9\xfe\xc0\xd9\x55\ +\x5b\x1e\x70\x0f\x0f\xf4\xdb\x07\x87\x87\x6d\x23\xbf\x0f\x8a\x23\ +\x13\x93\xd8\x4a\x2b\x50\x9c\x76\x9c\xde\x75\xca\x74\xba\x50\x76\ +\x3d\xe6\xdc\xb9\xeb\x11\xcf\xa5\x8b\xdf\xfc\x3a\x70\x2b\x62\x11\ +\xe0\x87\x6b\xce\xe3\x77\xac\xb0\x0e\x27\x06\xbe\xba\xda\xe8\xbd\ +\xf0\x74\x4f\xc7\x3e\xf1\xbe\x50\xef\xce\xdc\xcc\xf5\xd9\xd1\xc1\ +\x2b\x46\x74\x36\x4e\x3c\x99\x40\xf1\x05\x90\x7d\x41\xc7\x2f\xb1\ +\xad\x9f\xbe\xf4\xfa\x2e\x79\x59\x62\x35\x25\xad\xb3\xe4\x13\x33\ +\xf7\xaf\x1f\x7d\xe8\xe8\x8b\xef\x7d\x02\x70\xec\xe0\x47\x5f\x36\ +\x04\x47\x8f\x24\x54\x55\x48\xce\x4e\xa1\x65\x52\x00\xf8\x82\xeb\ +\x45\xaf\xac\x8d\x4c\x68\xe1\xee\x65\x6b\xfc\xd4\xa1\x17\x3e\x0f\ +\x3b\x72\x3f\xcf\xaa\x85\xc3\x0b\x9d\x0e\x49\xe0\xdb\xa9\x10\xab\ +\x37\xd5\x2f\x4a\x72\xa7\xfb\x98\xf8\x6b\x9c\xca\xc2\xf4\x8d\x7c\ +\xed\x9d\x6f\x77\x9d\x00\xb0\x02\x04\xcb\xe2\x5f\xa4\x26\xed\xdd\ +\xc7\x8e\xbe\xb9\x64\xe7\x07\xdb\xba\x96\xac\x65\x3c\x77\x31\x3a\ +\xd4\xc3\x87\x27\x17\xe9\xe0\xe5\x43\x07\x8f\x03\x27\xcc\x52\xa4\ +\xc6\xed\xd7\x14\xb7\x97\x54\x6a\xee\xb8\xc9\x64\xd2\x1c\x26\x59\ +\xfa\x9f\xb9\xc5\x66\x5f\x14\x37\x32\x3a\x42\x32\x99\xc4\xeb\xf5\ +\xb2\xff\x40\x6b\xad\xa9\x18\x58\xeb\xf7\x07\x00\x48\x24\x13\xb8\ +\x64\xd7\xbf\x2a\x2d\xc2\x28\x14\x00\x78\xec\xc8\x69\x74\x4d\xe3\ +\xcc\xe1\x3d\x00\x28\x8a\x42\x2c\x16\x8b\x00\x97\xc5\xf9\xd8\xdd\ +\x75\x5b\xeb\x49\xa7\xd3\x08\x08\xa6\xd2\xea\x86\x1d\x54\x37\xec\ +\xa0\x5c\xf1\x02\x90\xcd\x66\xe6\x6c\x3a\xce\xba\xda\xbb\x59\x53\ +\xd3\x00\x40\x89\x54\x02\xc0\x3d\x4d\xf7\x02\x6c\x36\x4b\x01\x3c\ +\x5c\x59\x59\xb5\x48\xd5\xbb\xcf\x37\x33\xf0\xd3\xf7\x00\x8c\x8d\ +\x0e\x81\x61\x90\xd7\x73\x64\xd2\x49\xe2\x33\x2a\x00\x43\x57\x7a\ +\x79\x67\xdf\x76\x33\x27\x18\x0c\x01\xbc\x02\x20\xee\x3f\xd0\x5a\ +\x2b\x49\x25\xd8\xed\x76\x3c\x1e\x8f\x19\x14\xf0\x86\xe8\x68\xd9\ +\x46\x7f\x6f\x0f\x15\xde\x4a\x0a\x86\x31\x5f\x07\x83\x32\xb7\x87\ +\x81\xcb\x17\xe8\x68\xd9\x46\xa8\x32\x02\x80\x2c\xcb\xf3\xe5\xf0\ +\xb0\xff\x40\x6b\xad\x08\x44\x56\xfa\xfc\x00\xa8\xaa\x8a\x2c\xcb\ +\xc8\xb2\x8c\x28\x8a\x84\xab\x36\xd0\xb9\xb7\xc9\x24\x17\x05\x01\ +\x57\x79\x05\x03\x97\x2f\xd0\xb9\xb7\x89\x70\xd5\x06\x33\x5e\x55\ +\xe7\x4e\x51\x5e\x56\x0e\x10\x11\x81\xcd\x77\x6e\xdc\x44\x2e\x97\ +\x33\xac\x56\x0b\x33\x33\x33\xa8\xaa\x8a\x24\x49\xc8\xb2\x4c\x4d\ +\xa4\x9e\x53\x6d\xcd\xf4\xf7\xf6\xe0\x5e\x11\x60\xb8\xef\x12\xa7\ +\xda\x9a\xa9\x89\xd4\x9b\x84\xaa\xaa\x92\xcf\xeb\x24\x12\x09\x63\ +\xed\x9a\xb5\x00\x4d\x62\x36\x9b\xdd\xe2\xf7\xfb\x71\xb9\x5c\x82\ +\xae\xe7\x29\xcc\xdf\x78\x31\x41\x55\x55\x2a\x4a\x03\x74\x3c\xdb\ +\x40\x7f\x6f\x0f\x6f\x3d\x53\x4f\x45\x69\xc0\xf4\x01\xe4\xf3\x3a\ +\x08\xa0\x28\x8a\x10\x0c\xae\x22\x93\xc9\x96\x5a\x1d\x0e\xc7\xee\ +\x48\x24\x62\xb6\xcb\xcd\xa0\x28\x8d\x7c\x7d\xb2\x71\xd9\xdf\x65\ +\x75\x75\x35\x4e\xa7\xa3\xc5\x0a\x10\x8b\xc5\x98\x9c\x9c\xbc\x79\ +\xdf\x16\x2f\xee\x36\x7c\x5e\xef\x5c\x6b\x16\x3f\x10\x34\x4d\xc3\ +\xe3\xf1\x70\xee\xdc\x39\x1c\x0e\x07\xb2\xab\x84\x52\x57\x19\xa2\ +\x28\x22\x49\x12\x82\x20\x20\x49\x12\x2e\x97\x0b\x55\x55\xf1\xf9\ +\x7c\x44\xa3\xd1\x25\xb6\x48\x2c\x2e\xdc\x4d\xd7\x75\x2c\x16\x0b\ +\x86\x61\x20\x0a\x16\xb2\xd9\x2c\x9a\xa6\x61\x18\x06\x36\x9b\xed\ +\x96\xca\x6f\x84\xa9\x58\x10\x04\xd2\xe9\x34\x75\x75\xb5\xe8\x7a\ +\x1e\x8b\xc5\x82\xae\xeb\x66\xa0\xdd\x6e\xe7\xbf\xc0\x0a\x10\x8d\ +\x46\x0d\xbf\xdf\x3f\xff\x4c\x79\x96\x4d\x72\xbb\xdd\x00\x84\xc3\ +\xe1\x25\x76\x6c\x6c\xcc\x00\x04\xe1\x89\x27\x1f\xdf\x13\x08\xf8\ +\xcf\xf0\x3f\x62\x6c\x6c\x7c\xfb\xdf\xe7\x60\x9f\x53\x70\x22\xd1\ +\x6a\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x05\x61\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -3063,80 +3226,6 @@ qt_resource_data = "\ \x16\xcd\xc6\xa7\x9b\x3d\x77\xd1\x5f\x8d\x3f\x00\xaa\x6e\x0b\x3a\ \xfe\x92\xe9\xdb\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \ -\x00\x00\x04\x7e\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x16\x00\x00\x00\x16\x08\x06\x00\x00\x00\xc4\xb4\x6c\x3b\ -\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ -\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\ -\x0d\xd7\x01\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ -\xd5\x0b\x0a\x0d\x34\x15\x48\x65\x72\x06\x00\x00\x04\x0b\x49\x44\ -\x41\x54\x38\xcb\xad\x95\x5d\x6c\x14\x55\x14\xc7\x7f\x33\xfb\x39\ -\x9d\x6d\xbb\xd3\x65\x97\xdd\xd6\x65\x59\x3e\x96\x22\x05\xda\x92\ -\x56\x8b\x1f\x58\x13\x1f\x30\x42\x34\x46\x7c\xd0\x17\xd3\xf8\x00\ -\x35\x48\x55\x52\x03\xfa\x20\x46\x02\x18\x8b\x69\x6c\x1f\x44\x1f\ -\x34\x8a\x92\xc8\x8b\x0f\x68\x0c\x31\x36\xc6\x88\x34\x41\x54\xac\ -\x25\xb4\xb5\x36\xc5\xd6\xdd\xb6\xd3\x76\xbf\x67\x67\x77\x7c\x68\ -\x77\x6c\xa9\x50\x4c\xfc\x27\x37\xe7\xe6\x9e\x73\xfe\xf7\x7f\xcf\ -\x3d\x33\x57\x60\x01\x9e\x7b\xa3\xfd\xfd\x3f\x12\x95\x8f\xc6\xa6\ -\xb4\x8a\x85\xeb\x92\xcd\x48\xfa\x5c\xf1\xbe\x95\xf2\x6f\x6d\x9d\ -\xaf\x7e\xf6\x1d\xb7\x01\xa1\x38\xe9\xfa\xa0\xad\xf6\xec\x8f\xe1\ -\x8b\xab\x23\x1b\xb3\xa1\x95\x3e\x31\x9e\x13\x9d\x55\x65\x4e\xf1\ -\xcf\xb8\xc6\x54\x3a\xcb\xf4\xd5\xf3\x43\xa9\x64\xc6\x11\x56\xfa\ -\xdb\xbb\x5f\x3b\xfd\xf1\x72\xc4\x96\x22\xe9\xf9\xfe\xc0\xd9\x55\ -\x5b\x1e\x70\x0f\x0f\xf4\xdb\x07\x87\x87\x6d\x23\xbf\x0f\x8a\x23\ -\x13\x93\xd8\x4a\x2b\x50\x9c\x76\x9c\xde\x75\xca\x74\xba\x50\x76\ -\x3d\xe6\xdc\xb9\xeb\x11\xcf\xa5\x8b\xdf\xfc\x3a\x70\x2b\x62\x11\ -\xe0\x87\x6b\xce\xe3\x77\xac\xb0\x0e\x27\x06\xbe\xba\xda\xe8\xbd\ -\xf0\x74\x4f\xc7\x3e\xf1\xbe\x50\xef\xce\xdc\xcc\xf5\xd9\xd1\xc1\ -\x2b\x46\x74\x36\x4e\x3c\x99\x40\xf1\x05\x90\x7d\x41\xc7\x2f\xb1\ -\xad\x9f\xbe\xf4\xfa\x2e\x79\x59\x62\x35\x25\xad\xb3\xe4\x13\x33\ -\xf7\xaf\x1f\x7d\xe8\xe8\x8b\xef\x7d\x02\x70\xec\xe0\x47\x5f\x36\ -\x04\x47\x8f\x24\x54\x55\x48\xce\x4e\xa1\x65\x52\x00\xf8\x82\xeb\ -\x45\xaf\xac\x8d\x4c\x68\xe1\xee\x65\x6b\xfc\xd4\xa1\x17\x3e\x0f\ -\x3b\x72\x3f\xcf\xaa\x85\xc3\x0b\x9d\x0e\x49\xe0\xdb\xa9\x10\xab\ -\x37\xd5\x2f\x4a\x72\xa7\xfb\x98\xf8\x6b\x9c\xca\xc2\xf4\x8d\x7c\ -\xed\x9d\x6f\x77\x9d\x00\xb0\x02\x04\xcb\xe2\x5f\xa4\x26\xed\xdd\ -\xc7\x8e\xbe\xb9\x64\xe7\x07\xdb\xba\x96\xac\x65\x3c\x77\x31\x3a\ -\xd4\xc3\x87\x27\x17\xe9\xe0\xe5\x43\x07\x8f\x03\x27\xcc\x52\xa4\ -\xc6\xed\xd7\x14\xb7\x97\x54\x6a\xee\xb8\xc9\x64\xd2\x1c\x26\x59\ -\xfa\x9f\xb9\xc5\x66\x5f\x14\x37\x32\x3a\x42\x32\x99\xc4\xeb\xf5\ -\xb2\xff\x40\x6b\xad\xa9\x18\x58\xeb\xf7\x07\x00\x48\x24\x13\xb8\ -\x64\xd7\xbf\x2a\x2d\xc2\x28\x14\x00\x78\xec\xc8\x69\x74\x4d\xe3\ -\xcc\xe1\x3d\x00\x28\x8a\x42\x2c\x16\x8b\x00\x97\xc5\xf9\xd8\xdd\ -\x75\x5b\xeb\x49\xa7\xd3\x08\x08\xa6\xd2\xea\x86\x1d\x54\x37\xec\ -\xa0\x5c\xf1\x02\x90\xcd\x66\xe6\x6c\x3a\xce\xba\xda\xbb\x59\x53\ -\xd3\x00\x40\x89\x54\x02\xc0\x3d\x4d\xf7\x02\x6c\x36\x4b\x01\x3c\ -\x5c\x59\x59\xb5\x48\xd5\xbb\xcf\x37\x33\xf0\xd3\xf7\x00\x8c\x8d\ -\x0e\x81\x61\x90\xd7\x73\x64\xd2\x49\xe2\x33\x2a\x00\x43\x57\x7a\ -\x79\x67\xdf\x76\x33\x27\x18\x0c\x01\xbc\x02\x20\xee\x3f\xd0\x5a\ -\x2b\x49\x25\xd8\xed\x76\x3c\x1e\x8f\x19\x14\xf0\x86\xe8\x68\xd9\ -\x46\x7f\x6f\x0f\x15\xde\x4a\x0a\x86\x31\x5f\x07\x83\x32\xb7\x87\ -\x81\xcb\x17\xe8\x68\xd9\x46\xa8\x32\x02\x80\x2c\xcb\xf3\xe5\xf0\ -\xb0\xff\x40\x6b\xad\x08\x44\x56\xfa\xfc\x00\xa8\xaa\x8a\x2c\xcb\ -\xc8\xb2\x8c\x28\x8a\x84\xab\x36\xd0\xb9\xb7\xc9\x24\x17\x05\x01\ -\x57\x79\x05\x03\x97\x2f\xd0\xb9\xb7\x89\x70\xd5\x06\x33\x5e\x55\ -\xe7\x4e\x51\x5e\x56\x0e\x10\x11\x81\xcd\x77\x6e\xdc\x44\x2e\x97\ -\x33\xac\x56\x0b\x33\x33\x33\xa8\xaa\x8a\x24\x49\xc8\xb2\x4c\x4d\ -\xa4\x9e\x53\x6d\xcd\xf4\xf7\xf6\xe0\x5e\x11\x60\xb8\xef\x12\xa7\ -\xda\x9a\xa9\x89\xd4\x9b\x84\xaa\xaa\x92\xcf\xeb\x24\x12\x09\x63\ -\xed\x9a\xb5\x00\x4d\x62\x36\x9b\xdd\xe2\xf7\xfb\x71\xb9\x5c\x82\ -\xae\xe7\x29\xcc\xdf\x78\x31\x41\x55\x55\x2a\x4a\x03\x74\x3c\xdb\ -\x40\x7f\x6f\x0f\x6f\x3d\x53\x4f\x45\x69\xc0\xf4\x01\xe4\xf3\x3a\ -\x08\xa0\x28\x8a\x10\x0c\xae\x22\x93\xc9\x96\x5a\x1d\x0e\xc7\xee\ -\x48\x24\x62\xb6\xcb\xcd\xa0\x28\x8d\x7c\x7d\xb2\x71\xd9\xdf\x65\ -\x75\x75\x35\x4e\xa7\xa3\xc5\x0a\x10\x8b\xc5\x98\x9c\x9c\xbc\x79\ -\xdf\x16\x2f\xee\x36\x7c\x5e\xef\x5c\x6b\x16\x3f\x10\x34\x4d\xc3\ -\xe3\xf1\x70\xee\xdc\x39\x1c\x0e\x07\xb2\xab\x84\x52\x57\x19\xa2\ -\x28\x22\x49\x12\x82\x20\x20\x49\x12\x2e\x97\x0b\x55\x55\xf1\xf9\ -\x7c\x44\xa3\xd1\x25\xb6\x48\x2c\x2e\xdc\x4d\xd7\x75\x2c\x16\x0b\ -\x86\x61\x20\x0a\x16\xb2\xd9\x2c\x9a\xa6\x61\x18\x06\x36\x9b\xed\ -\x96\xca\x6f\x84\xa9\x58\x10\x04\xd2\xe9\x34\x75\x75\xb5\xe8\x7a\ -\x1e\x8b\xc5\x82\xae\xeb\x66\xa0\xdd\x6e\xe7\xbf\xc0\x0a\x10\x8d\ -\x46\x0d\xbf\xdf\x3f\xff\x4c\x79\x96\x4d\x72\xbb\xdd\x00\x84\xc3\ -\xe1\x25\x76\x6c\x6c\xcc\x00\x04\xe1\x89\x27\x1f\xdf\x13\x08\xf8\ -\xcf\xf0\x3f\x62\x6c\x6c\x7c\xfb\xdf\xe7\x60\x9f\x53\x70\x22\xd1\ -\x6a\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x13\x8f\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -8910,10 +8999,6 @@ qt_resource_name = "\ \x00\xe0\x1d\x47\ \x00\x66\ \x00\x69\x00\x6c\x00\x65\x00\x2d\x00\x70\x00\x65\x00\x6e\x00\x64\x00\x69\x00\x6e\x00\x67\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x08\ -\x0a\x85\x58\x07\ -\x00\x73\ -\x00\x74\x00\x61\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x12\ \x08\xc2\x5a\x27\ \x00\x43\ @@ -8923,6 +9008,10 @@ qt_resource_name = "\ \x04\x5f\xb4\x38\ \x00\x31\ \x00\x32\x00\x38\x00\x78\x00\x31\x00\x32\x00\x38\ +\x00\x08\ +\x0a\x85\x58\x07\ +\x00\x73\ +\x00\x74\x00\x61\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x15\ \x07\x05\x4c\x07\ \x00\x6d\ @@ -9049,20 +9138,30 @@ qt_resource_name = "\ \x08\x89\x88\x67\ \x00\x70\ \x00\x69\x00\x63\x00\x61\x00\x72\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x15\ +\x00\x24\xf9\x87\ +\x00\x61\ +\x00\x63\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2d\x00\x67\x00\x6f\x00\x2d\x00\x64\x00\x6f\x00\x77\x00\x6e\x00\x2d\x00\x31\x00\x36\ +\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x17\ -\x0d\xcd\xf8\x07\ -\x00\x6d\ -\x00\x65\x00\x64\x00\x69\x00\x61\x00\x2d\x00\x6f\x00\x70\x00\x74\x00\x69\x00\x63\x00\x61\x00\x6c\x00\x2d\x00\x73\x00\x61\x00\x76\ -\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x0e\ -\x0c\xaa\xc0\xa7\ -\x00\x65\ -\x00\x64\x00\x69\x00\x74\x00\x2d\x00\x70\x00\x61\x00\x73\x00\x74\x00\x65\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x09\xec\xdc\x07\ +\x00\x61\ +\x00\x63\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2d\x00\x67\x00\x6f\x00\x2d\x00\x62\x00\x6f\x00\x74\x00\x74\x00\x6f\x00\x6d\x00\x2d\ +\x00\x31\x00\x36\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x11\ \x0f\xe3\xd5\x67\ \x00\x64\ \x00\x6f\x00\x63\x00\x75\x00\x6d\x00\x65\x00\x6e\x00\x74\x00\x2d\x00\x73\x00\x61\x00\x76\x00\x65\x00\x2e\x00\x70\x00\x6e\x00\x67\ \ +\x00\x0e\ +\x0c\xaa\xc0\xa7\ +\x00\x65\ +\x00\x64\x00\x69\x00\x74\x00\x2d\x00\x70\x00\x61\x00\x73\x00\x74\x00\x65\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x17\ +\x0d\xcd\xf8\x07\ +\x00\x6d\ +\x00\x65\x00\x64\x00\x69\x00\x61\x00\x2d\x00\x6f\x00\x70\x00\x74\x00\x69\x00\x63\x00\x61\x00\x6c\x00\x2d\x00\x73\x00\x61\x00\x76\ +\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x10\ \x08\x15\x13\x67\ \x00\x76\ @@ -9112,7 +9211,7 @@ qt_resource_struct = "\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x1a\x00\x00\x00\x02\ \x00\x00\x02\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x57\x19\ -\x00\x00\x02\x8c\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x2f\ +\x00\x00\x02\x8c\x00\x02\x00\x00\x00\x0e\x00\x00\x00\x2f\ \x00\x00\x01\xce\x00\x02\x00\x00\x00\x0e\x00\x00\x00\x21\ \x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x00\xe0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1f\ @@ -9120,13 +9219,13 @@ qt_resource_struct = "\ \x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x02\xde\x00\x00\x00\x00\x00\x01\x00\x00\x5c\xbd\ \x00\x00\x02\xba\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xea\ -\x00\x00\x00\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\ +\x00\x00\x00\x72\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\ \x00\x00\x00\xcc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1c\ \x00\x00\x01\xb8\x00\x00\x00\x00\x00\x01\x00\x00\x53\xf6\ \x00\x00\x00\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x49\x37\ -\x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x08\xa7\ -\x00\x00\x02\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x5a\x50\ \x00\x00\x00\x48\x00\x00\x00\x00\x00\x01\x00\x00\x01\x81\ +\x00\x00\x02\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x5a\x50\ +\x00\x00\x00\x86\x00\x00\x00\x00\x00\x01\x00\x00\x42\x11\ \x00\x00\x01\xfc\x00\x00\x00\x00\x00\x01\x00\x00\x56\x5d\ \x00\x00\x01\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x53\x2b\ \x00\x00\x01\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x51\x9f\ @@ -9137,37 +9236,39 @@ qt_resource_struct = "\ \x00\x00\x01\xde\x00\x00\x00\x00\x00\x01\x00\x00\x55\x9d\ \x00\x00\x01\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x52\x58\ \x00\x00\x01\x20\x00\x00\x00\x00\x00\x01\x00\x00\x50\xdf\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xda\xf0\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x01\xc6\x3d\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xbd\x10\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xd0\xa3\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x02\x1d\x7a\ -\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x9a\xd0\ -\x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x92\xb5\ -\x00\x00\x05\xde\x00\x00\x00\x00\x00\x01\x00\x00\xa8\x86\ -\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x00\xb0\x52\ -\x00\x00\x05\x00\x00\x00\x00\x00\x00\x01\x00\x00\x7f\xe4\ -\x00\x00\x05\x5a\x00\x00\x00\x00\x00\x01\x00\x00\x8f\x5d\ -\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xa3\x6c\ -\x00\x00\x05\x28\x00\x00\x00\x00\x00\x01\x00\x00\x86\x33\ -\x00\x00\x05\xb4\x00\x00\x00\x00\x00\x01\x00\x00\xa5\x70\ -\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\xa0\x45\ -\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\xb3\x29\ -\x00\x00\x05\x86\x00\x00\x00\x00\x00\x01\x00\x00\x96\xa2\ -\x00\x00\x06\x02\x00\x00\x00\x00\x00\x01\x00\x00\xab\x5b\ -\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\xb8\x8e\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xe0\x3a\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x01\xcb\x87\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xc2\x5a\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xd5\xed\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x02\x22\xc4\ +\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\xa0\x1a\ +\x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x97\xff\ +\x00\x00\x06\x42\x00\x00\x00\x00\x00\x01\x00\x00\xad\xd0\ +\x00\x00\x06\x8e\x00\x00\x00\x00\x00\x01\x00\x00\xb5\x9c\ +\x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x85\x2e\ +\x00\x00\x05\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x94\xa7\ +\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xa8\xb6\ +\x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x8b\x7d\ +\x00\x00\x06\x18\x00\x00\x00\x00\x00\x01\x00\x00\xaa\xba\ +\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\xa5\x8f\ +\x00\x00\x05\x0a\x00\x00\x00\x00\x00\x01\x00\x00\xbc\xf5\ +\x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x00\x9b\xec\ +\x00\x00\x06\x66\x00\x00\x00\x00\x00\x01\x00\x00\xb0\xa5\ +\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\xb8\x73\ +\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x73\x34\ \x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x68\x62\ \x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x60\x89\ \x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x6d\xff\ \x00\x00\x03\xae\x00\x00\x00\x00\x00\x01\x00\x00\x65\x37\ -\x00\x00\x04\xda\x00\x00\x00\x00\x00\x01\x00\x00\x7c\x50\ +\x00\x00\x05\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x81\x9a\ \x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\x6f\xbb\ +\x00\x00\x04\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x75\xe3\ \x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x09\ -\x00\x00\x04\x90\x00\x00\x00\x00\x00\x01\x00\x00\x76\x88\ +\x00\x00\x04\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x7c\x11\ \x00\x00\x03\x2c\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x39\ -\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x73\x34\ +\x00\x00\x05\x0a\x00\x00\x00\x00\x00\x01\x00\x00\x7e\x46\ \x00\x00\x03\x88\x00\x00\x00\x00\x00\x01\x00\x00\x62\xa6\ -\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x78\xbd\ +\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x78\x7e\ " def qInitResources(): diff --git a/picard/ui/infostatus.py b/picard/ui/infostatus.py index 3c8fbad9b..6bbf79f12 100644 --- a/picard/ui/infostatus.py +++ b/picard/ui/infostatus.py @@ -39,14 +39,14 @@ class InfoStatus(QtGui.QWidget, Ui_InfoStatus): self.label1.setPixmap(self.icon_file.pixmap(size)) self.label2.setPixmap(self.icon_cd.pixmap(size)) self.label3.setPixmap(self.icon_file_pending.pixmap(size)) - self.label4.setPixmap(self.icon_web.pixmap(size, QIcon.Disabled)) + self.label4.setPixmap(self.icon_download.pixmap(size, QIcon.Disabled)) self._init_tooltips() def _create_icons(self): self.icon_cd = icontheme.lookup('media-optical') self.icon_file = QtGui.QIcon(":/images/file.png") self.icon_file_pending = QtGui.QIcon(":/images/file-pending.png") - self.icon_web = icontheme.lookup('lookup-musicbrainz') + self.icon_download = QtGui.QIcon(":/images/16x16/action-go-down-16.png") def _init_tooltips(self): t1 = _("Files") @@ -76,5 +76,5 @@ class InfoStatus(QtGui.QWidget, Ui_InfoStatus): enabled = QIcon.Disabled else: enabled = QIcon.Normal - self.label4.setPixmap(self.icon_web.pixmap(self._size, enabled)) + self.label4.setPixmap(self.icon_download.pixmap(self._size, enabled)) self.val4.setText(unicode(num)) diff --git a/resources/images/16x16/action-go-down-16.png b/resources/images/16x16/action-go-down-16.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd7fccdf06321880f69d65631a363e4b813ba04 GIT binary patch literal 683 zcmV;c0#yBpP)i}4~9FCHr5K@{qtRq-ka(iJ?ZP=uTm zA)-=KTXM11RS2~$6nm&uv{cQSuKf$?CbP4jht?XK7~kVJ-#qia^UW|KLbc-EY953z z>WRV7dqi_}NvUZfgl}C)!*&F0M_|b+V97E80CykVr~%gk05Haon|Y41T|%KagO77# zXg_$ht|?xxSRKlv`0H+L2mld?91sx{?rsQB5|>q-9K_b`yI?say^?H5vawt?QN0%L zQr8VKjyDQ9NJT;|(TgXq`xKW7BF8I9f|vxjL{S!?dO5hlaQ@UaF9;B#f^;@jnqQnt zF(N{uTTQn`k0*~rlb-kaSCCvlqKp-L5!3TI5NItHN89$(7@Zg?Pfm^Zz3vh1d@XXv z%dw2{#h9WC`nbQFn~GCVFR$PcMeY21IKx0j@A8ZjM9Y6Ue=LTlryr z>(@2W+wdKbgN~t*f$yfVK)ah_*yWGG{JKoJQ9bX-)!YpMx+aPwk<4VDSzECWL8lc@ z`=3~j{FA#{Y~yeIt$3GuF4Da7HUP}#KVRBt{l5SJIDAFD4* - images/CoverArtShadow.png + images/16x16/action-go-down-16.png + images/16x16/applications-system.png images/16x16/dialog-error.png - images/file.png - images/file-pending.png - images/note.png - images/star.png - images/star-gray.png + images/16x16/document-open.png + images/16x16/document-save.png + images/16x16/edit-cut.png + images/16x16/edit-paste.png + images/16x16/folder.png + images/16x16/media-optical-saved.png + images/16x16/media-optical.png images/16x16/picard.png + images/16x16/preferences-desktop.png + images/16x16/view-refresh.png + images/22x22/document-open.png + images/22x22/document-save.png + images/22x22/folder.png + images/22x22/list-remove.png + images/22x22/lookup-musicbrainz.png + images/22x22/media-optical-saved.png + images/22x22/media-optical.png + images/22x22/picard-analyze.png + images/22x22/picard-auto-tag.png + images/22x22/picard-cluster.png + images/22x22/picard-edit-tags.png + images/22x22/picard-submit.png + images/22x22/preferences-desktop.png + images/22x22/system-search.png images/24x24/picard.png images/32x32/picard.png images/48x48/picard.png images/128x128/picard.png images/256x256/picard.png - images/22x22/picard-analyze.png - images/16x16/media-optical.png - images/16x16/media-optical-saved.png - images/22x22/media-optical.png - images/22x22/media-optical-saved.png - images/22x22/list-remove.png - images/22x22/system-search.png - images/22x22/picard-submit.png - images/22x22/lookup-musicbrainz.png + images/CoverArtShadow.png + images/file-pending.png + images/file.png images/match-50.png images/match-60.png images/match-70.png @@ -34,21 +47,9 @@ images/match-pending-80.png images/match-pending-90.png images/match-pending-100.png - images/22x22/picard-auto-tag.png + images/note.png + images/star-gray.png + images/star.png images/track-saved.png - images/22x22/picard-edit-tags.png - images/16x16/edit-cut.png - images/16x16/edit-paste.png - images/16x16/document-open.png - images/22x22/document-open.png - images/16x16/folder.png - images/22x22/folder.png - images/16x16/preferences-desktop.png - images/22x22/preferences-desktop.png - images/16x16/document-save.png - images/22x22/document-save.png - images/16x16/view-refresh.png - images/22x22/picard-cluster.png - images/16x16/applications-system.png From 90c43c72e1ebb4303abee128bc86fb536f86b685 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 11 Jun 2013 12:53:14 -0500 Subject: [PATCH 37/75] Add @throttle decorator to reduce the number of GUI updates --- picard/ui/mainwindow.py | 4 +++- picard/ui/metadatabox.py | 3 ++- picard/util/__init__.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 1c4b05c0b..19b6428b0 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -36,7 +36,7 @@ from picard.ui.options.dialog import OptionsDialog from picard.ui.infodialog import FileInfoDialog, AlbumInfoDialog from picard.ui.infostatus import InfoStatus from picard.ui.passworddialog import PasswordDialog -from picard.util import icontheme, webbrowser2, find_existing_path +from picard.util import icontheme, webbrowser2, find_existing_path, throttle from picard.util.cdrom import get_cdrom_drives from picard.plugin import ExtensionPoint @@ -220,6 +220,7 @@ class MainWindow(QtGui.QMainWindow): self.tagger.listen_port_changed.connect(self.update_statusbar_listen_port) self.update_statusbar_stats() + @throttle(250) def update_statusbar_stats(self): """Updates the status bar information.""" self.infostatus.setFiles(len(self.tagger.files)) @@ -734,6 +735,7 @@ been merged with that of single artist albums."""), def browser_lookup(self): self.tagger.browser_lookup(self.selected_objects[0]) + @throttle(100) def update_actions(self): can_remove = False can_save = False diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index 7e9e53c5e..884682b7c 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -25,7 +25,7 @@ from picard.album import Album from picard.cluster import Cluster from picard.track import Track from picard.file import File -from picard.util import partial, format_time +from picard.util import partial, format_time, throttle from picard.util.tags import display_tag_name from picard.ui.edittagdialog import EditTagDialog from picard.metadata import MULTI_VALUED_JOINER @@ -327,6 +327,7 @@ class MetadataBox(QtGui.QTableWidget): self.objects = objects self.selection_mutex.unlock() + @throttle(100) def update(self): if self.editing: return diff --git a/picard/util/__init__.py b/picard/util/__init__.py index 8851e0113..c502b4c99 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -22,6 +22,7 @@ import os import re import sys import unicodedata +from time import time from PyQt4 import QtCore from encodings import rot_13; from string import Template @@ -336,3 +337,39 @@ def parse_amazon_url(url): if match is not None: return match.groupdict() return None + + +def throttle(interval): + """ + Throttle a function so that it will only execute once per ``interval`` + (specified in milliseconds). + """ + mutex = QtCore.QMutex() + + def decorator(func): + def later(*args, **kwargs): + mutex.lock() + func(*args, **kwargs) + decorator.prev = time() + decorator.is_ticking = False + mutex.unlock() + + def throttled_func(*args, **kwargs): + if decorator.is_ticking: + return + mutex.lock() + now = time() + r = interval - (now-decorator.prev)*1000.0 + if r <= 0: + func(*args, **kwargs) + decorator.prev = now + else: + QtCore.QTimer.singleShot(r, partial(later, *args, **kwargs)) + decorator.is_ticking = True + mutex.unlock() + + return throttled_func + + decorator.prev = 0 + decorator.is_ticking = False + return decorator From fd30cb62ee5a936d7652ea63153a5276979c143a Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 30 Jun 2013 16:15:27 -0500 Subject: [PATCH 38/75] functools.partial is 2.5+ --- picard/acoustid.py | 3 ++- picard/acoustidmanager.py | 2 +- picard/collection.py | 2 +- picard/coverart.py | 6 ++++-- picard/file.py | 2 +- picard/releasegroup.py | 2 +- picard/tagger.py | 2 +- picard/track.py | 3 ++- picard/ui/itemviews.py | 3 ++- picard/ui/metadatabox.py | 3 ++- picard/ui/options/renaming.py | 4 ++-- picard/util/__init__.py | 15 +-------------- picard/webservice.py | 2 +- 13 files changed, 21 insertions(+), 28 deletions(-) diff --git a/picard/acoustid.py b/picard/acoustid.py index ecbc98471..79aa1ad5a 100644 --- a/picard/acoustid.py +++ b/picard/acoustid.py @@ -18,10 +18,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from collections import deque +from functools import partial from PyQt4 import QtCore from picard import config, log from picard.const import ACOUSTID_KEY, FPCALC_NAMES -from picard.util import partial, call_next, find_executable +from picard.util import call_next, find_executable from picard.webservice import XmlNode diff --git a/picard/acoustidmanager.py b/picard/acoustidmanager.py index 3cbf7b09f..3237daa23 100644 --- a/picard/acoustidmanager.py +++ b/picard/acoustidmanager.py @@ -17,8 +17,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +from functools import partial from PyQt4 import QtCore -from picard.util import partial class Submission(object): diff --git a/picard/collection.py b/picard/collection.py index 1f49cd559..1c1ab38cd 100644 --- a/picard/collection.py +++ b/picard/collection.py @@ -17,9 +17,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +from functools import partial from PyQt4 import QtCore from picard import config -from picard.util import partial user_collections = {} diff --git a/picard/coverart.py b/picard/coverart.py index c2c37001a..7df342c81 100644 --- a/picard/coverart.py +++ b/picard/coverart.py @@ -20,14 +20,15 @@ # 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. + import json import re import traceback import picard.webservice - +from functools import partial from picard import config, log from picard.metadata import Metadata, is_front_image -from picard.util import partial, mimetype, parse_amazon_url +from picard.util import mimetype, parse_amazon_url from PyQt4.QtCore import QUrl, QObject # data transliterated from the perl stuff used to find cover art for the @@ -280,6 +281,7 @@ def _process_url_relation(try_list, relation): return True return False + def _process_asin_relation(try_list, relation): amz = parse_amazon_url(relation.target[0].text) if amz is not None: diff --git a/picard/file.py b/picard/file.py index d1402ba43..a97254a1e 100644 --- a/picard/file.py +++ b/picard/file.py @@ -24,6 +24,7 @@ import shutil import sys import re import unicodedata +from functools import partial from operator import itemgetter from collections import defaultdict from PyQt4 import QtCore @@ -41,7 +42,6 @@ from picard.util import ( replace_win32_incompat, replace_non_ascii, sanitize_filename, - partial, unaccent, format_time, pathcmp, diff --git a/picard/releasegroup.py b/picard/releasegroup.py index 6f740a4ba..d3a222f94 100644 --- a/picard/releasegroup.py +++ b/picard/releasegroup.py @@ -18,12 +18,12 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import traceback +from functools import partial from PyQt4 import QtCore from picard import config, log from picard.metadata import Metadata from picard.dataobj import DataObject from picard.mbxml import media_formats_from_node, label_info_from_node -from picard.util import partial class ReleaseGroup(DataObject): diff --git a/picard/tagger.py b/picard/tagger.py index baf1970b4..90f27d022 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -26,6 +26,7 @@ import shutil import signal import sys from collections import deque +from functools import partial from itertools import chain # A "fix" for http://python.org/sf/1438480 @@ -58,7 +59,6 @@ from picard.acoustidmanager import AcoustIDManager from picard.util import ( decode_filename, encode_filename, - partial, queue, thread, mbid_validate, diff --git a/picard/track.py b/picard/track.py index c29c9c00a..936e5cdf5 100644 --- a/picard/track.py +++ b/picard/track.py @@ -18,10 +18,11 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +from functools import partial from picard import config, log from picard.metadata import Metadata, run_track_metadata_processors from picard.dataobj import DataObject -from picard.util import asciipunct, partial +from picard.util import asciipunct from picard.mbxml import recording_to_metadata from picard.script import ScriptParser from picard.const import VARIOUS_ARTISTS_ID diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 7fb9bcdc2..c3910487f 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -19,13 +19,14 @@ import os import re +from functools import partial from PyQt4 import QtCore, QtGui from picard import config, log from picard.album import Album, NatAlbum from picard.cluster import Cluster, ClusterList, UnmatchedFiles from picard.file import File from picard.track import Track, NonAlbumTrack -from picard.util import encode_filename, icontheme, partial +from picard.util import encode_filename, icontheme from picard.plugin import ExtensionPoint from picard.ui.ratingwidget import RatingWidget from picard.ui.collectionmenu import CollectionMenu diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index 884682b7c..0a42afc89 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -20,12 +20,13 @@ from PyQt4 import QtCore, QtGui from collections import defaultdict +from functools import partial from picard import config from picard.album import Album from picard.cluster import Cluster from picard.track import Track from picard.file import File -from picard.util import partial, format_time, throttle +from picard.util import format_time, throttle from picard.util.tags import display_tag_name from picard.ui.edittagdialog import EditTagDialog from picard.metadata import MULTI_VALUED_JOINER diff --git a/picard/ui/options/renaming.py b/picard/ui/options/renaming.py index 24da4d6ce..042f08292 100644 --- a/picard/ui/options/renaming.py +++ b/picard/ui/options/renaming.py @@ -20,6 +20,7 @@ import os.path import sys +from functools import partial from PyQt4 import QtCore, QtGui from picard import config from picard.file import File @@ -27,7 +28,6 @@ from picard.script import ScriptParser, SyntaxError, UnknownFunction from picard.ui.options import OptionsPage, OptionsCheckError, register_options_page from picard.ui.ui_options_renaming import Ui_RenamingOptionsPage from picard.ui.options.scripting import TaggerScriptSyntaxHighlighter -from picard.util import partial class RenamingOptionsPage(OptionsPage): @@ -61,7 +61,7 @@ class RenamingOptionsPage(OptionsPage): self.ui.move_files.clicked.connect(self.update_examples) self.ui.move_files_to.editingFinished.connect(self.update_examples) - # The following code is there to fix + # The following code is there to fix # http://tickets.musicbrainz.org/browse/PICARD-417 # In some older version of PyQt/sip it's impossible to connect a signal # emitting an `int` to a slot expecting a `bool`. diff --git a/picard/util/__init__.py b/picard/util/__init__.py index c502b4c99..6cd980b23 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -26,6 +26,7 @@ from time import time from PyQt4 import QtCore from encodings import rot_13; from string import Template +from functools import partial def asciipunct(s): @@ -253,20 +254,6 @@ def translate_from_sortname(name, sortname): return name -try: - from functools import partial -except ImportError: - def partial(func, *args, **keywords): - def newfunc(*fargs, **fkeywords): - newkeywords = keywords.copy() - newkeywords.update(fkeywords) - return func(*(args + fargs), **newkeywords) - newfunc.func = func - newfunc.args = args - newfunc.keywords = keywords - return newfunc - - def find_existing_path(path): path = encode_filename(path) while path and not os.path.isdir(path): diff --git a/picard/webservice.py b/picard/webservice.py index b734da840..f869ab20a 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -28,11 +28,11 @@ import re import time import os.path from collections import deque, defaultdict +from functools import partial from PyQt4 import QtCore, QtNetwork from PyQt4.QtGui import QDesktopServices from PyQt4.QtCore import QUrl, QXmlStreamReader from picard import version_string, config, log -from picard.util import partial from picard.const import ACOUSTID_KEY, ACOUSTID_HOST From fe17c2cb3c403ab7d131abca2bbd89fb5f66d501 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 30 Jun 2013 17:56:00 -0500 Subject: [PATCH 39/75] Copy value list in Metadata.update --- picard/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/metadata.py b/picard/metadata.py index 053e19f63..a083972a6 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -231,7 +231,7 @@ class Metadata(dict): def update(self, other): for key in other.iterkeys(): - self.set(key, other.getall(key)) + self.set(key, other.getall(key)[:]) if other.images: self.images = other.images[:] if other.length: From 403bafbbaaa56c299eb9e1bb65ab649b8a440374 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Mon, 1 Jul 2013 13:29:57 +0200 Subject: [PATCH 40/75] Deprecated failUnlessEqual() -> assertEqual() http://docs.python.org/2/library/unittest.html#deprecated-aliases --- test/test_amazon_urls.py | 10 +++---- test/test_compatid3.py | 22 +++++++-------- test/test_mbxml.py | 42 ++++++++++++++-------------- test/test_similarity.py | 4 +-- test/test_utils.py | 60 ++++++++++++++++++++-------------------- 5 files changed, 69 insertions(+), 69 deletions(-) diff --git a/test/test_amazon_urls.py b/test/test_amazon_urls.py index de19306b6..eff50fcc6 100644 --- a/test/test_amazon_urls.py +++ b/test/test_amazon_urls.py @@ -10,30 +10,30 @@ class ParseAmazonUrlTest(unittest.TestCase): url = 'http://www.amazon.com/dp/020530902X' expected = {'asin': '020530902X', 'host': 'amazon.com'} r = parse_amazon_url(url) - self.failUnlessEqual(r, expected) + self.assertEqual(r, expected) def test_2(self): url = 'http://ec1.amazon.co.jp/gp/product/020530902X' expected = {'asin': '020530902X', 'host': 'ec1.amazon.co.jp'} r = parse_amazon_url(url) - self.failUnlessEqual(r, expected) + self.assertEqual(r, expected) def test_3(self): url = 'http://amazon.com/Dark-Side-Moon-Pink-Floyd/dp/B004ZN9RWK/ref=sr_1_1?s=music&ie=UTF8&qid=1372605047&sr=1-1&keywords=pink+floyd+dark+side+of+the+moon' expected = {'asin': 'B004ZN9RWK', 'host': 'amazon.com'} r = parse_amazon_url(url) - self.failUnlessEqual(r, expected) + self.assertEqual(r, expected) def test_4(self): #incorrect ASIN url = 'http://www.amazon.com/dp/A20530902X' expected = None r = parse_amazon_url(url) - self.failUnlessEqual(r, expected) + self.assertEqual(r, expected) def test_5(self): #incorrect ASIN url = 'http://www.amazon.com/dp/020530902x' expected = None r = parse_amazon_url(url) - self.failUnlessEqual(r, expected) + self.assertEqual(r, expected) diff --git a/test/test_compatid3.py b/test/test_compatid3.py index 22293a870..c497286ec 100644 --- a/test/test_compatid3.py +++ b/test/test_compatid3.py @@ -10,51 +10,51 @@ class UpdateToV23Test(unittest.TestCase): tags = compatid3.CompatID3() tags.add(id3.TALB(encoding=0, text=["123","abc"])) tags.update_to_v23() - self.failUnlessEqual(tags["TALB"].text, ["123/abc"]) + self.assertEqual(tags["TALB"].text, ["123/abc"]) def test_encoding(self): tags = compatid3.CompatID3() tags.add(id3.TALB(encoding=2, text="abc")) tags.add(id3.TIT2(encoding=3, text="abc")) tags.update_to_v23() - self.failUnlessEqual(tags["TALB"].encoding, 1) - self.failUnlessEqual(tags["TIT2"].encoding, 1) + self.assertEqual(tags["TALB"].encoding, 1) + self.assertEqual(tags["TIT2"].encoding, 1) def test_tdrc(self): tags = compatid3.CompatID3() tags.add(id3.TDRC(encoding=1, text="2003-04-05 12:03")) tags.update_to_v23() - self.failUnlessEqual(tags["TYER"].text, ["2003"]) - self.failUnlessEqual(tags["TDAT"].text, ["0504"]) - self.failUnlessEqual(tags["TIME"].text, ["1203"]) + self.assertEqual(tags["TYER"].text, ["2003"]) + self.assertEqual(tags["TDAT"].text, ["0504"]) + self.assertEqual(tags["TIME"].text, ["1203"]) def test_tdor(self): tags = compatid3.CompatID3() tags.add(id3.TDOR(encoding=1, text="2003-04-05 12:03")) tags.update_to_v23() - self.failUnlessEqual(tags["TORY"].text, ["2003"]) + self.assertEqual(tags["TORY"].text, ["2003"]) def test_genre_from_v24_1(self): tags = compatid3.CompatID3() tags.add(id3.TCON(encoding=1, text=["4","Rock"])) tags.update_to_v23() - self.failUnlessEqual(tags["TCON"].text, ["Disco/Rock"]) + self.assertEqual(tags["TCON"].text, ["Disco/Rock"]) def test_genre_from_v24_2(self): tags = compatid3.CompatID3() tags.add(id3.TCON(encoding=1, text=["RX", "3", "CR"])) tags.update_to_v23() - self.failUnlessEqual(tags["TCON"].text, ["Remix/Dance/Cover"]) + self.assertEqual(tags["TCON"].text, ["Remix/Dance/Cover"]) def test_genre_from_v23_1(self): tags = compatid3.CompatID3() tags.add(id3.TCON(encoding=1, text=["(4)Rock"])) tags.update_to_v23() - self.failUnlessEqual(tags["TCON"].text, ["Disco/Rock"]) + self.assertEqual(tags["TCON"].text, ["Disco/Rock"]) def test_genre_from_v23_2(self): tags = compatid3.CompatID3() tags.add(id3.TCON(encoding=1, text=["(RX)(3)(CR)"])) tags.update_to_v23() - self.failUnlessEqual(tags["TCON"].text, ["Remix/Dance/Cover"]) + self.assertEqual(tags["TCON"].text, ["Remix/Dance/Cover"]) diff --git a/test/test_mbxml.py b/test/test_mbxml.py index 7f77058da..97884dbfb 100644 --- a/test/test_mbxml.py +++ b/test/test_mbxml.py @@ -70,13 +70,13 @@ class TrackTest(unittest.TestCase): track = Track() m = track.metadata = Metadata() track_to_metadata(node, track) - self.failUnlessEqual('123', m['musicbrainz_trackid']) - self.failUnlessEqual('456; 789', m['musicbrainz_artistid']) - self.failUnlessEqual('Foo', m['title']) - self.failUnlessEqual('Foo Bar & Baz', m['artist']) - self.failUnlessEqual('Bar, Foo & Baz', m['artistsort']) - self.failUnlessEqual('workid123', m['musicbrainz_workid']) - self.failUnlessEqual('eng', m['language']) + self.assertEqual('123', m['musicbrainz_trackid']) + self.assertEqual('456; 789', m['musicbrainz_artistid']) + self.assertEqual('Foo', m['title']) + self.assertEqual('Foo Bar & Baz', m['artist']) + self.assertEqual('Bar, Foo & Baz', m['artistsort']) + self.assertEqual('workid123', m['musicbrainz_workid']) + self.assertEqual('eng', m['language']) class ReleaseTest(unittest.TestCase): @@ -117,17 +117,17 @@ class ReleaseTest(unittest.TestCase): }) m = Metadata() release_to_metadata(release, m) - self.failUnlessEqual('123', m['musicbrainz_albumid']) - self.failUnlessEqual('456; 789', m['musicbrainz_albumartistid']) - self.failUnlessEqual('Foo', m['album']) - self.failUnlessEqual('official', m['releasestatus']) - self.failUnlessEqual('eng', m['~releaselanguage']) - self.failUnlessEqual('Latn', m['script']) - self.failUnlessEqual('Foo Bar & Baz', m['albumartist']) - self.failUnlessEqual('Bar, Foo & Baz', m['albumartistsort']) - self.failUnlessEqual('2009-08-07', m['date']) - self.failUnlessEqual('GB', m['releasecountry']) - self.failUnlessEqual('012345678929', m['barcode']) - self.failUnlessEqual('B123456789', m['asin']) - self.failUnlessEqual('ABC', m['label']) - self.failUnlessEqual('ABC 123', m['catalognumber']) + self.assertEqual('123', m['musicbrainz_albumid']) + self.assertEqual('456; 789', m['musicbrainz_albumartistid']) + self.assertEqual('Foo', m['album']) + self.assertEqual('official', m['releasestatus']) + self.assertEqual('eng', m['~releaselanguage']) + self.assertEqual('Latn', m['script']) + self.assertEqual('Foo Bar & Baz', m['albumartist']) + self.assertEqual('Bar, Foo & Baz', m['albumartistsort']) + self.assertEqual('2009-08-07', m['date']) + self.assertEqual('GB', m['releasecountry']) + self.assertEqual('012345678929', m['barcode']) + self.assertEqual('B123456789', m['asin']) + self.assertEqual('ABC', m['label']) + self.assertEqual('ABC 123', m['catalognumber']) diff --git a/test/test_similarity.py b/test/test_similarity.py index 1473a633e..be56e79ca 100644 --- a/test/test_similarity.py +++ b/test/test_similarity.py @@ -4,7 +4,7 @@ from picard.similarity import similarity class SimilarityTest(unittest.TestCase): def test_correct(self): - self.failUnlessEqual(similarity(u"K!", u"K!"), 1.0) - self.failUnlessEqual(similarity(u"BBB", u"AAA"), 0.0) + self.assertEqual(similarity(u"K!", u"K!"), 1.0) + self.assertEqual(similarity(u"BBB", u"AAA"), 0.0) self.failUnlessAlmostEqual(similarity(u"ABC", u"ABB"), 0.7, 1) diff --git a/test/test_utils.py b/test/test_utils.py index 131cf5c1b..25e1f02d6 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -8,12 +8,12 @@ from picard import util class UnaccentTest(unittest.TestCase): def test_correct(self): - self.failUnlessEqual(util.unaccent(u"Lukáš"), u"Lukas") - self.failUnlessEqual(util.unaccent(u"Björk"), u"Bjork") - self.failUnlessEqual(util.unaccent(u"Trentemøller"), u"Trentemoller") - self.failUnlessEqual(util.unaccent(u"小室哲哉"), u"小室哲哉") - self.failUnlessEqual(util.unaccent(u"Ænima"), u"AEnima") - self.failUnlessEqual(util.unaccent(u"ænima"), u"aenima") + self.assertEqual(util.unaccent(u"Lukáš"), u"Lukas") + self.assertEqual(util.unaccent(u"Björk"), u"Bjork") + self.assertEqual(util.unaccent(u"Trentemøller"), u"Trentemoller") + self.assertEqual(util.unaccent(u"小室哲哉"), u"小室哲哉") + self.assertEqual(util.unaccent(u"Ænima"), u"AEnima") + self.assertEqual(util.unaccent(u"ænima"), u"aenima") def test_incorrect(self): self.failIfEqual(util.unaccent(u"Björk"), u"Björk") @@ -23,10 +23,10 @@ class UnaccentTest(unittest.TestCase): class ReplaceNonAsciiTest(unittest.TestCase): def test_correct(self): - self.failUnlessEqual(util.replace_non_ascii(u"Lukáš"), u"Luk__") - self.failUnlessEqual(util.replace_non_ascii(u"Björk"), u"Bj_rk") - self.failUnlessEqual(util.replace_non_ascii(u"Trentemøller"), u"Trentem_ller") - self.failUnlessEqual(util.replace_non_ascii(u"小室哲哉"), u"____") + self.assertEqual(util.replace_non_ascii(u"Lukáš"), u"Luk__") + self.assertEqual(util.replace_non_ascii(u"Björk"), u"Bj_rk") + self.assertEqual(util.replace_non_ascii(u"Trentemøller"), u"Trentem_ller") + self.assertEqual(util.replace_non_ascii(u"小室哲哉"), u"____") def test_incorrect(self): self.failIfEqual(util.replace_non_ascii(u"Lukáš"), u"Lukáš") @@ -36,9 +36,9 @@ class ReplaceNonAsciiTest(unittest.TestCase): class ReplaceWin32IncompatTest(unittest.TestCase): def test_correct(self): - self.failUnlessEqual(util.replace_win32_incompat("c:\\test\\te\"st/2"), + self.assertEqual(util.replace_win32_incompat("c:\\test\\te\"st/2"), "c_\\test\\te_st/2") - self.failUnlessEqual(util.replace_win32_incompat("A\"*:<>?|b"), + self.assertEqual(util.replace_win32_incompat("A\"*:<>?|b"), "A_______b") def test_incorrect(self): @@ -49,12 +49,12 @@ class ReplaceWin32IncompatTest(unittest.TestCase): class SanitizeDateTest(unittest.TestCase): def test_correct(self): - self.failUnlessEqual(util.sanitize_date("2006--"), "2006") - self.failUnlessEqual(util.sanitize_date("2006--02"), "2006") - self.failUnlessEqual(util.sanitize_date("2006 "), "2006") - self.failUnlessEqual(util.sanitize_date("2006 02"), "") - self.failUnlessEqual(util.sanitize_date("2006.02"), "") - self.failUnlessEqual(util.sanitize_date("2006-02"), "2006-02") + self.assertEqual(util.sanitize_date("2006--"), "2006") + self.assertEqual(util.sanitize_date("2006--02"), "2006") + self.assertEqual(util.sanitize_date("2006 "), "2006") + self.assertEqual(util.sanitize_date("2006 02"), "") + self.assertEqual(util.sanitize_date("2006.02"), "") + self.assertEqual(util.sanitize_date("2006-02"), "2006-02") def test_incorrect(self): self.failIfEqual(util.sanitize_date("2006--02"), "2006-02") @@ -64,42 +64,42 @@ class ShortFilenameTest(unittest.TestCase): def test_short(self): fn = util.make_short_filename("/home/me/", os.path.join("a1234567890", "b1234567890"), 255) - self.failUnlessEqual(fn, os.path.join("a1234567890", "b1234567890")) + self.assertEqual(fn, os.path.join("a1234567890", "b1234567890")) def test_long(self): fn = util.make_short_filename("/home/me/", os.path.join("a1234567890", "b1234567890"), 20) - self.failUnlessEqual(fn, os.path.join("a123456", "b1")) + self.assertEqual(fn, os.path.join("a123456", "b1")) def test_long_2(self): fn = util.make_short_filename("/home/me/", os.path.join("a1234567890", "b1234567890"), 22) - self.failUnlessEqual(fn, os.path.join("a12345678", "b1")) + self.assertEqual(fn, os.path.join("a12345678", "b1")) def test_too_long(self): self.failUnlessRaises(IOError, util.make_short_filename, "/home/me/", os.path.join("a1234567890", "b1234567890"), 10) def test_whitespace(self): fn = util.make_short_filename("/home/me/", os.path.join("a1234567890 ", " b1234567890 "), 22) - self.failUnlessEqual(fn, os.path.join("a12345678", "b1")) + self.assertEqual(fn, os.path.join("a12345678", "b1")) class TranslateArtistTest(unittest.TestCase): def test_latin(self): - self.failUnlessEqual(u"Jean Michel Jarre", util.translate_from_sortname(u"Jean Michel Jarre", u"Jarre, Jean Michel")) + self.assertEqual(u"Jean Michel Jarre", util.translate_from_sortname(u"Jean Michel Jarre", u"Jarre, Jean Michel")) self.failIfEqual(u"Jarre, Jean Michel", util.translate_from_sortname(u"Jean Michel Jarre", u"Jarre, Jean Michel")) def test_kanji(self): - self.failUnlessEqual(u"Tetsuya Komuro", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya")) + self.assertEqual(u"Tetsuya Komuro", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya")) self.failIfEqual(u"Komuro, Tetsuya", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya")) self.failIfEqual(u"小室哲哉", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya")) def test_kanji2(self): - self.failUnlessEqual(u"Ayumi Hamasaki & Keiko", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko")) + self.assertEqual(u"Ayumi Hamasaki & Keiko", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko")) self.failIfEqual(u"浜崎あゆみ & KEIKO", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko")) self.failIfEqual(u"Hamasaki, Ayumi & Keiko", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko")) def test_cyrillic(self): - self.failUnlessEqual(U"Pyotr Ilyich Tchaikovsky", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) + self.assertEqual(U"Pyotr Ilyich Tchaikovsky", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) self.failIfEqual(u"Tchaikovsky, Pyotr Ilyich", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) self.failIfEqual(u"Пётр Ильич Чайковский", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) @@ -107,10 +107,10 @@ class TranslateArtistTest(unittest.TestCase): class FormatTimeTest(unittest.TestCase): def test(self): - self.failUnlessEqual("?:??", util.format_time(0)) - self.failUnlessEqual("3:00", util.format_time(179750)) - self.failUnlessEqual("3:00", util.format_time(179500)) - self.failUnlessEqual("2:59", util.format_time(179499)) + self.assertEqual("?:??", util.format_time(0)) + self.assertEqual("3:00", util.format_time(179750)) + self.assertEqual("3:00", util.format_time(179500)) + self.assertEqual("2:59", util.format_time(179499)) class LoadReleaseTypeScoresTest(unittest.TestCase): From 6062fe404ea0b5ba3ebc25ab34f3651e36c36d6f Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Mon, 1 Jul 2013 13:31:41 +0200 Subject: [PATCH 41/75] Deprecated failIfEqual() -> assertNotEqual() http://docs.python.org/2/library/unittest.html#deprecated-aliases --- test/test_utils.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/test_utils.py b/test/test_utils.py index 25e1f02d6..0e839c9cb 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -16,8 +16,8 @@ class UnaccentTest(unittest.TestCase): self.assertEqual(util.unaccent(u"ænima"), u"aenima") def test_incorrect(self): - self.failIfEqual(util.unaccent(u"Björk"), u"Björk") - self.failIfEqual(util.unaccent(u"小室哲哉"), u"Tetsuya Komuro") + self.assertNotEqual(util.unaccent(u"Björk"), u"Björk") + self.assertNotEqual(util.unaccent(u"小室哲哉"), u"Tetsuya Komuro") class ReplaceNonAsciiTest(unittest.TestCase): @@ -29,8 +29,8 @@ class ReplaceNonAsciiTest(unittest.TestCase): self.assertEqual(util.replace_non_ascii(u"小室哲哉"), u"____") def test_incorrect(self): - self.failIfEqual(util.replace_non_ascii(u"Lukáš"), u"Lukáš") - self.failIfEqual(util.replace_non_ascii(u"Lukáš"), u"Luk____") + self.assertNotEqual(util.replace_non_ascii(u"Lukáš"), u"Lukáš") + self.assertNotEqual(util.replace_non_ascii(u"Lukáš"), u"Luk____") class ReplaceWin32IncompatTest(unittest.TestCase): @@ -42,7 +42,7 @@ class ReplaceWin32IncompatTest(unittest.TestCase): "A_______b") def test_incorrect(self): - self.failIfEqual(util.replace_win32_incompat("c:\\test\\te\"st2"), + self.assertNotEqual(util.replace_win32_incompat("c:\\test\\te\"st2"), "c:\\test\\te\"st2") @@ -57,8 +57,8 @@ class SanitizeDateTest(unittest.TestCase): self.assertEqual(util.sanitize_date("2006-02"), "2006-02") def test_incorrect(self): - self.failIfEqual(util.sanitize_date("2006--02"), "2006-02") - self.failIfEqual(util.sanitize_date("2006.03.02"), "2006-03-02") + self.assertNotEqual(util.sanitize_date("2006--02"), "2006-02") + self.assertNotEqual(util.sanitize_date("2006.03.02"), "2006-03-02") class ShortFilenameTest(unittest.TestCase): @@ -86,22 +86,22 @@ class TranslateArtistTest(unittest.TestCase): def test_latin(self): self.assertEqual(u"Jean Michel Jarre", util.translate_from_sortname(u"Jean Michel Jarre", u"Jarre, Jean Michel")) - self.failIfEqual(u"Jarre, Jean Michel", util.translate_from_sortname(u"Jean Michel Jarre", u"Jarre, Jean Michel")) + self.assertNotEqual(u"Jarre, Jean Michel", util.translate_from_sortname(u"Jean Michel Jarre", u"Jarre, Jean Michel")) def test_kanji(self): self.assertEqual(u"Tetsuya Komuro", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya")) - self.failIfEqual(u"Komuro, Tetsuya", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya")) - self.failIfEqual(u"小室哲哉", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya")) + self.assertNotEqual(u"Komuro, Tetsuya", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya")) + self.assertNotEqual(u"小室哲哉", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya")) def test_kanji2(self): self.assertEqual(u"Ayumi Hamasaki & Keiko", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko")) - self.failIfEqual(u"浜崎あゆみ & KEIKO", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko")) - self.failIfEqual(u"Hamasaki, Ayumi & Keiko", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko")) + self.assertNotEqual(u"浜崎あゆみ & KEIKO", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko")) + self.assertNotEqual(u"Hamasaki, Ayumi & Keiko", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko")) def test_cyrillic(self): self.assertEqual(U"Pyotr Ilyich Tchaikovsky", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) - self.failIfEqual(u"Tchaikovsky, Pyotr Ilyich", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) - self.failIfEqual(u"Пётр Ильич Чайковский", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) + self.assertNotEqual(u"Tchaikovsky, Pyotr Ilyich", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) + self.assertNotEqual(u"Пётр Ильич Чайковский", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) class FormatTimeTest(unittest.TestCase): From 60ed2434700fd6d47175a21b5e9964f3bb21e178 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Mon, 1 Jul 2013 13:33:42 +0200 Subject: [PATCH 42/75] Deprecated failUnlessRaises() -> assertRaises() http://docs.python.org/2/library/unittest.html#deprecated-aliases --- test/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_utils.py b/test/test_utils.py index 0e839c9cb..029226562 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -75,7 +75,7 @@ class ShortFilenameTest(unittest.TestCase): self.assertEqual(fn, os.path.join("a12345678", "b1")) def test_too_long(self): - self.failUnlessRaises(IOError, util.make_short_filename, "/home/me/", os.path.join("a1234567890", "b1234567890"), 10) + self.assertRaises(IOError, util.make_short_filename, "/home/me/", os.path.join("a1234567890", "b1234567890"), 10) def test_whitespace(self): fn = util.make_short_filename("/home/me/", os.path.join("a1234567890 ", " b1234567890 "), 22) From 329ed8cedb64be6f43cfd9a0ae964e292b1ec695 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Mon, 1 Jul 2013 13:34:53 +0200 Subject: [PATCH 43/75] Deprecated failUnlessAlmostEqual() -> assertAlmostEqual() http://docs.python.org/2/library/unittest.html#deprecated-aliases --- test/test_similarity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_similarity.py b/test/test_similarity.py index be56e79ca..8e2eb1437 100644 --- a/test/test_similarity.py +++ b/test/test_similarity.py @@ -6,5 +6,5 @@ class SimilarityTest(unittest.TestCase): def test_correct(self): self.assertEqual(similarity(u"K!", u"K!"), 1.0) self.assertEqual(similarity(u"BBB", u"AAA"), 0.0) - self.failUnlessAlmostEqual(similarity(u"ABC", u"ABB"), 0.7, 1) + self.assertAlmostEqual(similarity(u"ABC", u"ABB"), 0.7, 1) From 97f00b2d29e825c3b65c8f2c80d644341684f2aa Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Tue, 2 Jul 2013 01:45:25 +0200 Subject: [PATCH 44/75] Handle configuration file versions. version_info was renamed PICARD_VERSION version_string was renamed PICARD_VERSION_STR short version string is stored in PICARD_VERSION_STR_SHORT (for display) Hooks can be created to handle upgrades (ie. options renaming), see config.register_upgrade_hook() and config.run_upgrade_hooks() --- picard/__init__.py | 30 +++++++++++++------ picard/config.py | 61 ++++++++++++++++++++++++++++++++++++++ picard/tagger.py | 20 +++++++++++-- picard/ui/options/about.py | 4 +-- picard/webservice.py | 8 ++--- test/test_versions.py | 53 +++++++++++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 test/test_versions.py diff --git a/picard/__init__.py b/picard/__init__.py index 075049e0e..dbfe2ed59 100644 --- a/picard/__init__.py +++ b/picard/__init__.py @@ -17,16 +17,28 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -version_info = (1, 2, 0, 'final', 0) +import re -if version_info[3] == 'final': - if version_info[2] == 0: - version_string = '%d.%d' % version_info[:2] +PICARD_VERSION = (1, 2, 0, 'final', 0) + + +def version_to_string(version_tuple, short=False): + assert len(version_tuple) == 5 + assert version_tuple[3] in ('final', 'dev') + if short and version_tuple[3] == 'final': + if version_tuple[2] == 0: + version_str = '%d.%d' % version_tuple[:2] + else: + version_str = '%d.%d.%d' % version_tuple[:3] else: - version_string = '%d.%d.%d' % version_info[:3] -else: - version_string = '%d.%d.%d%s%d' % version_info -__version__ = version_string + version_str = '%d.%d.%d%s%d' % version_tuple + return version_str + +def version_from_string(version_str): + g = re.match(r"^(\d+).(\d+).(\d+)(dev|final)(\d+)$", version_str).groups() + return (int(g[0]), int(g[1]), int(g[2]), g[3], int(g[4])) + +__version__ = PICARD_VERSION_STR = version_to_string(PICARD_VERSION) +PICARD_VERSION_STR_SHORT = version_to_string(PICARD_VERSION, short=True) api_versions = ["0.15.0", "0.15.1", "0.16.0", "1.0.0", "1.1.0", "1.2.0"] - diff --git a/picard/config.py b/picard/config.py index d7ea9b127..493d3b63c 100644 --- a/picard/config.py +++ b/picard/config.py @@ -18,9 +18,14 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from PyQt4 import QtCore +from picard import PICARD_VERSION, version_to_string, version_from_string, log from picard.util import LockableObject, rot13 +class ConfigUpgradeError(Exception): + pass + + class ConfigSection(LockableObject): """Configuration section.""" @@ -67,11 +72,16 @@ class Config(QtCore.QSettings): def __init__(self): """Initializes the configuration.""" QtCore.QSettings.__init__(self, "MusicBrainz", "Picard") + self.application = ConfigSection(self, "application") self.setting = ConfigSection(self, "setting") self.persist = ConfigSection(self, "persist") self.profile = ConfigSection(self, "profile/default") self.current_preset = "default" + TextOption("application", "version", '0.0.0dev0') + self._version = version_from_string(self.application["version"]) + self._upgrade_hooks = [] + def switchProfile(self, profilename): """Sets the current profile.""" key = u"profile/%s" % (profilename,) @@ -80,6 +90,57 @@ class Config(QtCore.QSettings): else: raise KeyError, "Unknown profile '%s'" % (profilename,) + def register_upgrade_hook(self, to_version_str, func, *args): + """Register a function to upgrade from one config version to another""" + to_version = version_from_string(to_version_str) + assert to_version <= PICARD_VERSION, "%r > %r !!!" % (to_version, PICARD_VERSION) + hook = { + 'to': to_version, + 'func': func, + 'args': args, + 'done': False + } + self._upgrade_hooks.append(hook) + + def run_upgrade_hooks(self): + """Executes registered functions to upgrade config version to the latest""" + if not self._upgrade_hooks: + return + if self._version >= PICARD_VERSION: + if self._version > PICARD_VERSION: + m = "Warning: config file %r was created by a more recent version of Picard (current is %r)" + print(m % (version_to_string(self._version), + version_to_string(PICARD_VERSION))) + return + #remove executed hooks if any, and sort + self._upgrade_hooks = [item for item in self._upgrade_hooks if not item['done']] + self._upgrade_hooks.sort(key=lambda k: (k['to'])) + for hook in self._upgrade_hooks: + if self._version < hook['to']: + try: + hook['func'](*hook['args']) + except Exception as e: + raise ConfigUpgradeError, "Error during config upgrade from version %d to %d using %s(): %s" \ + % (self._version, hook['to'], hook['func'].__name__, e) + else: + hook['done'] = True + self._version = hook['to'] + self._write_version() + else: + #hook is not applicable, mark as done + hook['done'] = True + + # remove executed hooks + self._upgrade_hooks = [item for item in self._upgrade_hooks if not item['done']] + if not self._upgrade_hooks: + # all hooks were executed, ensure config is marked with latest version + self._version = PICARD_VERSION + self._write_version() + + def _write_version(self): + self.application["version"] = version_to_string(self._version) + self.sync() + class Option(QtCore.QObject): """Generic option.""" diff --git a/picard/tagger.py b/picard/tagger.py index 90f27d022..181d0a395 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -40,7 +40,7 @@ import picard.resources import picard.plugins from picard.i18n import setup_gettext -from picard import version_string, log, acoustid, config +from picard import PICARD_VERSION_STR, log, acoustid, config from picard.album import Album, NatAlbum from picard.browser.browser import BrowserIntegration from picard.browser.filelookup import FileLookup @@ -127,6 +127,8 @@ class Tagger(QtGui.QApplication): check_io_encoding() + self._upgrade_config() + setup_gettext(localedir, config.setting["ui_language"], log.debug) self.xmlws = XmlWebService() @@ -185,6 +187,20 @@ class Tagger(QtGui.QApplication): # default format, disabled remove_va_file_naming_format(merge=False) + def _upgrade_config(self): + cfg = config._config + + def upgrade_conf_test(*args): + """dummy function to test config upgrades, print its arguments""" + print(args[0]) + + #upgrade from config format without version to first version + cfg.register_upgrade_hook('1.0.0final0', + upgrade_conf_test, + "Add version to config file") + + cfg.run_upgrade_hooks() + def move_files_to_album(self, files, albumid=None, album=None): """Move `files` to tracks on album `albumid`.""" if album is None: @@ -564,7 +580,7 @@ Options: def version(): - print """MusicBrainz Picard %s""" % (version_string) + print """MusicBrainz Picard %s""" % (PICARD_VERSION_STR) def main(localedir=None, autoupdate=True): diff --git a/picard/ui/options/about.py b/picard/ui/options/about.py index 88c9854c8..9e8fea09a 100644 --- a/picard/ui/options/about.py +++ b/picard/ui/options/about.py @@ -19,7 +19,7 @@ from mutagen import version_string as mutagen_version from PyQt4.QtCore import PYQT_VERSION_STR as pyqt_version -from picard import __version__ as picard_version +from picard import PICARD_VERSION_STR_SHORT from picard.formats import supported_formats from picard.ui.options import OptionsPage, register_options_page from picard.ui.ui_options_about import Ui_AboutOptionsPage @@ -41,7 +41,7 @@ class AboutOptionsPage(OptionsPage): def load(self): args = { - "version": picard_version, + "version": PICARD_VERSION_STR_SHORT, "mutagen-version": mutagen_version, "pyqt-version": pyqt_version, "libdiscid-version": libdiscid_version() diff --git a/picard/webservice.py b/picard/webservice.py index f869ab20a..0e34cc699 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -32,14 +32,14 @@ from functools import partial from PyQt4 import QtCore, QtNetwork from PyQt4.QtGui import QDesktopServices from PyQt4.QtCore import QUrl, QXmlStreamReader -from picard import version_string, config, log +from picard import PICARD_VERSION_STR, config, log from picard.const import ACOUSTID_KEY, ACOUSTID_HOST REQUEST_DELAY = defaultdict(lambda: 1000) REQUEST_DELAY[(ACOUSTID_HOST, 80)] = 333 REQUEST_DELAY[("coverartarchive.org", 80)] = 0 -USER_AGENT_STRING = 'MusicBrainz%%20Picard-%s' % version_string +USER_AGENT_STRING = 'MusicBrainz%%20Picard-%s' % PICARD_VERSION_STR def _escape_lucene_query(text): @@ -172,7 +172,7 @@ class XmlWebService(QtCore.QObject): if cacheloadcontrol is not None: request.setAttribute(QtNetwork.QNetworkRequest.CacheLoadControlAttribute, cacheloadcontrol) - request.setRawHeader("User-Agent", "MusicBrainz-Picard/%s" % version_string) + request.setRawHeader("User-Agent", "MusicBrainz-Picard/%s" % PICARD_VERSION_STR) if data is not None: if method == "POST" and host == config.setting["server_host"]: request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/xml; charset=utf-8") @@ -399,7 +399,7 @@ class XmlWebService(QtCore.QObject): def _encode_acoustid_args(self, args): filters = [] args['client'] = ACOUSTID_KEY - args['clientversion'] = version_string + args['clientversion'] = PICARD_VERSION_STR args['format'] = 'xml' for name, value in args.items(): value = str(QUrl.toPercentEncoding(value)) diff --git a/test/test_versions.py b/test/test_versions.py new file mode 100644 index 000000000..69d5a813a --- /dev/null +++ b/test/test_versions.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import unittest +from picard import version_to_string, version_from_string + + +class VersionsTest(unittest.TestCase): + + def test_version_conv_1(self): + l, s = (0, 0, 1, 'dev', 1), '0.0.1dev1' + self.assertEqual(version_to_string(l), s) + self.assertEqual(l, version_from_string(s)) + + def test_version_conv_2(self): + l, s = (1, 1, 0, 'final', 0), '1.1.0final0' + self.assertEqual(version_to_string(l), s) + self.assertEqual(l, version_from_string(s)) + + def test_version_conv_3(self): + l, s = (1, 1, 0, 'dev', 0), '1.1.0dev0' + self.assertEqual(version_to_string(l), s) + self.assertEqual(l, version_from_string(s)) + + def test_version_conv_4(self): + l, s = (1, 0, 2, '', 0), '1.0.2' + self.assertRaises(AssertionError, version_to_string, (l)) + self.assertRaises(AttributeError, version_from_string, (s)) + + def test_version_conv_5(self): + l, s = (999, 999, 999, 'dev', 999), '999.999.999dev999' + self.assertEqual(version_to_string(l), s) + self.assertEqual(l, version_from_string(s)) + + def test_version_conv_6(self): + self.assertRaises(TypeError, version_to_string, ('1', 0, 2, 'final', 0)) + self.assertRaises(AssertionError, version_to_string, (1, 0)) + self.assertRaises(TypeError, version_from_string, 1) + + def test_version_conv_7(self): + l, s = (1, 1, 0, 'final', 0), '1.1' + self.assertEqual(version_to_string(l, short=True), s) + + def test_version_conv_8(self): + l, s = (1, 1, 1, 'final', 0), '1.1.1' + self.assertEqual(version_to_string(l, short=True), s) + + def test_version_conv_9(self): + l, s = (1, 1, 0, 'final', 1), '1.1' + self.assertEqual(version_to_string(l, short=True), s) + + def test_version_conv_10(self): + l, s = (1, 1, 0, 'dev', 0), '1.1.0dev0' + self.assertEqual(version_to_string(l, short=True), s) From 1265d5fd1c8193ac0e80e45b20c41dbbbb882b17 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Tue, 2 Jul 2013 10:19:34 +0200 Subject: [PATCH 45/75] PEP8 compliance fixes --- picard/__init__.py | 1 + picard/config.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/picard/__init__.py b/picard/__init__.py index dbfe2ed59..e7761e469 100644 --- a/picard/__init__.py +++ b/picard/__init__.py @@ -34,6 +34,7 @@ def version_to_string(version_tuple, short=False): version_str = '%d.%d.%d%s%d' % version_tuple return version_str + def version_from_string(version_str): g = re.match(r"^(\d+).(\d+).(\d+)(dev|final)(\d+)$", version_str).groups() return (int(g[0]), int(g[1]), int(g[2]), g[3], int(g[4])) diff --git a/picard/config.py b/picard/config.py index 493d3b63c..9eea8ce28 100644 --- a/picard/config.py +++ b/picard/config.py @@ -52,7 +52,7 @@ class ConfigSection(LockableObject): self.lock_for_write() try: self.__config.setValue("%s/%s" % (self.__name, name), - QtCore.QVariant(value)) + QtCore.QVariant(value)) finally: self.unlock() @@ -88,7 +88,7 @@ class Config(QtCore.QSettings): if self.contains(key): self.profile.name = key else: - raise KeyError, "Unknown profile '%s'" % (profilename,) + raise KeyError("Unknown profile '%s'" % (profilename,)) def register_upgrade_hook(self, to_version_str, func, *args): """Register a function to upgrade from one config version to another""" @@ -120,8 +120,8 @@ class Config(QtCore.QSettings): try: hook['func'](*hook['args']) except Exception as e: - raise ConfigUpgradeError, "Error during config upgrade from version %d to %d using %s(): %s" \ - % (self._version, hook['to'], hook['func'].__name__, e) + raise ConfigUpgradeError("Error during config upgrade from version %d to %d using %s(): %s" % + (self._version, hook['to'], hook['func'].__name__, e)) else: hook['done'] = True self._version = hook['to'] @@ -161,7 +161,7 @@ class Option(QtCore.QObject): try: return cls.registry[(section, name)] except KeyError: - raise KeyError, "Option %s.%s not found." % (section, name) + raise KeyError("Option %s.%s not found." % (section, name)) class TextOption(Option): From 8d40e6700d05c6573e7305a3ffe5c9f61d2de702 Mon Sep 17 00:00:00 2001 From: Laurent Monin Date: Tue, 2 Jul 2013 23:01:13 +0200 Subject: [PATCH 46/75] pep8 fixes --- picard/acoustid.py | 3 +- picard/acoustidmanager.py | 1 - picard/album.py | 4 +- picard/browser/filelookup.py | 1 + picard/cluster.py | 5 +- picard/const.py | 4 +- picard/coverart.py | 18 +++---- picard/coverartarchive.py | 4 +- picard/dataobj.py | 1 + picard/disc.py | 4 +- picard/formats/__init__.py | 7 ++- picard/formats/apev2.py | 14 ++++- picard/formats/asf.py | 3 ++ picard/formats/id3.py | 45 ++++++++++------ picard/formats/mp4.py | 1 + picard/formats/mutagenext/compatid3.py | 69 +++++++++++++++++-------- picard/formats/mutagenext/tak.py | 6 ++- picard/formats/vorbis.py | 20 +++++++- picard/log.py | 1 + picard/mbxml.py | 4 ++ picard/metadata.py | 3 ++ picard/parsefilename.py | 2 +- picard/script.py | 71 +++++++++++++++++++++++--- picard/similarity.py | 3 ++ picard/tagger.py | 15 ++++-- picard/ui/cdlookup.py | 1 + picard/ui/filebrowser.py | 3 +- picard/ui/infodialog.py | 19 ++++--- picard/ui/infostatus.py | 2 +- picard/ui/item.py | 1 + picard/ui/itemviews.py | 24 +++++---- picard/ui/mainwindow.py | 13 +++-- picard/ui/metadatabox.py | 2 +- picard/ui/options/__init__.py | 4 +- picard/ui/options/plugins.py | 4 +- picard/ui/options/renaming.py | 15 +++--- picard/ui/options/scripting.py | 4 +- picard/ui/ratingwidget.py | 2 +- picard/ui/util.py | 1 - picard/util/__init__.py | 22 ++++++-- picard/util/icontheme.py | 1 + picard/util/mimetype.py | 5 +- picard/util/queue.py | 3 +- picard/util/tags.py | 2 +- picard/webservice.py | 12 +++-- setup.py | 3 +- tagger.py | 1 - 47 files changed, 325 insertions(+), 128 deletions(-) diff --git a/picard/acoustid.py b/picard/acoustid.py index 79aa1ad5a..dd10e9e12 100644 --- a/picard/acoustid.py +++ b/picard/acoustid.py @@ -60,7 +60,7 @@ class AcoustIDClient(QtCore.QObject): return artist_credit_el def parse_recording(recording): - if 'title' not in recording.children: # we have no metadata for this recording + if 'title' not in recording.children: # we have no metadata for this recording return recording_id = recording.id[0].text recording_el = recording_list_el.append_child('recording') @@ -213,4 +213,3 @@ class AcoustIDClient(QtCore.QObject): if task[0] != file: new_queue.appendleft(task) self._queue = new_queue - diff --git a/picard/acoustidmanager.py b/picard/acoustidmanager.py index 3237daa23..9bbf7bdcd 100644 --- a/picard/acoustidmanager.py +++ b/picard/acoustidmanager.py @@ -86,4 +86,3 @@ class AcoustIDManager(QtCore.QObject): for submission in fingerprints: submission.orig_trackid = submission.trackid self._check_unsubmitted() - diff --git a/picard/album.py b/picard/album.py index 3a338e54e..6b749962e 100644 --- a/picard/album.py +++ b/picard/album.py @@ -420,7 +420,7 @@ class Album(DataObject, Item): for track in self.tracks: for file in track.linked_files: if not file.is_saved(): - count+=1 + count += 1 return count def column(self, column): @@ -429,7 +429,7 @@ class Album(DataObject, Item): linked_tracks = 0 for track in self.tracks: if track.is_linked(): - linked_tracks+=1 + linked_tracks += 1 text = u'%s\u200E (%d/%d' % (self.metadata['album'], linked_tracks, len(self.tracks)) unmatched = self.get_num_unmatched_files() if unmatched: diff --git a/picard/browser/filelookup.py b/picard/browser/filelookup.py index d99c60a65..0bc6b9bbc 100644 --- a/picard/browser/filelookup.py +++ b/picard/browser/filelookup.py @@ -24,6 +24,7 @@ import re from picard import log from picard.util import webbrowser2 + class FileLookup(object): def __init__(self, parent, server, port, localPort): diff --git a/picard/cluster.py b/picard/cluster.py index cc1901240..fdac5d64c 100644 --- a/picard/cluster.py +++ b/picard/cluster.py @@ -339,7 +339,7 @@ class ClusterDict(object): return word def getToken(self, index): - token = None; + token = None try: word, token = self.ids[index] except KeyError: @@ -416,7 +416,7 @@ class ClusterEngine(object): for i in xrange(self.clusterDict.getSize()): word, count = self.clusterDict.getWordAndCount(i) if word and count > 1: - self.clusterBins[self.clusterCount] = [ i ] + self.clusterBins[self.clusterCount] = [i] self.idClusterIndex[i] = self.clusterCount self.clusterCount = self.clusterCount + 1 #print "init ", @@ -475,4 +475,3 @@ class ClusterEngine(object): def can_refresh(self): return False - diff --git a/picard/const.py b/picard/const.py index 399a32188..8803ce276 100644 --- a/picard/const.py +++ b/picard/const.py @@ -17,7 +17,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -import os, sys, re +import os +import sys +import re # Install gettext "noop" function in case const.py gets imported directly. import __builtin__ diff --git a/picard/coverart.py b/picard/coverart.py index 7df342c81..cae09474a 100644 --- a/picard/coverart.py +++ b/picard/coverart.py @@ -53,31 +53,31 @@ COVERART_SITES = ( AMAZON_SERVER = { "amazon.jp": { "server": "ec1.images-amazon.com", - "id" : "09", + "id": "09", }, "amazon.co.jp": { "server": "ec1.images-amazon.com", - "id" : "09", + "id": "09", }, "amazon.co.uk": { "server": "ec1.images-amazon.com", - "id" : "02", + "id": "02", }, "amazon.de": { "server": "ec2.images-amazon.com", - "id" : "03", + "id": "03", }, "amazon.com": { "server": "ec1.images-amazon.com", - "id" : "01", + "id": "01", }, "amazon.ca": { "server": "ec1.images-amazon.com", - "id" : "01", # .com and .ca are identical + "id": "01", # .com and .ca are identical }, "amazon.fr": { "server": "ec1.images-amazon.com", - "id" : "08" + "id": "08" }, } @@ -156,9 +156,9 @@ def _caa_append_image_to_trylist(try_list, imagedata): else: url = QUrl(imagedata["thumbnails"][thumbsize]) extras = { - 'type': imagedata["types"][0].lower(), # FIXME: we pass only 1 type + 'type': imagedata["types"][0].lower(), # FIXME: we pass only 1 type 'desc': imagedata["comment"], - 'front': imagedata['front'], # front image indicator from CAA + 'front': imagedata['front'], # front image indicator from CAA } _try_list_append_image_url(try_list, url, extras) diff --git a/picard/coverartarchive.py b/picard/coverartarchive.py index 9e574177d..8ad268614 100644 --- a/picard/coverartarchive.py +++ b/picard/coverartarchive.py @@ -30,7 +30,7 @@ CAA_TYPES = [ {'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 + {'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 +CAA_TYPES_SEPARATOR = ' ' # separator to use when joining/splitting list of types diff --git a/picard/dataobj.py b/picard/dataobj.py index 85bc36f34..989e6650b 100644 --- a/picard/dataobj.py +++ b/picard/dataobj.py @@ -19,6 +19,7 @@ from picard.util import LockableObject + class DataObject(LockableObject): def __init__(self, id): diff --git a/picard/disc.py b/picard/disc.py index 4008dc5a3..c0fab2815 100644 --- a/picard/disc.py +++ b/picard/disc.py @@ -135,12 +135,12 @@ def _openLibrary(): except OSError, e: raise NotImplementedError('Error opening library: ' + str(e)) - assert False # not reached + assert False # not reached def _setPrototypes(libDiscId): ct = ctypes - libDiscId.discid_new.argtypes = ( ) + libDiscId.discid_new.argtypes = () libDiscId.discid_new.restype = ct.c_void_p libDiscId.discid_free.argtypes = (ct.c_void_p, ) diff --git a/picard/formats/__init__.py b/picard/formats/__init__.py index 6ac9915f4..07dae5af5 100644 --- a/picard/formats/__init__.py +++ b/picard/formats/__init__.py @@ -18,16 +18,19 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import sys +from mutagen import _util from picard.plugin import ExtensionPoint _formats = ExtensionPoint() _extensions = {} + def register_format(format): _formats.register(format.__module__, format) for ext in format.EXTENSIONS: _extensions[ext[1:]] = format + def supported_formats(): """Returns list of supported formats.""" formats = [] @@ -35,6 +38,7 @@ def supported_formats(): formats.append((format.EXTENSIONS, format.NAME)) return formats + def open(filename): """Open the specified file and return a File instance with the appropriate format handler, or None.""" i = filename.rfind(".") @@ -48,8 +52,6 @@ def open(filename): return format(filename) -from mutagen import _util - def _insert_bytes_no_mmap(fobj, size, offset, BUFFER_SIZE=2**16): """Insert size bytes of empty space starting at offset. @@ -101,6 +103,7 @@ def _insert_bytes_no_mmap(fobj, size, offset, BUFFER_SIZE=2**16): if locked: _util.unlock(fobj) + def _delete_bytes_no_mmap(fobj, size, offset, BUFFER_SIZE=2**16): """Delete size bytes of empty space starting at offset. diff --git a/picard/formats/apev2.py b/picard/formats/apev2.py index e89e152dd..d2a476f7f 100644 --- a/picard/formats/apev2.py +++ b/picard/formats/apev2.py @@ -149,24 +149,28 @@ class APEv2File(File): cover_filename = 'Cover Art (Front)' cover_filename += mimetype.get_extension(image["mime"], '.jpg') tags['Cover Art (Front)'] = mutagen.apev2.APEValue(cover_filename + '\0' + image["data"], mutagen.apev2.BINARY) - break # can't save more than one item with the same name - # (mp3tags does this, but it's against the specs) + break # can't save more than one item with the same name + # (mp3tags does this, but it's against the specs) tags.save(encode_filename(filename)) + class MusepackFile(APEv2File): """Musepack file.""" EXTENSIONS = [".mpc", ".mp+"] NAME = "Musepack" _File = mutagen.musepack.Musepack + def _info(self, metadata, file): super(MusepackFile, self)._info(metadata, file) metadata['~format'] = "Musepack, SV%d" % file.info.version + class WavPackFile(APEv2File): """WavPack file.""" EXTENSIONS = [".wv"] NAME = "WavPack" _File = mutagen.wavpack.WavPack + def _info(self, metadata, file): super(WavPackFile, self)._info(metadata, file) metadata['~format'] = self.NAME @@ -179,11 +183,13 @@ class WavPackFile(APEv2File): self._rename(wvc_filename, metadata, config.setting) return File._save_and_rename(self, old_filename, metadata, config.setting) + class OptimFROGFile(APEv2File): """OptimFROG file.""" EXTENSIONS = [".ofr", ".ofs"] NAME = "OptimFROG" _File = mutagen.optimfrog.OptimFROG + def _info(self, metadata, file): super(OptimFROGFile, self)._info(metadata, file) if file.filename.lower().endswith(".ofs"): @@ -191,20 +197,24 @@ class OptimFROGFile(APEv2File): else: metadata['~format'] = "OptimFROG Lossless Audio" + class MonkeysAudioFile(APEv2File): """Monkey's Audio file.""" EXTENSIONS = [".ape"] NAME = "Monkey's Audio" _File = mutagen.monkeysaudio.MonkeysAudio + def _info(self, metadata, file): super(MonkeysAudioFile, self)._info(metadata, file) metadata['~format'] = self.NAME + class TAKFile(APEv2File): """TAK file.""" EXTENSIONS = [".tak"] NAME = "Tom's lossless Audio Kompressor" _File = mutagenext.tak.TAK + def _info(self, metadata, file): super(TAKFile, self)._info(metadata, file) metadata['~format'] = self.NAME diff --git a/picard/formats/asf.py b/picard/formats/asf.py index 494835f69..3fd723a3e 100644 --- a/picard/formats/asf.py +++ b/picard/formats/asf.py @@ -25,6 +25,7 @@ from picard.metadata import Metadata, save_this_image_to_tags from mutagen.asf import ASF, ASFByteArrayAttribute import struct + def unpack_image(data): """ Helper function to unpack image data from a WM/Picture tag. @@ -51,6 +52,7 @@ def unpack_image(data): image_data = data[pos:pos+size] return (mime.decode("utf-16-le"), image_data, type, description.decode("utf-16-le")) + def pack_image(mime, data, type=3, description=""): """ Helper function to pack image data for a WM/Picture tag. @@ -62,6 +64,7 @@ def pack_image(mime, data, type=3, description=""): tag_data += data return tag_data + class ASFFile(File): """ASF (WMA) metadata reader/writer""" EXTENSIONS = [".wma"] diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 3be35273c..51d0f09e6 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -36,8 +36,10 @@ from urlparse import urlparse def patched_EncodedTextSpec_write(self, frame, value): try: enc, term = self._encodings[frame.encoding] - except AttributeError: enc, term = self.encodings[frame.encoding] + except AttributeError: + enc, term = self.encodings[frame.encoding] return value.encode(enc, 'ignore') + term + id3.EncodedTextSpec.write = patched_EncodedTextSpec_write @@ -55,6 +57,8 @@ def patched_MultiSpec_write(self, frame, value): if data.endswith(term): data = data[:-len(term)] return data + + id3.MultiSpec._write_orig = id3.MultiSpec.write id3.MultiSpec.write = patched_MultiSpec_write @@ -63,23 +67,25 @@ id3.TCMP = compatid3.TCMP id3.TSO2 = compatid3.TSO2 __ID3_IMAGE_TYPE_MAP = { - "other": 0, - "obi": 0, - "tray": 0, - "spine": 0, - "sticker": 0, - "front": 3, - "back": 4, - "booklet": 5, - "medium": 6, - "track": 6, - } + "other": 0, + "obi": 0, + "tray": 0, + "spine": 0, + "sticker": 0, + "front": 3, + "back": 4, + "booklet": 5, + "medium": 6, + "track": 6, +} + +__ID3_REVERSE_IMAGE_TYPE_MAP = dict([(v, k) for k, v in __ID3_IMAGE_TYPE_MAP.iteritems()]) -__ID3_REVERSE_IMAGE_TYPE_MAP = dict([(v,k) for k, v in __ID3_IMAGE_TYPE_MAP.iteritems()]) def image_type_from_id3_num(id3type): return __ID3_REVERSE_IMAGE_TYPE_MAP.get(id3type, "other") + def image_type_as_id3_num(texttype): return __ID3_IMAGE_TYPE_MAP.get(texttype, 0) @@ -324,9 +330,9 @@ class ID3File(File): tmcl.people.append([role, value]) elif name.startswith('comment:'): desc = name.split(':', 1)[1] - if desc.lower()[:4]=="itun": + if desc.lower()[:4] == "itun": tags.delall('COMM:' + desc) - tags.add(id3.COMM(encoding=0, desc=desc, lang='eng', text=[v+u'\x00' for v in values])) + tags.add(id3.COMM(encoding=0, desc=desc, lang='eng', text=[v + u'\x00' for v in values])) else: tags.add(id3.COMM(encoding=encoding, desc=desc, lang='eng', text=values)) elif name.startswith('lyrics:') or name == 'lyrics': @@ -401,8 +407,10 @@ class ID3File(File): tags.save(encode_filename(filename), v2=4, v1=v1) if self._IsMP3 and config.setting["remove_ape_from_mp3"]: - try: mutagen.apev2.delete(encode_filename(filename)) - except: pass + try: + mutagen.apev2.delete(encode_filename(filename)) + except: + pass def supports_tag(self, name): return name in self.__rtranslate or name in self.__rtranslate_freetext\ @@ -417,15 +425,18 @@ class MP3File(ID3File): NAME = "MPEG-1 Audio" _File = mutagen.mp3.MP3 _IsMP3 = True + def _info(self, metadata, file): super(MP3File, self)._info(metadata, file) metadata['~format'] = 'MPEG-1 Layer %d' % file.info.layer + class TrueAudioFile(ID3File): """TTA file.""" EXTENSIONS = [".tta"] NAME = "The True Audio" _File = mutagen.trueaudio.TrueAudio + def _info(self, metadata, file): super(TrueAudioFile, self)._info(metadata, file) metadata['~format'] = self.NAME diff --git a/picard/formats/mp4.py b/picard/formats/mp4.py index b816c4186..736f5040c 100644 --- a/picard/formats/mp4.py +++ b/picard/formats/mp4.py @@ -23,6 +23,7 @@ from picard.file import File from picard.metadata import Metadata, save_this_image_to_tags from picard.util import encode_filename + class MP4File(File): EXTENSIONS = [".m4a", ".m4b", ".m4p", ".mp4"] NAME = "MPEG-4 Audio" diff --git a/picard/formats/mutagenext/compatid3.py b/picard/formats/mutagenext/compatid3.py index 78782b0ba..d1fdc9bcf 100644 --- a/picard/formats/mutagenext/compatid3.py +++ b/picard/formats/mutagenext/compatid3.py @@ -25,18 +25,23 @@ from mutagen._util import insert_bytes from mutagen.id3 import ID3, Frame, Frames, Frames_2_2, TextFrame, TORY, \ TYER, TIME, APIC, IPLS, TDAT, BitPaddedInt, MakeID3v1 + class TCMP(TextFrame): pass + class TSO2(TextFrame): pass + class XDOR(TextFrame): pass + class XSOP(TextFrame): pass + class CompatID3(ID3): """ Additional features over mutagen.id3.ID3: @@ -56,7 +61,7 @@ class CompatID3(ID3): known_frames["XDOR"] = XDOR known_frames["XSOP"] = XSOP kwargs["known_frames"] = known_frames - super(CompatID3, self).__init__(*args, **kwargs) + super(CompatID3, self).__init__(*args, **kwargs) def save(self, filename=None, v1=1, v2=4): """Save changes to a file. @@ -90,28 +95,37 @@ class CompatID3(ID3): self.delete(filename) except EnvironmentError, err: from errno import ENOENT - if err.errno != ENOENT: raise + if err.errno != ENOENT: + raise return framedata = ''.join(framedata) framesize = len(framedata) - if filename is None: filename = self.filename - try: f = open(filename, 'rb+') + if filename is None: + filename = self.filename + try: + f = open(filename, 'rb+') except IOError, err: from errno import ENOENT - if err.errno != ENOENT: raise - f = open(filename, 'ab') # create, then reopen + if err.errno != ENOENT: + raise + f = open(filename, 'ab') # create, then reopen f = open(filename, 'rb+') try: idata = f.read(10) - try: id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata) - except struct.error: id3, insize = '', 0 + try: + id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata) + except struct.error: + id3, insize = '', 0 insize = BitPaddedInt(insize) - if id3 != 'ID3': insize = -10 + if id3 != 'ID3': + insize = -10 - if insize >= framesize: outsize = insize - else: outsize = (framesize + 1023) & ~0x3FF + if insize >= framesize: + outsize = insize + else: + outsize = (framesize + 1023) & ~0x3FF framedata += '\x00' * (outsize - framesize) framesize = BitPaddedInt.to_str(outsize, width=4) @@ -128,13 +142,16 @@ class CompatID3(ID3): f.seek(-128, 2) except IOError, err: from errno import EINVAL - if err.errno != EINVAL: raise - f.seek(0, 2) # ensure read won't get "TAG" + if err.errno != EINVAL: + raise + f.seek(0, 2) # ensure read won't get "TAG" if f.read(3) == "TAG": f.seek(-128, 2) - if v1 > 0: f.write(MakeID3v1(self)) - else: f.truncate() + if v1 > 0: + f.write(MakeID3v1(self)) + else: + f.truncate() elif v1 == 2: f.seek(0, 2) f.write(MakeID3v1(self)) @@ -145,10 +162,13 @@ class CompatID3(ID3): def __save_frame(self, frame, v2): flags = 0 if self.PEDANTIC and isinstance(frame, TextFrame): - if len(str(frame)) == 0: return '' + if len(str(frame)) == 0: + return '' framedata = frame._writeData() - if v2 == 3: bits=8 - else: bits=7 + if v2 == 3: + bits = 8 + else: + bits = 7 datasize = BitPaddedInt.to_str(len(framedata), width=4, bits=bits) header = pack('>4s4sH', type(frame).__name__, datasize, flags) return header + framedata @@ -161,7 +181,8 @@ class CompatID3(ID3): at some point. """ - if self.version < (2,3,0): del self.unknown_frames[:] + if self.version < (2, 3, 0): + del self.unknown_frames[:] # TMCL, TIPL -> TIPL if "TIPL" in self or "TMCL" in self: @@ -177,7 +198,7 @@ class CompatID3(ID3): # TODO: # * EQU2 -> EQUA - # * RVA2 -> RVAD + # * RVA2 -> RVAD # TDOR -> TORY if "TDOR" in self: @@ -205,7 +226,10 @@ class CompatID3(ID3): if self.version < (2, 3): # ID3v2.2 PIC frames are slightly different. pics = self.getall("APIC") - mimes = { "PNG": "image/png", "JPG": "image/jpeg" } + mimes = { + "PNG": "image/png", + "JPG": "image/jpeg" + } self.delall("APIC") for pic in pics: newpic = APIC( @@ -222,7 +246,8 @@ class CompatID3(ID3): # New frames added in v2.4. for key in ["ASPI", "EQU2", "RVA2", "SEEK", "SIGN", "TDRL", "TDTG", "TMOO", "TPRO"]: - if key in self: del(self[key]) + if key in self: + del(self[key]) for frame in self.values(): # ID3v2.3 doesn't support UTF-8 (and WMP can't read UTF-16 BE) diff --git a/picard/formats/mutagenext/tak.py b/picard/formats/mutagenext/tak.py index b4568cb8f..b7dd94fb0 100644 --- a/picard/formats/mutagenext/tak.py +++ b/picard/formats/mutagenext/tak.py @@ -20,7 +20,10 @@ __all__ = ["TAK", "Open", "delete"] from mutagen.apev2 import APEv2File, error, delete -class TAKHeaderError(error): pass + +class TAKHeaderError(error): + pass + class TAKInfo(object): """TAK stream information. @@ -37,6 +40,7 @@ class TAKInfo(object): def pprint(self): return "Tom's lossless Audio Kompressor" + class TAK(APEv2File): _Info = TAKInfo _mimes = ["audio/x-tak"] diff --git a/picard/formats/vorbis.py b/picard/formats/vorbis.py index c1d1df99f..75c7f4e65 100644 --- a/picard/formats/vorbis.py +++ b/picard/formats/vorbis.py @@ -36,6 +36,7 @@ from picard.formats.id3 import image_type_from_id3_num, image_type_as_id3_num from picard.metadata import Metadata, save_this_image_to_tags from picard.util import encode_filename, sanitize_date + class VCommentFile(File): """Generic VComment-based file.""" _File = None @@ -67,8 +68,10 @@ class VCommentFile(File): name += value[start + 2:-1] value = value[:start] elif name.startswith('rating'): - try: name, email = name.split(':', 1) - except ValueError: email = '' + try: + name, email = name.split(':', 1) + except ValueError: + email = '' if email != config.setting['rating_user_email']: continue name = '~rating' @@ -179,60 +182,73 @@ class VCommentFile(File): except TypeError: file.save() + class FLACFile(VCommentFile): """FLAC file.""" EXTENSIONS = [".flac"] NAME = "FLAC" _File = mutagen.flac.FLAC + def _info(self, metadata, file): super(FLACFile, self)._info(metadata, file) metadata['~format'] = self.NAME + class OggFLACFile(VCommentFile): """FLAC file.""" EXTENSIONS = [".oggflac"] NAME = "Ogg FLAC" _File = mutagen.oggflac.OggFLAC + def _info(self, metadata, file): super(OggFLACFile, self)._info(metadata, file) metadata['~format'] = self.NAME + class OggSpeexFile(VCommentFile): """Ogg Speex file.""" EXTENSIONS = [".spx"] NAME = "Speex" _File = mutagen.oggspeex.OggSpeex + def _info(self, metadata, file): super(OggSpeexFile, self)._info(metadata, file) metadata['~format'] = self.NAME + class OggTheoraFile(VCommentFile): """Ogg Theora file.""" EXTENSIONS = [".oggtheora"] NAME = "Ogg Theora" _File = mutagen.oggtheora.OggTheora + def _info(self, metadata, file): super(OggTheoraFile, self)._info(metadata, file) metadata['~format'] = self.NAME + class OggVorbisFile(VCommentFile): """Ogg Vorbis file.""" EXTENSIONS = [".ogg"] NAME = "Ogg Vorbis" _File = mutagen.oggvorbis.OggVorbis + def _info(self, metadata, file): super(OggVorbisFile, self)._info(metadata, file) metadata['~format'] = self.NAME + class OggOpusFile(VCommentFile): """Ogg Opus file.""" EXTENSIONS = [".opus"] NAME = "Ogg Opus" _File = OggOpus + def _info(self, metadata, file): super(OggOpusFile, self)._info(metadata, file) metadata['~format'] = self.NAME + def OggAudioFile(filename): """Generic Ogg audio file.""" options = [OggFLACFile, OggSpeexFile, OggVorbisFile] diff --git a/picard/log.py b/picard/log.py index a547fcb7b..2dd353cb3 100644 --- a/picard/log.py +++ b/picard/log.py @@ -58,6 +58,7 @@ def add_receiver(receiver): _log_debug_messages = False + def debug(message, *args, **kwargs): if _log_debug_messages: thread.proxy_to_main(_message, "D:", message, args, kwargs) diff --git a/picard/mbxml.py b/picard/mbxml.py index bb08b91c2..46932c12d 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -54,6 +54,8 @@ def _decamelcase(text): _REPLACE_MAP = {} _EXTRA_ATTRS = ['guest', 'additional', 'minor'] _BLANK_SPECIAL_RELTYPES = {'vocal': 'vocals'} + + def _parse_attributes(attrs, reltype): attrs = [_decamelcase(_REPLACE_MAP.get(a, a)) for a in attrs] prefix = ' '.join([a for a in attrs if a in _EXTRA_ATTRS]) @@ -243,6 +245,7 @@ def recording_to_metadata(node, track): m['~rating'] = nodes[0].text m['~length'] = format_time(m.length) + def work_to_metadata(work, m): m.add("musicbrainz_workid", work.attribs['id']) if 'language' in work.children: @@ -250,6 +253,7 @@ def work_to_metadata(work, m): if 'relation_list' in work.children: _relations_to_metadata(work.relation_list, m) + def medium_to_metadata(node, m): for name, nodes in node.children.iteritems(): if not nodes: diff --git a/picard/metadata.py b/picard/metadata.py index a083972a6..93a3287f6 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -26,6 +26,7 @@ from picard.mbxml import artist_credit_from_node MULTI_VALUED_JOINER = '; ' + def is_front_image(image): # CAA has a flag for "front" image, use it in priority caa_front = image.get('front', None) @@ -34,6 +35,7 @@ def is_front_image(image): return (image['type'] == 'front') return caa_front + def save_this_image_to_tags(image): if not config.setting["save_only_front_images_to_tags"]: return True @@ -41,6 +43,7 @@ def save_this_image_to_tags(image): return True return False + class Metadata(dict): """List of metadata items with dict-like access.""" diff --git a/picard/parsefilename.py b/picard/parsefilename.py index 663b3de58..871857ce1 100644 --- a/picard/parsefilename.py +++ b/picard/parsefilename.py @@ -35,6 +35,7 @@ _patterns = [ re.compile(r"(?:.*(/|\\))?(?P.*)(/|\\)(?P.*)(/|\\)(?P.*)-(?P\d{2})-(?P.*)\.(?:\w{2,5})$"), ] + def parseFileName(filename, metadata): for pattern in _patterns: match = pattern.match(filename) @@ -79,4 +80,3 @@ if __name__ == "__main__": ok += 1 print "OK" print len(testCases), ok - diff --git a/picard/script.py b/picard/script.py index 3713abdec..31b2825ea 100644 --- a/picard/script.py +++ b/picard/script.py @@ -26,11 +26,26 @@ from picard.metadata import MULTI_VALUED_JOINER from picard.plugin import ExtensionPoint from inspect import getargspec -class ScriptError(Exception): pass -class ParseError(ScriptError): pass -class EndOfFile(ParseError): pass -class SyntaxError(ParseError): pass -class UnknownFunction(ScriptError): pass + +class ScriptError(Exception): + pass + + +class ParseError(ScriptError): + pass + + +class EndOfFile(ParseError): + pass + + +class SyntaxError(ParseError): + pass + + +class UnknownFunction(ScriptError): + pass + class ScriptText(unicode): @@ -262,7 +277,7 @@ def register_script_function(function, name=None, eval_args=True, function will not be verified.""" argspec = getargspec(function) - argcount = (len(argspec[0]) - 1,) # -1 for the parser + argcount = (len(argspec[0]) - 1,) # -1 for the parser if argspec[3] is not None: argcount = range(argcount[0] - len(argspec[3]), argcount[0] + 1) @@ -274,6 +289,7 @@ def register_script_function(function, name=None, eval_args=True, argcount if argcount and check_argcount else False) ) + def func_if(parser, _if, _then, _else=None): """If ``if`` is not empty, it returns ``then``, otherwise it returns ``else``.""" if _if.eval(parser): @@ -282,6 +298,7 @@ def func_if(parser, _if, _then, _else=None): return _else.eval(parser) return '' + def func_if2(parser, *args): """Returns first non empty argument.""" for arg in args: @@ -290,48 +307,60 @@ def func_if2(parser, *args): return arg return '' + def func_noop(parser, *args): """Does nothing :)""" return '' + def func_left(parser, text, length): """Returns first ``num`` characters from ``text``.""" return text[:int(length)] + def func_right(parser, text, length): """Returns last ``num`` characters from ``text``.""" return text[-int(length):] + def func_lower(parser, text): """Returns ``text`` in lower case.""" return text.lower() + def func_upper(parser, text): """Returns ``text`` in upper case.""" return text.upper() + def func_pad(parser, text, length, char): return char * (int(length) - len(text)) + text + def func_strip(parser, text): return re.sub("\s+", " ", text).strip() + def func_replace(parser, text, old, new): return text.replace(old, new) + def func_in(parser, text, needle): if needle in text: return "1" else: return "" + def func_inmulti(parser, text, value, separator=MULTI_VALUED_JOINER): """Splits ``text`` by ``separator``, and returns true if the resulting list contains ``value``.""" return func_in(parser, text.split(separator) if separator else [text], value) + def func_rreplace(parser, text, old, new): return re.sub(old, new, text) + def func_rsearch(parser, text, pattern): match = re.search(pattern, text) if match: @@ -341,6 +370,7 @@ def func_rsearch(parser, text, pattern): return match.group(0) return u"" + def func_num(parser, text, length): format = "%%0%dd" % int(length) try: @@ -349,6 +379,7 @@ def func_num(parser, text, length): value = 0 return format % value + def func_unset(parser, name): """Unsets the variable ``name``.""" if name.startswith("_"): @@ -359,6 +390,7 @@ def func_unset(parser, name): pass return "" + def func_set(parser, name, value): """Sets the variable ``name`` to ``value``.""" if value: @@ -369,16 +401,19 @@ def func_set(parser, name, value): func_unset(parser, name) return "" + def func_setmulti(parser, name, value, separator=MULTI_VALUED_JOINER): """Sets the variable ``name`` to ``value`` as a list; splitting by the passed string, or "; " otherwise.""" return func_set(parser, name, value.split(separator) if value and separator else value) + def func_get(parser, name): """Returns the variable ``name`` (equivalent to ``%name%``).""" if name.startswith("_"): name = "~" + name[1:] return parser.context.get(name, u"") + def func_copy(parser, new, old): """Copies content of variable ``old`` to variable ``new``.""" if new.startswith("_"): @@ -388,6 +423,7 @@ def func_copy(parser, new, old): parser.context[new] = parser.context.getall(old)[:] return "" + def func_copymerge(parser, new, old): """Copies content of variable ``old`` and appends it into variable ``new``, removing duplicates. This is normally used to merge a multi-valued variable into another, existing multi-valued variable.""" @@ -400,6 +436,7 @@ def func_copymerge(parser, new, old): parser.context[new] = newvals + list(set(oldvals) - set(newvals)) return "" + def func_trim(parser, text, char=None): """Trims all leading and trailing whitespaces from ``text``. The optional second parameter specifies the character to trim.""" @@ -408,6 +445,7 @@ def func_trim(parser, text, char=None): else: return text.strip() + def func_add(parser, x, y): """Add ``y`` to ``x``.""" try: @@ -415,6 +453,7 @@ def func_add(parser, x, y): except ValueError: return "" + def func_sub(parser, x, y): """Substracts ``y`` from ``x``.""" try: @@ -422,6 +461,7 @@ def func_sub(parser, x, y): except ValueError: return "" + def func_div(parser, x, y): """Divides ``x`` by ``y``.""" try: @@ -429,6 +469,7 @@ def func_div(parser, x, y): except ValueError: return "" + def func_mod(parser, x, y): """Returns the remainder of ``x`` divided by ``y``.""" try: @@ -436,6 +477,7 @@ def func_mod(parser, x, y): except ValueError: return "" + def func_mul(parser, x, y): """Multiplies ``x`` by ``y``.""" try: @@ -443,6 +485,7 @@ def func_mul(parser, x, y): except ValueError: return "" + def func_or(parser, x, y): """Returns true, if either ``x`` or ``y`` not empty.""" if x or y: @@ -450,6 +493,7 @@ def func_or(parser, x, y): else: return "" + def func_and(parser, x, y): """Returns true, if both ``x`` and ``y`` are not empty.""" if x and y: @@ -457,6 +501,7 @@ def func_and(parser, x, y): else: return "" + def func_not(parser, x): """Returns true, if ``x`` is empty.""" if not x: @@ -464,6 +509,7 @@ def func_not(parser, x): else: return "" + def func_eq(parser, x, y): """Returns true, if ``x`` equals ``y``.""" if x == y: @@ -471,6 +517,7 @@ def func_eq(parser, x, y): else: return "" + def func_ne(parser, x, y): """Returns true, if ``x`` not equals ``y``.""" if x != y: @@ -478,6 +525,7 @@ def func_ne(parser, x, y): else: return "" + def func_lt(parser, x, y): """Returns true, if ``x`` is lower than ``y``.""" try: @@ -487,6 +535,7 @@ def func_lt(parser, x, y): pass return "" + def func_lte(parser, x, y): """Returns true, if ``x`` is lower than or equals ``y``.""" try: @@ -496,6 +545,7 @@ def func_lte(parser, x, y): pass return "" + def func_gt(parser, x, y): """Returns true, if ``x`` is greater than ``y``.""" try: @@ -505,6 +555,7 @@ def func_gt(parser, x, y): pass return "" + def func_gte(parser, x, y): """Returns true, if ``x`` is greater than or equals ``y``.""" try: @@ -514,9 +565,11 @@ def func_gte(parser, x, y): pass return "" + def func_len(parser, text): return str(len(text)) + def func_performer(parser, pattern="", join=", "): values = [] for name, value in parser.context.items(): @@ -524,12 +577,14 @@ def func_performer(parser, pattern="", join=", "): values.append(value) return join.join(values) + def func_matchedtracks(parser, arg): if parser.file: if parser.file.parent: return str(parser.file.parent.album.get_num_matched_tracks()) return "0" + def func_firstalphachar(parser, text, nonalpha="#"): if len(text) == 0: return nonalpha @@ -539,9 +594,11 @@ def func_firstalphachar(parser, text, nonalpha="#"): else: return nonalpha + def func_initials(parser, text): return "".join(a[:1] for a in text.split(" ") if a[:1].isalpha()) + def func_firstwords(parser, text, length): try: length = int(length) @@ -554,6 +611,7 @@ def func_firstwords(parser, text, length): return text[:length] return text[:length].rsplit(' ', 1)[0] + def func_truncate(parser, text, length): try: length = int(length) @@ -561,6 +619,7 @@ def func_truncate(parser, text, length): length = None return text[:length].rstrip() + register_script_function(func_if, "if", eval_args=False) register_script_function(func_if2, "if2", eval_args=False, check_argcount=False) register_script_function(func_noop, "noop", eval_args=False, check_argcount=False) diff --git a/picard/similarity.py b/picard/similarity.py index 5b079f95e..222297775 100644 --- a/picard/similarity.py +++ b/picard/similarity.py @@ -33,6 +33,7 @@ _replace_words = { "disc 8": "CD8", } + def normalize(orig_string): """Strips non-alphanumeric characters from a string unless doing so would make it blank.""" string = strip_non_alnum(orig_string.lower()) @@ -40,6 +41,7 @@ def normalize(orig_string): string = orig_string return string + def similarity(a1, b1): """Calculates similarity of single words as a function of their edit distance.""" a2 = normalize(a1) @@ -52,6 +54,7 @@ def similarity(a1, b1): _split_words_re = re.compile('\W+', re.UNICODE) + def similarity2(a, b): """Calculates similarity of a multi-word strings.""" alist = filter(bool, _split_words_re.split(a.lower())) diff --git a/picard/tagger.py b/picard/tagger.py index 90f27d022..a3eabea4d 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -29,10 +29,15 @@ from collections import deque from functools import partial from itertools import chain + # A "fix" for http://python.org/sf/1438480 def _patched_shutil_copystat(src, dst): - try: _orig_shutil_copystat(src, dst) - except OSError: pass + try: + _orig_shutil_copystat(src, dst) + except OSError: + pass + + _orig_shutil_copystat = shutil.copystat shutil.copystat = _patched_shutil_copystat @@ -66,6 +71,7 @@ from picard.util import ( ) from picard.webservice import XmlWebService + class Tagger(QtGui.QApplication): tagger_stats_changed = QtCore.pyqtSignal() @@ -163,7 +169,7 @@ class Tagger(QtGui.QApplication): def remove_va_file_naming_format(merge=True): if merge: config.setting["file_naming_format"] = \ - "$if($eq(%compilation%,1),\n$noop(Various Artist albums)\n"+\ + "$if($eq(%compilation%,1),\n$noop(Various Artist albums)\n" + \ "%s,\n$noop(Single Artist Albums)\n%s)" %\ (config.setting["va_file_naming_format"].toString(), config.setting["file_naming_format"]) @@ -414,7 +420,7 @@ class Tagger(QtGui.QApplication): def remove_files(self, files, from_parent=True): """Remove files from the tagger.""" for file in files: - if self.files.has_key(file.filename): + if file.filename in self.files: file.clear_lookup_task() self._acoustid.stop_analyze(file) del self.files[file.filename] @@ -553,6 +559,7 @@ class Tagger(QtGui.QApplication): def instance(cls): return cls.__instance + def help(): print """Usage: %s [OPTIONS] [FILE] [FILE] ... diff --git a/picard/ui/cdlookup.py b/picard/ui/cdlookup.py index 41b0959bb..cc5bd4810 100644 --- a/picard/ui/cdlookup.py +++ b/picard/ui/cdlookup.py @@ -21,6 +21,7 @@ from PyQt4 import QtCore, QtGui from picard.ui.ui_cdlookup import Ui_Dialog from picard.mbxml import artist_credit_from_node, label_info_from_node + class CDLookupDialog(QtGui.QDialog): def __init__(self, releases, disc, parent=None): diff --git a/picard/ui/filebrowser.py b/picard/ui/filebrowser.py index 7394b8f13..a820c6d2f 100644 --- a/picard/ui/filebrowser.py +++ b/picard/ui/filebrowser.py @@ -18,7 +18,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # -import os, sys +import os +import sys from PyQt4 import QtCore, QtGui from picard import config from picard.formats import supported_formats diff --git a/picard/ui/infodialog.py b/picard/ui/infodialog.py index eff24d105..e81917cbd 100644 --- a/picard/ui/infodialog.py +++ b/picard/ui/infodialog.py @@ -54,10 +54,10 @@ class InfoDialog(QtGui.QDialog): pixmap.loadFromData(data) icon = QtGui.QIcon(pixmap) item.setIcon(icon) - s = "%s (%s)\n%d x %d" % (bytes2human.decimal(size), - bytes2human.binary(size), - pixmap.width(), - pixmap.height()) + s = "%s (%s)\n%d x %d" % (bytes2human.decimal(size), + bytes2human.binary(size), + pixmap.width(), + pixmap.height()) item.setText(s) self.ui.artwork_list.addItem(item) @@ -81,7 +81,7 @@ class FileInfoDialog(InfoDialog): info.append((_('Format:'), file.orig_metadata['~format'])) try: size = os.path.getsize(encode_filename(file.filename)) - sizestr = "%s (%s)" % (bytes2human.decimal(size), bytes2human.binary(size)) + sizestr = "%s (%s)" % (bytes2human.decimal(size), bytes2human.binary(size)) info.append((_('Size:'), sizestr)) except: pass @@ -95,9 +95,12 @@ class FileInfoDialog(InfoDialog): 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) + if ch == 1: + ch = _('Mono') + elif ch == 2: + ch = _('Stereo') + else: + ch = str(ch) info.append((_('Channels:'), ch)) text = '<br/>'.join(map(lambda i: '<b>%s</b><br/>%s' % i, info)) self.ui.info.setText(text) diff --git a/picard/ui/infostatus.py b/picard/ui/infostatus.py index 6bbf79f12..769a5b9db 100644 --- a/picard/ui/infostatus.py +++ b/picard/ui/infostatus.py @@ -46,7 +46,7 @@ class InfoStatus(QtGui.QWidget, Ui_InfoStatus): self.icon_cd = icontheme.lookup('media-optical') self.icon_file = QtGui.QIcon(":/images/file.png") self.icon_file_pending = QtGui.QIcon(":/images/file-pending.png") - self.icon_download = QtGui.QIcon(":/images/16x16/action-go-down-16.png") + self.icon_download = QtGui.QIcon(":/images/16x16/action-go-down-16.png") def _init_tooltips(self): t1 = _("Files") diff --git a/picard/ui/item.py b/picard/ui/item.py index d88302994..2bc705bf3 100644 --- a/picard/ui/item.py +++ b/picard/ui/item.py @@ -17,6 +17,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + class Item(object): def can_save(self): diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index c3910487f..d27746be7 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -54,21 +54,27 @@ _clusterlist_actions = ExtensionPoint() _track_actions = ExtensionPoint() _file_actions = ExtensionPoint() + def register_album_action(action): _album_actions.register(action.__module__, action) + def register_cluster_action(action): _cluster_actions.register(action.__module__, action) + def register_clusterlist_action(action): _clusterlist_actions.register(action.__module__, action) + def register_track_action(action): _track_actions.register(action.__module__, action) + def register_file_action(action): _file_actions.register(action.__module__, action) + def get_match_color(similarity, basecolor): c1 = (basecolor.red(), basecolor.green(), basecolor.blue()) c2 = (223, 125, 125) @@ -556,7 +562,7 @@ class TreeItem(QtGui.QTreeWidgetItem): obj.item = self if sortable: self.__lt__ = self._lt - self.setTextAlignment(1, QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter) + self.setTextAlignment(1, QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) def _lt(self, other): column = self.treeWidget().sortColumn() @@ -609,7 +615,7 @@ class AlbumItem(TreeItem): if update_tracks: oldnum = self.childCount() - 1 newnum = len(album.tracks) - if oldnum > newnum: # remove old items + if oldnum > newnum: # remove old items for i in xrange(oldnum - newnum): self.takeChild(newnum - 1) oldnum = newnum @@ -620,14 +626,14 @@ class AlbumItem(TreeItem): item.obj = track track.item = item item.update(update_album=False) - if newnum > oldnum: # add new items + if newnum > oldnum: # add new items items = [] - for i in xrange(newnum - 1, oldnum - 1, -1): # insertChildren is backwards + for i in xrange(newnum - 1, oldnum - 1, -1): # insertChildren is backwards item = TrackItem(album.tracks[i], False) - item.setHidden(False) # Workaround to make sure the parent state gets updated + item.setHidden(False) # Workaround to make sure the parent state gets updated items.append(item) self.insertChildren(oldnum, items) - for item in items: # Update after insertChildren so that setExpanded works + for item in items: # Update after insertChildren so that setExpanded works item.update(update_album=False) self.setIcon(0, AlbumItem.icon_cd_saved if album.is_complete() else AlbumItem.icon_cd) for i, column in enumerate(MainPanel.columns): @@ -653,17 +659,17 @@ class TrackItem(TreeItem): icon = TrackItem.icon_note oldnum = self.childCount() newnum = track.num_linked_files - if oldnum > newnum: # remove old items + if oldnum > newnum: # remove old items for i in xrange(oldnum - newnum): self.takeChild(newnum - 1).obj.item = None oldnum = newnum - for i in xrange(oldnum): # update existing items + for i in xrange(oldnum): # update existing items item = self.child(i) file = track.linked_files[i] item.obj = file file.item = item item.update() - if newnum > oldnum: # add new items + if newnum > oldnum: # add new items items = [] for i in xrange(newnum - 1, oldnum - 1, -1): item = FileItem(track.linked_files[i], False) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 19b6428b0..ada47174e 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -40,10 +40,14 @@ from picard.util import icontheme, webbrowser2, find_existing_path, throttle from picard.util.cdrom import get_cdrom_drives from picard.plugin import ExtensionPoint + ui_init = ExtensionPoint() -def register_ui_init (function): + + +def register_ui_init(function): ui_init.register(function.__module__, function) + class MainWindow(QtGui.QMainWindow): options = [ @@ -192,7 +196,9 @@ class MainWindow(QtGui.QMainWindow): pos = config.persist["window_position"] size = config.persist["window_size"] self._desktopgeo = self.tagger.desktop().screenGeometry() - if pos.x() > 0 and pos.y() > 0 and pos.x()+size.width() < self._desktopgeo.width() and pos.y()+size.height() < self._desktopgeo.height(): + if (pos.x() > 0 and pos.y() > 0 + and pos.x() + size.width() < self._desktopgeo.width() + and pos.y() + size.height() < self._desktopgeo.height()): self.move(pos) if size.width() <= 0 or size.height() <= 0: size = QtCore.QSize(780, 560) @@ -619,7 +625,7 @@ class MainWindow(QtGui.QMainWindow): # Use a custom file selection dialog to allow the selection of multiple directories file_dialog = QtGui.QFileDialog(self, "", current_directory) file_dialog.setFileMode(QtGui.QFileDialog.DirectoryOnly) - if sys.platform == "darwin": # The native dialog doesn't allow selecting >1 directory + if sys.platform == "darwin": # The native dialog doesn't allow selecting >1 directory file_dialog.setOption(QtGui.QFileDialog.DontUseNativeDialog) tree_view = file_dialog.findChild(QtGui.QTreeView) tree_view.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) @@ -671,7 +677,6 @@ removed in this version of Picard. Your file naming scheme has automatically been merged with that of single artist albums."""), QtGui.QMessageBox.Ok) - def open_bug_report(self): webbrowser2.open("http://musicbrainz.org/doc/Picard_Troubleshooting") diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index 0a42afc89..5e22a8b05 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -174,7 +174,7 @@ class MetadataBox(QtGui.QTableWidget): self.objects = set() self.selection_mutex = QtCore.QMutex() self.selection_dirty = False - self.editing = None # the QTableWidgetItem being edited + self.editing = None # the QTableWidgetItem being edited self.clipboard = [""] self.add_tag_action = QtGui.QAction(_(u"Add New Tag..."), parent) self.add_tag_action.triggered.connect(partial(self.edit_tag, "")) diff --git a/picard/ui/options/__init__.py b/picard/ui/options/__init__.py index 31aa340b3..da17d3b4b 100644 --- a/picard/ui/options/__init__.py +++ b/picard/ui/options/__init__.py @@ -27,6 +27,7 @@ class OptionsCheckError(Exception): self.title = title self.info = info + class OptionsPage(QtGui.QWidget): PARENT = None @@ -44,7 +45,7 @@ class OptionsPage(QtGui.QWidget): def save(self): pass - + def display_error(self, error): dialog = QtGui.QMessageBox(QtGui.QMessageBox.Warning, error.title, error.info, QtGui.QMessageBox.Ok, self) dialog.exec_() @@ -52,5 +53,6 @@ class OptionsPage(QtGui.QWidget): _pages = ExtensionPoint() + def register_options_page(page_class): _pages.register(page_class.__module__, page_class) diff --git a/picard/ui/options/plugins.py b/picard/ui/options/plugins.py index b19fd739f..87528170d 100644 --- a/picard/ui/options/plugins.py +++ b/picard/ui/options/plugins.py @@ -53,9 +53,9 @@ class PluginsOptionsPage(OptionsPage): self.ui.plugins.dropEvent = self.dropEvent self.ui.plugins.dragEnterEvent = self.dragEnterEvent if sys.platform == "win32": - self.loader="file:///%s" + self.loader = "file:///%s" else: - self.loader="file://%s" + self.loader = "file://%s" self.ui.install_plugin.clicked.connect(self.open_plugins) self.ui.folder_open.clicked.connect(self.open_plugin_dir) self.ui.plugin_download.clicked.connect(self.open_plugin_site) diff --git a/picard/ui/options/renaming.py b/picard/ui/options/renaming.py index 042f08292..c276fc12d 100644 --- a/picard/ui/options/renaming.py +++ b/picard/ui/options/renaming.py @@ -124,7 +124,7 @@ class RenamingOptionsPage(OptionsPage): 'ascii_filenames': self.ui.ascii_filenames.isChecked(), 'rename_files': self.ui.rename_files.isChecked(), 'move_files': self.ui.move_files.isChecked(), - 'use_va_format': False, # TODO remove + 'use_va_format': False, # TODO remove 'file_naming_format': unicode(self.ui.file_naming_format.toPlainText()), 'move_files_to': os.path.normpath(unicode(self.ui.move_files_to.text())) } @@ -137,9 +137,12 @@ class RenamingOptionsPage(OptionsPage): if not settings["move_files"]: return os.path.basename(filename) return filename - except SyntaxError, e: return "" - except TypeError, e: return "" - except UnknownFunction, e: return "" + except SyntaxError: + return "" + except TypeError: + return "" + except UnknownFunction: + return "" def update_examples(self): # TODO: Here should be more examples etc. @@ -259,12 +262,12 @@ class RenamingOptionsPage(OptionsPage): self.ui.move_files_to.setText(path) def test(self): - self.ui.renaming_error.setStyleSheet(""); + self.ui.renaming_error.setStyleSheet("") self.ui.renaming_error.setText("") try: self.check_format() except OptionsCheckError, e: - self.ui.renaming_error.setStyleSheet(self.STYLESHEET_ERROR); + self.ui.renaming_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.renaming_error.setText(e.info) return diff --git a/picard/ui/options/scripting.py b/picard/ui/options/scripting.py index a1209cda7..5120dabbd 100644 --- a/picard/ui/options/scripting.py +++ b/picard/ui/options/scripting.py @@ -80,12 +80,12 @@ class ScriptingOptionsPage(OptionsPage): self.ui.tagger_script.textChanged.connect(self.live_checker) def live_checker(self): - self.ui.script_error.setStyleSheet(""); + self.ui.script_error.setStyleSheet("") self.ui.script_error.setText("") try: self.check() except OptionsCheckError, e: - self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR); + self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.script_error.setText(e.info) return diff --git a/picard/ui/ratingwidget.py b/picard/ui/ratingwidget.py index ce95e54ba..b1d601854 100644 --- a/picard/ui/ratingwidget.py +++ b/picard/ui/ratingwidget.py @@ -85,7 +85,7 @@ class RatingWidget(QtGui.QWidget): self.tagger.xmlws.submit_ratings(ratings, None) def paintEvent(self, event=None): - painter = QtGui.QPainter(self) + painter = QtGui.QPainter(self) offset = self._offset for i in range(1, self._maximum + 1): if i <= self._rating or i <= self._highlight: diff --git a/picard/ui/util.py b/picard/ui/util.py index 31599bf13..29c8d58b2 100644 --- a/picard/ui/util.py +++ b/picard/ui/util.py @@ -42,4 +42,3 @@ class StandardButton(QtGui.QPushButton): icon = self.tagger.style().standardIcon(getattr(QtGui.QStyle, iconname)) args = [icon, label] QtGui.QPushButton.__init__(self, *args) - diff --git a/picard/util/__init__.py b/picard/util/__init__.py index 6cd980b23..3d6729c1a 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -24,7 +24,7 @@ import sys import unicodedata from time import time from PyQt4 import QtCore -from encodings import rot_13; +from encodings import rot_13 from string import Template from functools import partial @@ -76,6 +76,7 @@ class LockableObject(QtCore.QObject): _io_encoding = sys.getfilesystemencoding() + #The following was adapted from k3b's source code: #// On a glibc system the system locale defaults to ANSI_X3.4-1968 #// It is very unlikely that one would set the locale to ANSI_X3.4-1968 @@ -96,6 +97,7 @@ Translation: Picard will have problems with non-english characters in filenames until you change your charset. """) + def encode_filename(filename): """Encode unicode strings to filesystem encoding.""" if isinstance(filename, unicode): @@ -106,6 +108,7 @@ def encode_filename(filename): else: return filename + def decode_filename(filename): """Decode strings from filesystem encoding to unicode.""" if isinstance(filename, unicode): @@ -113,9 +116,11 @@ def decode_filename(filename): else: return filename.decode(_io_encoding) + def pathcmp(a, b): return os.path.normcase(a) == os.path.normcase(b) + def format_time(ms): """Formats time in milliseconds to a string representation.""" ms = float(ms) @@ -124,6 +129,7 @@ def format_time(ms): else: return "%d:%02d" % (round(ms / 1000.0) / 60, round(ms / 1000.0) % 60) + def sanitize_date(datestr): """Sanitize date format. @@ -141,6 +147,7 @@ def sanitize_date(datestr): date.append(num) return ("", "%04d", "%04d-%02d", "%04d-%02d-%02d")[len(date)] % tuple(date) + _unaccent_dict = {u'Æ': u'AE', u'æ': u'ae', u'Œ': u'OE', u'œ': u'oe', u'ß': 'ss'} _re_latin_letter = re.compile(r"^(LATIN [A-Z]+ LETTER [A-Z]+) WITH") def unaccent(string): @@ -160,26 +167,31 @@ def unaccent(string): result.append(char) return "".join(result) + _re_non_ascii = re.compile(r'[^\x00-\x7F]', re.UNICODE) def replace_non_ascii(string, repl="_"): """Replace non-ASCII characters from ``string`` by ``repl``.""" return _re_non_ascii.sub(repl, asciipunct(string)) + _re_win32_incompat = re.compile(r'["*:<>?|]', re.UNICODE) def replace_win32_incompat(string, repl=u"_"): """Replace win32 filename incompatible characters from ``string`` by ``repl``.""" return _re_win32_incompat.sub(repl, string) + _re_non_alphanum = re.compile(r'\W+', re.UNICODE) def strip_non_alnum(string): """Remove all non-alphanumeric characters from ``string``.""" return _re_non_alphanum.sub(u" ", string).strip() + _re_slashes = re.compile(r'[\\/]', re.UNICODE) def sanitize_filename(string, repl="_"): return _re_slashes.sub(repl, string) + def make_short_filename(prefix, filename, max_path_length=240, max_length=200, mid_length=32, min_length=2): """ @@ -220,7 +232,7 @@ def make_short_filename(prefix, filename, max_path_length=240, max_length=200, break if left > 0: - raise IOError, "File name is too long." + raise IOError("File name is too long.") return os.path.join(*[a.strip() for a in reversed(parts)]) @@ -307,7 +319,11 @@ def load_release_type_scores(setting): scores = {} values = setting.split() for i in range(0, len(values), 2): - scores[values[i]] = float(values[i+1]) if i+1 < len(values) else 0.0 + try: + score = float(values[i + 1]) + except IndexError: + score = 0.0 + scores[values[i]] = score return scores diff --git a/picard/util/icontheme.py b/picard/util/icontheme.py index 790a206d3..c9ff83b0c 100644 --- a/picard/util/icontheme.py +++ b/picard/util/icontheme.py @@ -41,6 +41,7 @@ ICON_SIZE_MENU = ('16x16',) ICON_SIZE_TOOLBAR = ('22x22',) ICON_SIZE_ALL = ('22x22', '16x16') + def lookup(name, size=ICON_SIZE_ALL): icon = QtGui.QIcon() if _current_theme: diff --git a/picard/util/mimetype.py b/picard/util/mimetype.py index e3325067d..a6abe3d52 100644 --- a/picard/util/mimetype.py +++ b/picard/util/mimetype.py @@ -28,6 +28,7 @@ MIME_TYPE_EXTENSION_MAP = { EXTENSION_MIME_TYPE_MAP = dict([(b, a) for a, b in MIME_TYPE_EXTENSION_MAP.items()]) + def get_from_data(data, filename=None, default=None): """Tries to determine the mime type from the given data.""" if data.startswith('\xff\xd8\xff'): @@ -43,11 +44,13 @@ def get_from_data(data, filename=None, default=None): else: return default + def get_from_filename(filename, default=None): """Tries to determine the mime type from the given filename.""" name, ext = os.path.splitext(os.path.basename(filename)) return EXTENSION_MIME_TYPE_MAP.get(ext, default) + def get_extension(mimetype, default=None): """Returns the file extension for a given mime type.""" - return MIME_TYPE_EXTENSION_MAP.get(mimetype, default) \ No newline at end of file + return MIME_TYPE_EXTENSION_MAP.get(mimetype, default) diff --git a/picard/util/queue.py b/picard/util/queue.py index 7203775d1..c6d286307 100644 --- a/picard/util/queue.py +++ b/picard/util/queue.py @@ -3,6 +3,7 @@ from collections import deque from PyQt4 import QtCore + class Queue: """Create a queue object with a given maximum size. @@ -50,7 +51,7 @@ class Queue: finally: self.mutex.unlock() - def remove(self,item): + def remove(self, item): """Remove an item from the queue.""" self.mutex.lock() try: diff --git a/picard/util/tags.py b/picard/util/tags.py index 2862f5357..46622f417 100644 --- a/picard/util/tags.py +++ b/picard/util/tags.py @@ -86,6 +86,7 @@ TAG_NAMES = { '~rating': N_('Rating'), } + def display_tag_name(name): if ':' in name: name, desc = name.split(':', 1) @@ -101,4 +102,3 @@ def display_tag_name(name): return '%s []' % (_(new_name),) else: return _(new_name) - diff --git a/picard/webservice.py b/picard/webservice.py index f869ab20a..af1e8c53f 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -74,11 +74,12 @@ class XmlNode(object): try: return self.attribs[name] except KeyError: - raise AttributeError, name + raise AttributeError(name) _node_name_re = re.compile('[^a-zA-Z0-9]') + def _node_name(n): return _node_name_re.sub('_', unicode(n)) @@ -338,7 +339,8 @@ class XmlWebService(QtCore.QObject): host = config.setting["server_host"] port = config.setting["server_port"] path = "/ws/2/%s/%s?inc=%s" % (entitytype, entityid, "+".join(inc)) - if params: path += "&" + "&".join(params) + if params: + path += "&" + "&".join(params) return self.get(host, port, path, handler, priority=priority, important=important, mblogin=mblogin) def get_release_by_id(self, releaseid, handler, inc=[], priority=True, important=False, mblogin=False): @@ -361,8 +363,10 @@ class XmlWebService(QtCore.QObject): filters.append((name, value)) else: value = _escape_lucene_query(value).strip().lower() - if value: query.append('%s:(%s)' % (name, value)) - if query: filters.append(('query', ' '.join(query))) + if value: + query.append('%s:(%s)' % (name, value)) + if query: + filters.append(('query', ' '.join(query))) params = [] for name, value in filters: value = str(QUrl.toPercentEncoding(QtCore.QString(value))) diff --git a/setup.py b/setup.py index 5bba12dfc..bb3268fe6 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import glob, re +import glob import os +import re import sys from StringIO import StringIO from ConfigParser import RawConfigParser diff --git a/tagger.py b/tagger.py index f854d93c7..57c7ebcd7 100755 --- a/tagger.py +++ b/tagger.py @@ -8,4 +8,3 @@ from picard.tagger import main localedir = os.path.join(os.path.dirname(sys.argv[0]), 'locale') main(localedir, True) - From 258e9c2084d8d689361fd7bb630f70ffe2a25ba0 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Mon, 1 Jul 2013 02:03:58 +0200 Subject: [PATCH 47/75] Use info dialog to display per-album error log. When an error occurs during loading of a release, it will be visible in info dialog for this release (which was unused). New icon was added to mark visually releases with errors, and scrollbars will appear in info dialog if needed. --- picard/album.py | 20 +- picard/resources.py | 266 +++++++++++++----- picard/ui/infodialog.py | 19 +- picard/ui/itemviews.py | 8 +- picard/ui/ui_infodialog.py | 37 ++- .../images/16x16/media-optical-error.png | Bin 0 -> 964 bytes .../images/22x22/media-optical-error.png | Bin 0 -> 1426 bytes resources/picard.qrc | 2 + ui/infodialog.ui | 46 ++- 9 files changed, 285 insertions(+), 113 deletions(-) create mode 100644 resources/images/16x16/media-optical-error.png create mode 100644 resources/images/22x22/media-optical-error.png diff --git a/picard/album.py b/picard/album.py index 6b749962e..7b4d283f1 100644 --- a/picard/album.py +++ b/picard/album.py @@ -62,6 +62,7 @@ class Album(DataObject, Item): self._discid = discid self._after_load_callbacks = queue.Queue() self.unmatched_files = Cluster(_("Unmatched Files"), special=True, related_album=self, hide_if_empty=True) + self.errors = [] def __repr__(self): return '<Album %s %r>' % (self.id, self.metadata[u"album"]) @@ -132,7 +133,7 @@ class Album(DataObject, Item): try: run_album_metadata_processors(self, m, release_node) except: - log.error(traceback.format_exc()) + self.error_append(traceback.format_exc()) self._release_node = release_node return True @@ -144,7 +145,7 @@ class Album(DataObject, Item): parsed = False try: if error: - log.error("%r", unicode(http.errorString())) + self.error_append(unicode(http.errorString())) # Fix for broken NAT releases if error == QtNetwork.QNetworkReply.ContentNotFoundError: nats = False @@ -164,12 +165,16 @@ class Album(DataObject, Item): parsed = self._parse_release(document) except: error = True - log.error(traceback.format_exc()) + self.error_append(traceback.format_exc()) finally: self._requests -= 1 if parsed or error: self._finalize_loading(error) + def error_append(self, msg): + log.error(msg) + self.errors.append(msg) + def _finalize_loading(self, error): if error: self.metadata.clear() @@ -216,7 +221,7 @@ class Album(DataObject, Item): try: run_track_metadata_processors(self, tm, self._release_node, track_node) except: - log.error(traceback.format_exc()) + self.error_append(traceback.format_exc()) totalalbumtracks = str(totalalbumtracks) @@ -239,14 +244,14 @@ class Album(DataObject, Item): try: parser.eval(script, track.metadata) except: - log.error(traceback.format_exc()) + self.error_append(traceback.format_exc()) # Strip leading/trailing whitespace track.metadata.strip_whitespace() # Run tagger script for the album itself try: parser.eval(script, self._new_metadata) except: - log.error(traceback.format_exc()) + self.error_append(traceback.format_exc()) self._new_metadata.strip_whitespace() for track in self.tracks: @@ -280,6 +285,7 @@ class Album(DataObject, Item): self._new_metadata = Metadata() self._new_tracks = [] self._requests = 1 + self.errors = [] require_authentication = False inc = ['release-groups', 'media', 'recordings', 'artist-credits', 'artists', 'aliases', 'labels', 'isrcs', 'collections'] @@ -391,7 +397,7 @@ class Album(DataObject, Item): return True def can_view_info(self): - return bool(self.loaded and self.metadata and self.metadata.images) + return (self.loaded and self.metadata and self.metadata.images) or self.errors def is_album_like(self): return True diff --git a/picard/resources.py b/picard/resources.py index 00a7d37db..301b2212e 100644 --- a/picard/resources.py +++ b/picard/resources.py @@ -2,7 +2,7 @@ # Resource object code # -# Created: dim. juin 30 21:56:31 2013 +# Created: lun. juil. 1 02:03:25 2013 # by: The Resource Compiler for PyQt (Qt v4.8.2) # # WARNING! All changes made in this file will be lost! @@ -1914,6 +1914,69 @@ qt_resource_data = "\ \xbc\xfe\x92\x5f\x48\x9e\xe4\xec\x1d\xf1\xc4\xbe\x03\xd8\xc1\xbf\ \xce\x15\x14\xa7\xaa\x2a\x88\xcd\x00\x00\x00\x00\x49\x45\x4e\x44\ \xae\x42\x60\x82\ +\x00\x00\x03\xc4\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\ +\x0d\xd7\x01\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xdd\x06\x0f\x0c\x1a\x34\x2d\xd4\xb7\xad\x00\x00\x03\x51\x49\x44\ +\x41\x54\x38\xcb\x75\x93\x4b\x4c\x5c\x55\x00\x86\xbf\x73\xce\xbd\ +\x97\xb9\x33\x17\x86\x81\x21\x20\x94\x16\x28\x8d\x8f\x34\x84\x82\ +\x82\x49\x8d\x80\x89\x62\x37\x26\x9a\xa8\xa9\x12\x74\xa7\x2d\x75\ +\xe3\x63\xa1\x31\x31\x1a\xdd\x18\x31\x31\x6a\xd0\xc4\xa6\x58\x7c\ +\xa4\xa9\x24\xc6\x52\x11\x88\x69\x2b\x2a\xc2\x68\x84\x85\xe0\xa2\ +\x64\xca\x33\x35\xa1\x33\x0e\x05\xee\x3c\xce\xbd\xd7\x0d\x4d\xd4\ +\xe8\xb7\xfc\x93\x6f\xf3\xe7\xff\x05\xff\xe2\x95\x47\x1f\x6e\xdf\ +\x72\x22\x4f\x7a\x4a\x75\x02\xf5\xbb\x71\x52\x79\xde\x45\x67\x6b\ +\x7b\xf0\xd5\x33\x67\xa7\xf9\x3f\xfa\x8f\x74\x0f\x9c\xec\xed\x99\ +\x5b\x9d\x9b\xbb\xb0\x93\xc9\x64\x3c\xad\x7d\x4f\x6b\x7f\x27\x93\ +\xb9\xbe\x32\xfb\xeb\xd4\xc9\xde\x9e\xb9\xfe\x23\xdd\x03\xff\x29\ +\x9f\x6e\x6d\x19\x5d\xfb\x64\xe8\x73\x2f\x9f\xf7\xaf\x4e\x4e\x06\ +\xab\xa3\xa3\xc1\xca\xc8\x48\xb0\x3e\x31\x11\x6c\x24\x12\xc1\xf6\ +\xea\x6a\xe0\x69\xed\xaf\x0e\x0d\x8d\x9e\x6e\x6d\x19\xbd\xe1\x29\ +\x80\xb7\xef\xbf\x6f\xe0\x81\xa7\x8f\x89\x58\x57\xd7\x23\xeb\xc3\ +\xc3\xb2\xb0\xb1\x01\xb9\x1c\x12\x10\x80\xb2\x2c\xa4\x52\x78\x3b\ +\x3b\x22\x7a\xe8\x50\x63\x3c\x56\x9a\xaa\x75\xb7\x5b\xc7\x2e\x2f\ +\x9e\x57\x7d\x27\x8e\xb5\x37\x28\xe3\xf8\xe1\x17\x5f\xba\x27\x35\ +\x35\x25\xfd\x42\x01\x9d\x4e\x93\x4f\x5d\x23\x9f\x4e\xa1\x37\x37\ +\x91\xa6\x89\x0a\x87\x91\x86\x49\x10\x04\xc4\xda\xda\x6b\xe6\x27\ +\xc6\xc3\x7b\xef\xbe\xeb\x17\xd5\xd6\x76\xc7\xcb\xbd\x27\x9e\xd9\ +\x2b\xaf\x6f\x55\xea\xcd\x4d\xfc\x5c\x16\x59\x54\x84\x15\x8f\x13\ +\x69\xd8\x4f\x64\x5f\x1d\x76\xcd\x1e\xdc\xe5\x2b\xa8\x70\x18\xdf\ +\x75\xf1\xb5\x26\xba\x6f\x5f\x21\xf1\xfb\x82\x6d\x00\x9d\x15\x0d\ +\xfb\xeb\xb3\x4b\x4b\x28\xdb\xc6\xcf\xe5\xb0\x62\x65\xa4\x13\x09\ +\x16\x26\xc6\x01\x82\x3b\x9f\x7f\x41\x94\xb6\xb4\x90\x9a\xfe\x09\ +\xbb\xba\x06\xad\x35\xf1\xba\xfa\x38\xd0\x29\x81\x7a\x2b\x1c\xb6\ +\x54\x28\x84\xe1\x38\x20\x04\xa1\xea\x6a\x16\x26\xc6\x83\x38\xa2\ +\x49\x07\x41\x45\xa2\xff\x2d\x3f\x54\x55\x85\xbb\xb2\x8c\x9f\xcf\ +\xe3\xb9\x2e\xaa\xa8\xc8\x04\xea\x8d\xdd\x32\x85\xb2\x6d\xd0\x1a\ +\x69\x59\x98\xb1\x18\x4a\x08\x7f\x59\x17\x96\x01\x4c\xc8\x03\x21\ +\xcf\x75\xf1\xf3\x79\x30\x0c\x7c\xad\x05\x20\x24\x90\x2c\xb8\xae\ +\x56\xb6\x8d\xb2\x6d\x42\x95\x95\x78\xdb\xdb\xdc\xfe\xec\x73\xd2\ +\x96\xea\x6a\x08\x16\x5b\xfb\xfa\xac\xcc\xdc\x1c\xca\x0e\xe3\xe5\ +\x72\xf8\xf9\x3c\x6e\x26\xe3\x01\x49\x09\x5c\xbc\xb6\xb4\x94\x36\ +\x1c\x07\xc3\x71\x88\x34\x36\x52\xf8\x33\x45\x45\x47\x87\x78\xf0\ +\xd2\x77\xa1\x87\x26\x7f\x28\x2f\xbe\xf5\x36\x79\x65\xf0\x14\x46\ +\x34\x8a\x9f\xcd\x22\x2d\x8b\xd4\xda\xaa\x0b\x5c\x30\x80\xc1\xf1\ +\xfe\x37\xbb\x9f\xf8\xe8\x54\x85\x70\x1c\xa4\x65\x21\x0f\xdc\xcc\ +\x76\x32\x49\x6a\x66\x9a\x42\x3a\x8d\xb4\x2c\x4a\x5b\x5a\x10\x42\ +\x12\x78\x1e\x91\xda\x5a\x66\x5e\x7f\x2d\x4b\xb1\x33\xa8\x12\x33\ +\x3f\xaf\x75\x54\x55\xde\x52\x6d\x9a\x37\x95\x1e\x3c\x58\x22\xa4\ +\x44\x28\x85\xb4\x2c\xac\xf2\x72\x42\xd5\x35\x98\xb1\x32\x84\x52\ +\xe8\xad\x2d\x4a\x9b\x9b\x59\x39\xf7\x55\x66\x7e\xfe\xb7\xb3\x6f\ +\x7c\x76\xe6\x43\x05\x30\x76\x79\xf1\x7c\xf3\xca\xd2\xbd\xc5\x76\ +\x38\x1e\x6b\x6a\x0a\xa1\x14\x3a\x9b\x25\xf0\x7d\x7c\x20\x90\x12\ +\xa3\xa4\x84\x68\x53\x13\xc9\xe1\x2f\xf2\xb3\x23\xe7\x26\x8f\x5f\ +\x9a\x3c\x0a\x28\x75\x63\xd3\x5f\xae\xad\x7f\x7a\x60\x33\x53\x37\ +\xfb\xcd\xd7\xf5\x65\x7b\x6a\x2d\xd3\x71\x64\x28\x1e\xc7\x88\x46\ +\xd1\x4a\xf1\xc7\xca\xb2\x37\xf6\xee\x3b\x3b\xc9\x85\x85\x8f\x9f\ +\xfa\xfe\xc7\xa3\xbb\x5a\x20\xfe\xf6\x27\x01\x18\x8f\xf7\x3c\x76\ +\x38\x12\x09\xf7\x1a\xca\xe8\x12\x52\xd4\x0a\x21\x84\xef\xfb\x6b\ +\x5a\xeb\x6f\x4d\xd3\xfc\xe0\xfd\xf7\x06\xfe\x71\xe7\xbf\x00\xb7\ +\x72\x61\x14\x06\xc0\xe7\xf4\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ +\x42\x60\x82\ \x00\x00\x02\xab\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -1959,50 +2022,6 @@ qt_resource_data = "\ \x3f\x5f\xda\x68\xfd\xbf\x00\x00\x38\x7c\x45\xbd\x0f\x00\xc3\x37\ \xc5\xd9\x7f\xf5\xfc\x02\x73\x4a\x12\x2a\x86\x68\x1c\xcb\x00\x00\ \x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x02\x97\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\ -\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\x72\ -\x67\x9b\xee\x3c\x1a\x00\x00\x02\x29\x49\x44\x41\x54\x38\x8d\x95\ -\x52\x3b\x68\x53\x61\x18\x3d\xff\x7d\x24\x37\x52\x2c\x44\x4d\x3a\ -\x24\x45\xa9\xaf\x45\xb0\x76\x32\xad\x08\x1d\x04\x25\x1d\x8a\x88\ -\x8b\x43\x1d\x45\xb0\x88\xd6\xc1\x5d\x10\x7a\x47\x07\xa1\x4b\xa6\ -\x0e\x52\x51\x09\x6d\x84\x4c\x3e\xae\x85\x50\x4b\x34\x34\x42\x8b\ -\x42\x87\x94\xa4\x35\x5a\x31\x4d\xef\xbd\xff\xe3\x73\x88\xc4\xd4\ -\xa4\x62\x0f\xfc\xcb\xe1\xe3\x7c\xff\x39\xdf\x61\x44\x84\x56\x0c\ -\x4e\x98\x65\x10\xa2\xe8\x04\x86\x8a\x33\xc9\x7b\x5a\x29\xa3\x6d\ -\x88\x10\xb5\x6f\x3e\x81\x50\x02\x42\xf9\x90\x8a\x43\x28\x0e\x2e\ -\x3d\x3c\x4c\xdd\x6e\x13\x6e\x17\x00\xc0\x98\x86\xdc\x6a\x1a\x9e\ -\xd8\x86\x2b\xb6\xe0\xf1\x2d\x9c\x3d\x32\xda\xf1\x53\x5a\x47\x16\ -\x0c\x8c\xfd\x7e\x60\x00\x63\x90\x8a\xef\x45\xa0\x91\x0b\x11\x81\ -\x40\x00\xd1\xae\x02\xc6\xd0\x3d\xd3\x21\x85\x44\x73\xb7\x0e\x97\ -\x88\x2c\xa5\x24\x88\x14\x88\x14\x7c\xb9\x0d\xae\x7c\x30\x1d\xee\ -\xe0\x5d\xb3\x99\x3a\xd3\xf0\xce\x20\x85\x54\x2c\xd2\xd7\x7f\xeb\ -\xf2\x83\x90\xa1\x07\x40\xa4\x2c\x49\x02\x8a\x49\x40\x17\x00\x04\ -\xca\x9b\xcb\x60\xfa\x25\xdc\xb8\x7a\xdf\x02\x01\x9c\xfb\x98\xc9\ -\xa6\xea\xdf\x7f\x7c\x4d\x31\x22\xc2\xd0\x84\x39\x73\xee\xf4\xc5\ -\xe4\xf0\xc0\x68\xb0\x58\x79\x03\xc9\x7c\x48\xf8\xf8\xe6\x96\xb0\ -\x56\xfb\x04\xae\x7c\x80\x08\x44\x1a\xce\xf7\x5e\xc3\x62\x61\xc1\ -\x2b\x2e\xe7\xd3\x6f\x27\xf9\x15\xad\xe1\x15\x63\xaf\xf3\x99\xca\ -\x97\xf2\x12\xc5\xc2\xc7\x41\x1a\xc7\xba\xb7\x82\x92\xfb\x11\x2c\ -\x28\x10\x08\x69\x30\x43\x3a\xfa\x63\xc3\xa8\x6e\x56\xa9\xb8\x92\ -\xaf\x10\xe1\x7a\x33\x44\xc7\xe6\x35\x10\x92\xd3\x2f\x1f\xbb\x06\ -\xb3\x60\x9a\x26\x36\xfc\xcf\x30\x2d\x1d\x01\x4b\x43\x20\xa4\x23\ -\x1e\x3e\x86\x48\xa8\x0f\xd9\x57\xb3\x2e\x29\x24\x1d\x9b\xd7\x76\ -\x5c\xc1\xb1\x79\x41\x08\x6f\x7c\x3a\xf3\xa8\x7e\x70\x5f\x1c\x5d\ -\xa1\x6e\x98\xc1\xc6\xe6\xfd\x5d\xdd\x38\x15\xb9\x80\x74\xf6\x69\ -\x9d\x73\x7f\xdc\xb1\x79\xa1\xe3\x19\x1d\x9b\x4f\x95\x36\x56\x33\ -\xb9\x0f\x8e\x3f\xd0\x33\x02\xcd\x60\x30\x02\x1a\xce\x44\x47\x90\ -\x7b\x3f\xef\xaf\x57\xd7\xe6\x1c\x9b\x4f\xed\x38\x63\x5b\x03\x14\ -\xc6\x16\x0a\xf3\x4b\xbd\xb1\xc3\xf1\xa3\xe1\x04\xd3\x0d\x0d\xd5\ -\xd2\x4f\xca\x2d\x3a\x65\x25\x1b\xbe\xff\x59\x24\xc7\xe6\x35\x92\ -\x48\xce\x66\x5f\xb8\x87\x70\x12\x07\xd4\x09\x3c\x9f\x7b\xe6\x2a\ -\xf9\xc7\x77\x2b\x58\xe2\x8e\x41\x7f\x93\x7b\xc1\x2e\x55\xfe\x7f\ -\xfc\x02\xaa\x26\x0d\x2a\x62\xcc\x1b\x11\x00\x00\x00\x00\x49\x45\ -\x4e\x44\xae\x42\x60\x82\ \x00\x00\x03\x8f\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -2833,6 +2852,98 @@ qt_resource_data = "\ \x03\xa8\xc4\xbc\x7a\xfa\x46\x00\x0e\x00\xf6\x00\xbe\x62\xbe\x17\ \x67\xe6\x23\x00\x50\xfa\xb9\x2b\xca\xbe\x01\xd3\xd8\xe4\x64\x97\ \x03\xd0\xba\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x05\x92\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x16\x00\x00\x00\x16\x08\x06\x00\x00\x00\xc4\xb4\x6c\x3b\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\ +\x0d\xd7\x01\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xdd\x06\x0f\x0c\x19\x11\x4d\xfd\x30\x29\x00\x00\x05\x1f\x49\x44\ +\x41\x54\x38\xcb\xb5\x95\x6d\x8c\x5c\x55\x19\xc7\x7f\xe7\x9c\x7b\ +\xef\xdc\xb9\x3b\xfb\xd6\xe9\xb2\xcb\xbe\x18\xdb\x9d\xd2\xed\x3a\ +\x48\x48\x28\xaf\x0d\x2f\xc1\x1a\x05\xc5\x95\x28\x9a\xf8\x61\x79\ +\x89\xd1\x88\x26\x46\x4d\x93\x56\x3e\xa1\x82\x46\x82\x40\x42\x13\ +\x12\x08\x0b\x44\x23\xee\x12\x6b\x84\x6a\x6c\x2b\x21\x52\xa2\xd8\ +\x25\xdb\x6c\xd9\xd2\x29\xc8\xd2\xe2\x38\xed\xb2\xdb\x76\xee\xdc\ +\x3b\x77\xee\xcb\x39\x7e\xe8\x76\xb3\x85\x60\xe0\x83\xe7\xcb\xf9\ +\x70\x92\xdf\xf3\xe4\xff\xfc\x9f\xff\x81\xff\xd3\x11\xff\xeb\xf1\ +\xa1\xad\x37\x8e\xfb\x1d\xed\x5f\x4c\x2c\xfb\x0a\x23\x65\x1f\x20\ +\x84\xd1\x27\xec\x24\x79\xb5\xbd\xee\xef\xfa\xfe\x9e\x7d\x4f\x7d\ +\xac\x6a\x53\x9b\x46\xc6\x9f\xbc\x7c\xf3\xfc\xf4\xb6\x6d\x7e\xfd\ +\xad\xb7\xb2\xd8\xf7\x4d\x1a\xc7\x26\x8d\x13\x13\xfb\x75\x73\xa6\ +\x72\x44\xcf\xfe\xfc\xfe\xf0\xc9\x2b\x36\xbf\x3b\xb5\x69\x64\xfc\ +\x23\x75\xbc\x6b\x43\xe9\x91\xe1\x9b\xbf\xf0\x8d\xd2\xf6\xed\xdd\ +\x18\x23\xc2\x63\xc7\x88\x6a\x27\x31\x3a\xc3\xca\xbb\x58\x9e\x87\ +\x7b\x61\x1f\xb9\x9e\x1e\xb2\x38\x61\x6e\xc7\xf6\xe0\xd8\x4b\x2f\ +\x3d\x35\x56\x39\x7a\xf7\x6a\x8e\x7a\x3f\x74\xf4\x5b\xdf\xbe\xe3\ +\xa2\x1d\x3f\xee\xf4\xe7\xe6\xc4\xa9\x7f\x1e\x20\x3a\xb1\x80\x4e\ +\x53\xa4\x65\xa3\x72\x39\x8c\xce\x40\x6b\x74\xb3\x89\x50\x92\x81\ +\xaf\xde\xe6\x64\xd5\xea\xa5\x9f\x5b\x5a\x5c\xff\xdb\xf7\x16\xff\ +\xf0\x01\xf0\xd4\xa6\x91\xf1\x81\xd1\xd1\x6d\xe5\x07\x1e\xe8\x5c\ +\xf8\xeb\x8b\xd4\xe7\x0e\x63\xb4\x41\x67\x1a\x84\x40\xb5\xb5\xa1\ +\x5c\x17\x74\x86\x52\x12\x74\x0a\x46\xa3\x5b\x11\xbd\x9f\xbf\x49\ +\x36\xa6\xa7\xcb\xd7\xda\xf6\xf1\xdf\xff\xa7\x36\x03\x20\xcf\x81\ +\x97\x8a\x6b\x7e\x76\xf1\xa3\x3b\xbb\xfc\x4a\x85\xc4\xaf\x93\xeb\ +\xe9\x41\x3a\x0e\x48\x81\xce\x34\x59\x9a\x92\x35\x9b\x98\x38\x46\ +\x87\x0d\x4c\x2b\x84\x24\xc2\xa4\x09\x59\xc3\x67\xe3\x3d\xf7\x58\ +\x4b\x03\xfd\x0f\x9f\xd7\xf1\x7d\x63\xb7\x8c\x5f\xf7\x99\xad\x5f\ +\xef\xbb\xea\x6a\xab\x71\xa4\x02\x3a\x43\x5a\x0a\xa1\x14\x26\x4d\ +\xc9\x0f\x0d\x61\x17\x0a\x58\xed\x05\xa4\x49\x11\xad\x00\x29\x41\ +\x28\x05\xda\x90\xd4\x7d\x72\x3d\x17\xd0\x38\xf6\x8e\x3d\x54\x2a\ +\x9d\x7e\x79\x66\xe6\x1f\x16\x40\x58\x68\xfb\xda\x27\xbf\x7c\xab\ +\xd3\x5a\x58\x00\x61\x10\x18\x4c\x9a\xa0\x24\xe4\x3e\x31\xc8\x99\ +\x43\xb3\x54\x9e\x7d\x16\x8d\x61\xf8\xfa\x6b\x19\xdc\xba\x85\x2c\ +\x6c\x90\x26\x29\x69\xba\x88\xf4\x0a\x08\x27\xc7\xf0\x0d\x37\xca\ +\x57\x5e\xf8\xe3\x0f\x80\x47\xce\x4a\xe1\x38\x5b\x0a\x03\x03\xe8\ +\x66\x88\xb2\x2c\xa4\x49\x31\x61\x1d\xcb\x82\xe0\xc8\x61\x4e\xec\ +\x7e\xc1\x0c\x6d\xdc\xf8\xaf\x8b\x46\x3f\xb5\x3b\x98\x3f\x9e\xd5\ +\x5e\x3e\x80\xbd\xb6\x97\xd6\x7b\x0b\x08\x21\x10\x96\x8d\x8e\x63\ +\x8a\xa5\x12\x51\x92\x0c\xac\x68\x9c\x64\xba\x4d\x3a\x0e\xd2\x71\ +\x90\xb6\x42\x89\x04\x19\x9d\x41\xa5\x01\xaf\x3d\xf3\x34\x6b\x07\ +\x87\xfe\x74\xc9\xc4\xc4\xf0\xc8\x13\x4f\xdc\x6c\x07\xc1\xd6\xa3\ +\x7f\xfe\x0b\x4e\xff\x10\xaa\xa3\x1b\xab\x78\x01\xaa\xd0\x0e\x42\ +\x62\x7b\x1e\x49\x12\x5b\x2b\x60\x21\xce\xfa\x59\x58\x16\xd2\x92\ +\x48\x1d\x23\x92\x26\xa4\x2d\x8a\x8e\x43\xbd\x52\xf9\xc5\xb9\xa1\ +\x1c\xac\x55\xff\x66\xc0\xe0\xe4\x31\x56\x0e\x23\x25\xc6\x18\x8c\ +\xd6\x48\xb9\xe2\x05\x2c\x00\x5b\x59\x91\x49\x92\xbc\x72\x5d\x8c\ +\x57\x40\x77\xac\x45\x46\x29\x5a\xd9\xf4\x5e\x73\x35\x8d\x03\x33\ +\x8f\x4d\x5c\x7e\xd9\xc5\x00\xc3\x8d\xe0\xa1\xbe\xbb\x6e\x17\x59\ +\xe8\x93\xf8\x3e\x46\x28\x90\x12\xa1\x14\xb2\x15\xe1\xd8\x4e\xb6\ +\x02\xce\x5b\xd6\xbb\x61\xad\xb6\xc1\xed\xec\x40\x7b\x05\x54\xc7\ +\x1a\xb4\x86\x2c\x8a\xe9\xfd\xd2\xad\xb8\xa5\x91\x91\xd2\xe4\xef\ +\x5a\x18\xa3\x07\xef\xfe\x8e\x2a\x5e\x76\x09\x61\xe5\x10\xd2\xb6\ +\x48\xc3\x00\x21\x24\x6e\x5f\x1f\xf5\x6a\x15\x2f\x9f\x6f\xae\x80\ +\x59\x5c\x7a\xf4\xcd\xc9\xc9\x5f\x7d\xfa\x7b\xdf\x15\x32\x6a\xa1\ +\xd6\xf4\x20\x0b\x1d\x24\x41\x48\xdc\x08\xf1\x46\xca\x94\xee\xbd\ +\x54\x0a\x21\xa5\x30\x29\xfe\xdb\xef\x20\xe3\x08\xd5\xd6\x85\xae\ +\x87\xe8\x2c\xc6\x2e\x16\x39\x3a\x39\x89\xe7\x38\xaf\xae\x80\x77\ +\x4c\x3d\xf7\xf0\xc0\xd2\xe2\x2f\x47\xc7\xc7\x6d\xab\x50\x40\xd8\ +\x36\x69\x10\x20\x8c\x44\x66\x82\xe0\xdf\x55\x92\xd3\xf5\x65\xdf\ +\xa6\xd8\x39\x85\xed\x80\x0c\xea\x20\x14\x6e\x5f\x1f\x3a\x8a\xa8\ +\xbe\x3e\x6b\x46\xcf\xf8\xb7\x9d\xb7\xd2\xdf\xf4\xbc\x46\x63\x76\ +\xf6\xb3\xfd\x63\x63\x02\xc0\x20\xc0\x80\x11\x02\x1d\xa7\xc8\x7c\ +\x1e\xe5\x79\x28\xc7\x81\x2c\x83\x34\x45\x87\x0d\x72\xbd\xfd\x78\ +\xeb\xd6\xf3\xc6\x4f\xef\x25\x0e\xc2\x5d\xb7\x3c\xff\xfc\xc4\x79\ +\xe0\xdf\x2c\x2c\x4c\xdf\xd5\x6a\x89\xb0\x19\x5e\x57\xbc\x66\xcb\ +\xd9\xe8\x13\x12\x8c\x21\x0b\xc2\xb3\x6b\xea\xd8\x08\x0c\x64\x29\ +\x26\x4d\x71\xfb\x07\xf1\xd6\xad\xe3\xed\x9d\x3b\x59\x9a\x9d\x3d\ +\x75\xd3\xde\x7d\x57\x01\x06\x30\x6a\x39\x3a\x5d\x20\xff\xf8\xe2\ +\xe2\xcc\x57\xea\xf5\xf2\xc9\xd7\xa6\x4b\x6b\xaf\xbc\x12\xbb\xab\ +\x0b\x1d\x27\x68\x9d\x21\x2c\x85\xca\x39\x08\x29\xb1\x3b\x3b\x28\ +\x94\x86\xc9\xd2\x84\x23\x0f\x3e\x48\xf5\xd0\x6c\xf4\x93\xb9\xc3\ +\x5b\x8e\x47\x51\x0b\x48\x57\x83\xe5\xf2\x6d\x4f\x54\xab\x2f\x6e\ +\xe8\xee\x92\x07\xff\xfe\xca\x66\x1d\x35\x85\x55\x68\xc7\xee\xee\ +\xc6\x2e\x16\x51\x9d\x9d\x88\x8e\x76\xc2\x24\xe1\xe0\xbe\xbd\xec\ +\x79\x6e\x8a\x7a\xb3\x39\x75\xfb\x9e\xbd\x77\xce\x37\x9b\x75\x20\ +\x58\x06\xeb\xd5\x41\x2f\x97\x87\xd9\x0e\x58\x6d\xae\xdb\xb6\xe3\ +\x47\x3f\xdc\xad\xb5\x5e\xdf\x68\x06\x56\xab\xd5\x12\x00\xb9\x5c\ +\x0e\x2f\xef\x69\xdb\xb2\xab\xbf\x7e\xfa\x99\xb1\xd7\xe7\xe7\x4f\ +\x01\xa7\x81\x08\x68\x2d\x4b\xa1\x3f\xec\xcf\x93\xab\xd2\xcf\x2a\ +\x97\xcb\x5d\xe5\x72\xb9\xdf\x75\xdd\xb6\x5a\xad\x56\xdb\xbf\x7f\ +\xff\x49\xdf\xf7\xa3\x73\xdd\x9d\xd3\x75\x35\xe0\xbf\x05\xd5\x4c\ +\xec\x72\xd2\x98\xab\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ +\x82\ \x00\x00\x03\x12\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -9138,16 +9249,16 @@ qt_resource_name = "\ \x08\x89\x88\x67\ \x00\x70\ \x00\x69\x00\x63\x00\x61\x00\x72\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x17\ +\x08\x1f\x12\x27\ +\x00\x6d\ +\x00\x65\x00\x64\x00\x69\x00\x61\x00\x2d\x00\x6f\x00\x70\x00\x74\x00\x69\x00\x63\x00\x61\x00\x6c\x00\x2d\x00\x65\x00\x72\x00\x72\ +\x00\x6f\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x15\ \x00\x24\xf9\x87\ \x00\x61\ \x00\x63\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2d\x00\x67\x00\x6f\x00\x2d\x00\x64\x00\x6f\x00\x77\x00\x6e\x00\x2d\x00\x31\x00\x36\ \x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x17\ -\x09\xec\xdc\x07\ -\x00\x61\ -\x00\x63\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2d\x00\x67\x00\x6f\x00\x2d\x00\x62\x00\x6f\x00\x74\x00\x74\x00\x6f\x00\x6d\x00\x2d\ -\x00\x31\x00\x36\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x11\ \x0f\xe3\xd5\x67\ \x00\x64\ @@ -9211,8 +9322,8 @@ qt_resource_struct = "\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x1a\x00\x00\x00\x02\ \x00\x00\x02\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x57\x19\ -\x00\x00\x02\x8c\x00\x02\x00\x00\x00\x0e\x00\x00\x00\x2f\ -\x00\x00\x01\xce\x00\x02\x00\x00\x00\x0e\x00\x00\x00\x21\ +\x00\x00\x02\x8c\x00\x02\x00\x00\x00\x0e\x00\x00\x00\x30\ +\x00\x00\x01\xce\x00\x02\x00\x00\x00\x0f\x00\x00\x00\x21\ \x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x00\xe0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1f\ \x00\x00\x00\xf0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1e\ @@ -9236,39 +9347,40 @@ qt_resource_struct = "\ \x00\x00\x01\xde\x00\x00\x00\x00\x00\x01\x00\x00\x55\x9d\ \x00\x00\x01\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x52\x58\ \x00\x00\x01\x20\x00\x00\x00\x00\x00\x01\x00\x00\x50\xdf\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xe0\x3a\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x01\xcb\x87\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xc2\x5a\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xd5\xed\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x02\x22\xc4\ -\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\xa0\x1a\ -\x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x97\xff\ -\x00\x00\x06\x42\x00\x00\x00\x00\x00\x01\x00\x00\xad\xd0\ -\x00\x00\x06\x8e\x00\x00\x00\x00\x00\x01\x00\x00\xb5\x9c\ -\x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x85\x2e\ -\x00\x00\x05\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x94\xa7\ -\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xa8\xb6\ -\x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x8b\x7d\ -\x00\x00\x06\x18\x00\x00\x00\x00\x00\x01\x00\x00\xaa\xba\ -\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\xa5\x8f\ -\x00\x00\x05\x0a\x00\x00\x00\x00\x00\x01\x00\x00\xbc\xf5\ -\x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x00\x9b\xec\ -\x00\x00\x06\x66\x00\x00\x00\x00\x00\x01\x00\x00\xb0\xa5\ -\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\xb8\x73\ -\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x73\x34\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xe6\xfd\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x01\xd2\x4a\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xc9\x1d\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xdc\xb0\ +\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x02\x29\x87\ +\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\xa1\x47\ +\x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x99\x2c\ +\x00\x00\x06\x42\x00\x00\x00\x00\x00\x01\x00\x00\xb4\x93\ +\x00\x00\x06\x8e\x00\x00\x00\x00\x00\x01\x00\x00\xbc\x5f\ +\x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x86\x5b\ +\x00\x00\x05\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x95\xd4\ +\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xa9\xe3\ +\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\xab\xe7\ +\x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x8c\xaa\ +\x00\x00\x06\x18\x00\x00\x00\x00\x00\x01\x00\x00\xb1\x7d\ +\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\xa6\xbc\ +\x00\x00\x05\x0a\x00\x00\x00\x00\x00\x01\x00\x00\xc3\xb8\ +\x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x00\x9d\x19\ +\x00\x00\x06\x66\x00\x00\x00\x00\x00\x01\x00\x00\xb7\x68\ +\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\xbf\x36\ +\x00\x00\x04\x90\x00\x00\x00\x00\x00\x01\x00\x00\x76\xfc\ \x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x68\x62\ \x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x60\x89\ \x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x6d\xff\ \x00\x00\x03\xae\x00\x00\x00\x00\x00\x01\x00\x00\x65\x37\ -\x00\x00\x05\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x81\x9a\ +\x00\x00\x05\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x82\xc7\ +\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x73\x34\ \x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\x6f\xbb\ -\x00\x00\x04\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x75\xe3\ \x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x09\ -\x00\x00\x04\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x7c\x11\ +\x00\x00\x04\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x7d\x3e\ \x00\x00\x03\x2c\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x39\ -\x00\x00\x05\x0a\x00\x00\x00\x00\x00\x01\x00\x00\x7e\x46\ +\x00\x00\x05\x0a\x00\x00\x00\x00\x00\x01\x00\x00\x7f\x73\ \x00\x00\x03\x88\x00\x00\x00\x00\x00\x01\x00\x00\x62\xa6\ -\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x78\x7e\ +\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x79\xab\ " def qInitResources(): diff --git a/picard/ui/infodialog.py b/picard/ui/infodialog.py index e81917cbd..39ab7e7ab 100644 --- a/picard/ui/infodialog.py +++ b/picard/ui/infodialog.py @@ -18,7 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import os.path -from PyQt4 import QtGui +from PyQt4 import QtGui, QtCore from picard.util import format_time, encode_filename, bytes2human from picard.ui.ui_infodialog import Ui_InfoDialog @@ -102,7 +102,9 @@ class FileInfoDialog(InfoDialog): else: ch = str(ch) info.append((_('Channels:'), ch)) - text = '<br/>'.join(map(lambda i: '<b>%s</b><br/>%s' % i, info)) + text = '<br/>'.join(map(lambda i: '<b>%s</b><br/>%s' % + (QtCore.Qt.escape(i[0]), + QtCore.Qt.escape(i[1])), info)) self.ui.info.setText(text) @@ -114,4 +116,15 @@ class AlbumInfoDialog(InfoDialog): def _display_info_tab(self): tab = self.ui.info_tab - self.tab_hide(tab) + album = self.obj + if album.errors: + text = '<br />'.join(map(lambda s: '<font color="darkred">%s</font>' % + '<br />'.join(unicode(QtCore.Qt.escape(s)) + .replace('\t', ' ') + .replace(' ', ' ') + .splitlines() + ), album.errors) + ) + self.ui.info.setText(text + '<hr />') + else: + self.tab_hide(tab) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index d27746be7..752b63977 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -137,6 +137,7 @@ class MainPanel(QtGui.QSplitter): ClusterItem.icon_dir = icontheme.lookup('folder', icontheme.ICON_SIZE_MENU) AlbumItem.icon_cd = icontheme.lookup('media-optical', icontheme.ICON_SIZE_MENU) AlbumItem.icon_cd_saved = icontheme.lookup('media-optical-saved', icontheme.ICON_SIZE_MENU) + AlbumItem.icon_error = icontheme.lookup('media-optical-error', icontheme.ICON_SIZE_MENU) TrackItem.icon_note = QtGui.QIcon(":/images/note.png") FileItem.icon_file = QtGui.QIcon(":/images/file.png") FileItem.icon_file_pending = QtGui.QIcon(":/images/file-pending.png") @@ -635,7 +636,12 @@ class AlbumItem(TreeItem): self.insertChildren(oldnum, items) for item in items: # Update after insertChildren so that setExpanded works item.update(update_album=False) - self.setIcon(0, AlbumItem.icon_cd_saved if album.is_complete() else AlbumItem.icon_cd) + if album.errors: + self.setIcon(0, AlbumItem.icon_error) + elif album.is_complete(): + self.setIcon(0, AlbumItem.icon_cd_saved) + else: + self.setIcon(0, AlbumItem.icon_cd) for i, column in enumerate(MainPanel.columns): self.setText(i, album.column(column[1])) if self.isSelected(): diff --git a/picard/ui/ui_infodialog.py b/picard/ui/ui_infodialog.py index c4118a611..9ac35a643 100644 --- a/picard/ui/ui_infodialog.py +++ b/picard/ui/ui_infodialog.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file 'ui/infodialog.ui' # -# Created: Sat Oct 6 19:08:31 2012 -# by: PyQt4 UI code generator 4.9.4 +# Created: Sat Jun 15 12:58:18 2013 +# by: PyQt4 UI code generator 4.9.3 # # WARNING! All changes made in this file will be lost! @@ -18,26 +18,37 @@ class Ui_InfoDialog(object): def setupUi(self, InfoDialog): InfoDialog.setObjectName(_fromUtf8("InfoDialog")) InfoDialog.resize(535, 436) - self.vboxlayout = QtGui.QVBoxLayout(InfoDialog) - self.vboxlayout.setObjectName(_fromUtf8("vboxlayout")) + self.verticalLayout = QtGui.QVBoxLayout(InfoDialog) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.tabWidget = QtGui.QTabWidget(InfoDialog) self.tabWidget.setObjectName(_fromUtf8("tabWidget")) self.info_tab = QtGui.QWidget() self.info_tab.setObjectName(_fromUtf8("info_tab")) - self.vboxlayout1 = QtGui.QVBoxLayout(self.info_tab) - self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1")) - self.info = QtGui.QLabel(self.info_tab) + self.vboxlayout = QtGui.QVBoxLayout(self.info_tab) + self.vboxlayout.setObjectName(_fromUtf8("vboxlayout")) + self.info_scroll = QtGui.QScrollArea(self.info_tab) + self.info_scroll.setWidgetResizable(True) + self.info_scroll.setObjectName(_fromUtf8("info_scroll")) + self.scrollAreaWidgetContents = QtGui.QWidget() + self.scrollAreaWidgetContents.setEnabled(True) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 493, 334)) + self.scrollAreaWidgetContents.setObjectName(_fromUtf8("scrollAreaWidgetContents")) + self.verticalLayoutLabel = QtGui.QVBoxLayout(self.scrollAreaWidgetContents) + self.verticalLayoutLabel.setObjectName(_fromUtf8("verticalLayoutLabel")) + self.info = QtGui.QLabel(self.scrollAreaWidgetContents) self.info.setText(_fromUtf8("")) self.info.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) self.info.setWordWrap(True) self.info.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByKeyboard|QtCore.Qt.TextSelectableByMouse) self.info.setObjectName(_fromUtf8("info")) - self.vboxlayout1.addWidget(self.info) + self.verticalLayoutLabel.addWidget(self.info) + self.info_scroll.setWidget(self.scrollAreaWidgetContents) + self.vboxlayout.addWidget(self.info_scroll) self.tabWidget.addTab(self.info_tab, _fromUtf8("")) self.artwork_tab = QtGui.QWidget() self.artwork_tab.setObjectName(_fromUtf8("artwork_tab")) - self.vboxlayout2 = QtGui.QVBoxLayout(self.artwork_tab) - self.vboxlayout2.setObjectName(_fromUtf8("vboxlayout2")) + self.vboxlayout1 = QtGui.QVBoxLayout(self.artwork_tab) + self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1")) self.artwork_list = QtGui.QListWidget(self.artwork_tab) self.artwork_list.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self.artwork_list.setIconSize(QtCore.QSize(170, 170)) @@ -48,13 +59,13 @@ class Ui_InfoDialog(object): self.artwork_list.setSpacing(10) self.artwork_list.setViewMode(QtGui.QListView.IconMode) self.artwork_list.setObjectName(_fromUtf8("artwork_list")) - self.vboxlayout2.addWidget(self.artwork_list) + self.vboxlayout1.addWidget(self.artwork_list) self.tabWidget.addTab(self.artwork_tab, _fromUtf8("")) - self.vboxlayout.addWidget(self.tabWidget) + self.verticalLayout.addWidget(self.tabWidget) self.buttonBox = QtGui.QDialogButtonBox(InfoDialog) self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) self.buttonBox.setObjectName(_fromUtf8("buttonBox")) - self.vboxlayout.addWidget(self.buttonBox) + self.verticalLayout.addWidget(self.buttonBox) self.retranslateUi(InfoDialog) self.tabWidget.setCurrentIndex(0) diff --git a/resources/images/16x16/media-optical-error.png b/resources/images/16x16/media-optical-error.png new file mode 100644 index 0000000000000000000000000000000000000000..5cbe26afef49a7c321dff7757d85f7bec0da1d97 GIT binary patch literal 964 zcmV;#13UbQP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy000McNliru-3AW~8Z<4`x2*sG15rst zK~y-)b(2d>TvY&uzjMyLm$@?+hJhg<lolwBk2HjWf=P{liDEY<ny9G~bf+zK<71&Q zF&f<%F)?b;#HLt$q^TsvQW1!0D=NZhgoWUuWXdx&p)(Ez?mW)D*9}e7=(qfnZ}aE> z1^?oeM;~tAaw1Q9N_7JDyKz!^-bH6?d+^mWXQ%l;`j2!Eob2tHTb-M`u#?GTJgt3C zYkwz`xxO;{>(t5Kp1J-b-2?wApKfg#UHfF{b1$FwuTD+|tD~dA%E(BtJ~0t&Boe{) z>T2L=?XM0Ejh=388NJ~tfVc0zZ{UHakBL}U*CXr0!?Lik0l6F!5P-5Qq*8c0J0g1M zP-8q+nyPiTTgNUho>zS*j<q)^<M`nhU%DqXH6{H*0i8~hPhBIQPN6q9k*0|XhmnR! z5Cp{9+H2+~#>0E>zUvp&)^^9sy(ga8E3a==>dj63TozeWgcXkyY1mI>UmbSM9^B<C zsBjqHb@8<(x^G`0@%w^p0G$;L`|GnyODNmM&*iXUWuy}c7AD34f;*poL6o+%kedDp zySfIgHSxOoIM6A9dMg~ZR49buI3NU}>S`7y#)CMbNe4kiqW>*FR8@t$vW%b4<GC)X zsK^BBjomVZvTe{BX<3-D7)l6#Sr?W8OvnQv&vo(hc`yuotpy+?kSw^aR<@0@Z9<im zc-!0D{_Jzomg;H<7F+xJtjydT$`0e@a`^c?ZYJXaNeNuMwzQNs90$X3h%`1*_%lUC zM~8Uu(s!YUCVwa|zV$YFW$+Y6G>V_iA}x#5+Nuj&Fo41F{x`dy_~=xHa2%v%ArIWl zb}~t7W|~4ejkGLETUro85_lew+FE8_e=SR~GpG_XKd*IERo+r<nm3gmJXj*6L@0%{ zEUfZ!Le({xu`+~G=&dbEo10lV_f=+o{<pJle0JtT1u%Ab@qF{j(!IrYI38<i3ZWD_ zn<emld?1h#qojmrQxnPIFY>b^=O&L|nmh_9bz{?CuC0H1U^7!U`_0$&WqWEZ({W@d z9><7A(Ms{h%Ca~1?mIilg@uow`u@kGyILUr_9p=tkM}%wI1&l>8p`MrQq&3|gzx)n mTI+93)BNE5_XhrP=f42Aa$yt(z~}S;0000<MNUMnLSTaJqQ3+H literal 0 HcmV?d00001 diff --git a/resources/images/22x22/media-optical-error.png b/resources/images/22x22/media-optical-error.png new file mode 100644 index 0000000000000000000000000000000000000000..fa423292ef411e5cc031368a1a8475d192bb8ad8 GIT binary patch literal 1426 zcmV;D1#S9?P)<h;3K|Lk000e1NJLTq000&M000&U1^@s6#I$TX00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy000McNliru-3AW~84*qWFev~41s_R7 zK~y-)wUup*TvZvzf9ITg@7%dN`_}2Q%f1-fozm?(NJuEJ4KKkO1;v#pn)qRPiP4BA zMop7eKB0m}f<QtO5(o=KBkmGwglcRnAyT5)CEIM-(kaN&;yCTH+jj2UJ9qBOIezGN zvxQ*bgXhcnaFXBi<p2Di|AGJ05&!G)p{+NL`yK6HOf35fBV`{TgwZGLBzd*H?(h2k zo_$Y!tZGx6M#i7KeDnPDwrzg>t+%q+_e~ndCXEvl`*m}sazxMm{QmICD>lD7wP|Gh zBXzuML+O#>o4@dl()R7$7$d@C$B1etF*?J_yI7uwdto2Bo*uF|VQ$Cv;MhwqJvCN2 zdiQFMdOvjZTi?Fpq8&f(^ylWpsec|II<bIGQ>101ayg97Kx=ffiBOV(uioZl)#|06 zTUuQI+xHg#@B#4DrjhZ1(a~+?2Oj8L`0I<*=MH1EL1!9-K()1@To-hPQWA6uMx$F1 zy`R5HHm0Y`*V^{+-~XpJ10c_Vmx^nCcJZS-yZlOp#IKX<=|MUUNP*5YS(=hHo0vF8 zhYd_Cge1bG30cEu<30CSO9TB6pV#qyW4DZ7_nEC<@4M=1t8t_PIzw6tp%f-f$qx-- z7YbPI1*Az4tpF)OC<WS()O~V27tlENjy+T=oql;{<{uV7SZMp&C%=5F(^^;nVGtNh znxG`)J~7CdL$g$#eHM*j`1)%Z+`5G<Y>*}?X}XB?3J531@C`T0S6=wz4}eF`O5r$L z3IhY^W{9#Zq)9Ph9m|5?$YD<Ge!&cF-1x7HMnAPXm_MG?UOs@mwwKm>3kV?)mW_^M zilq`!k_@b9oMgI9Iu6otkhVgJ1R0${r2(%!^E7J*hyHZQ#KiE(6HnZ12f@~(fBp+j z{}8I98>@H$s?ZKXVtXD*5?d=^A<p_)SQgTfNF5`RCZsKjjzhgt`Ng@R9IRFUX29U& zF;)&ECB_(x)=0VH1uOttS&>PSymDQPS3q~HMMf!F+4Np>HH`x^k4;>D*~I|Ejo_ia zyKWa*=#%(9MktUHp%k(e;n+^L0?u33yJ4-i!R_oodj(X-8nlKiis}8*t+=I;k<#Sv zS{S1T@BWS|UUms#<q*=gNW%ajByL|H^=cI_pKq>!SzLPb&B@7MefoRf6*6j}*7P6? z9V9_W+z9bT%9QTCR0<)bFe(1q?;zs{)z*cshv+QEE*3dDIf>^uudaZ1Og;DTz|!I` zN5{u)t587LHfazbj6`OF;NMk})AeQFrr0@!?SKsGAQWz2A3BPtzCLS4&-k~Ux6<GA zyhdYo_OtzCV*<b+z<?0~9jC~A9_4u`$AK(^G)0FEa=ra{>(=wePwyoT!d<sK|NO*x z;NL7POyAXNiLe=7SG;D++2|7zj3Eocvuo<u2n=K?CQWhs2l3Xe<L#Y0S(=?ab<^H` zR{;hvYB@SxApiK{;>=g-_43Krrb}zDyb`;s3mqqDogplR$~g!ru{%2`l!nRDgd-0= zNcGTc^pm;6TaHIj3rJUk<??M$RI4v;=<bpS|NhEm9W{kjXvgmE#x537ot=n|cETj# z;J&@=d2Wh&vpIG9p1pU@H=A`JSO)9WK`)nw*$!B3uG_ZbM?c)HwO-$723D)p5&*ee z4lnO%+p?>_e)^fQ*XQR?0jGfoXe~>jKkUzwtJ2RZm&;w{a=G7i-L_h-R@=XS|4HBX gqjTMz({(lAzXjDy>~hkWs{jB107*qoM6N<$g315B(*OVf literal 0 HcmV?d00001 diff --git a/resources/picard.qrc b/resources/picard.qrc index 62fb48727..e2efe8ef9 100644 --- a/resources/picard.qrc +++ b/resources/picard.qrc @@ -8,6 +8,7 @@ <file>images/16x16/edit-cut.png</file> <file>images/16x16/edit-paste.png</file> <file>images/16x16/folder.png</file> + <file>images/16x16/media-optical-error.png</file> <file>images/16x16/media-optical-saved.png</file> <file>images/16x16/media-optical.png</file> <file>images/16x16/picard.png</file> @@ -18,6 +19,7 @@ <file>images/22x22/folder.png</file> <file>images/22x22/list-remove.png</file> <file>images/22x22/lookup-musicbrainz.png</file> + <file>images/22x22/media-optical-error.png</file> <file>images/22x22/media-optical-saved.png</file> <file>images/22x22/media-optical.png</file> <file>images/22x22/picard-analyze.png</file> diff --git a/ui/infodialog.ui b/ui/infodialog.ui index 6c647c229..44cf74b55 100644 --- a/ui/infodialog.ui +++ b/ui/infodialog.ui @@ -10,7 +10,7 @@ <height>436</height> </rect> </property> - <layout class="QVBoxLayout"> + <layout class="QVBoxLayout" name="verticalLayout"> <item> <widget class="QTabWidget" name="tabWidget"> <property name="currentIndex"> @@ -22,19 +22,41 @@ </attribute> <layout class="QVBoxLayout"> <item> - <widget class="QLabel" name="info"> - <property name="text"> - <string/> - </property> - <property name="alignment"> - <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> - </property> - <property name="wordWrap"> + <widget class="QScrollArea" name="info_scroll"> + <property name="widgetResizable"> <bool>true</bool> </property> - <property name="textInteractionFlags"> - <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> - </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>493</width> + <height>334</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayoutLabel"> + <item> + <widget class="QLabel" name="info"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </widget> </widget> </item> </layout> From 86ed552b212b1543aada52a6faaee402cef5385a Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Mon, 1 Jul 2013 02:05:17 +0200 Subject: [PATCH 48/75] Make modified releases a bit more obvious using a specific icon. --- picard/album.py | 8 + picard/resources.py | 378 ++++++++++++++++-- picard/ui/itemviews.py | 13 +- .../images/16x16/media-optical-modified.png | Bin 0 -> 946 bytes .../16x16/media-optical-saved-modified.png | Bin 0 -> 952 bytes .../images/22x22/media-optical-modified.png | Bin 0 -> 1420 bytes .../22x22/media-optical-saved-modified.png | Bin 0 -> 1392 bytes resources/picard.qrc | 4 + 8 files changed, 371 insertions(+), 32 deletions(-) create mode 100644 resources/images/16x16/media-optical-modified.png create mode 100644 resources/images/16x16/media-optical-saved-modified.png create mode 100644 resources/images/22x22/media-optical-modified.png create mode 100644 resources/images/22x22/media-optical-saved-modified.png diff --git a/picard/album.py b/picard/album.py index 7b4d283f1..d7afd67d5 100644 --- a/picard/album.py +++ b/picard/album.py @@ -421,6 +421,14 @@ class Album(DataObject, Item): else: return True + def is_modified(self): + if self.tracks: + for track in self.tracks: + for file in track.linked_files: + if not file.is_saved(): + return True + return False + def get_num_unsaved_files(self): count = 0 for track in self.tracks: diff --git a/picard/resources.py b/picard/resources.py index 301b2212e..c916bd5d3 100644 --- a/picard/resources.py +++ b/picard/resources.py @@ -2,7 +2,7 @@ # Resource object code # -# Created: lun. juil. 1 02:03:25 2013 +# Created: lun. juil. 1 02:04:46 2013 # by: The Resource Compiler for PyQt (Qt v4.8.2) # # WARNING! All changes made in this file will be lost! @@ -1826,6 +1826,68 @@ qt_resource_data = "\ \x68\x79\xd9\xe1\xb7\xf7\xef\xca\x9b\xab\x2c\xab\x03\xc2\x87\xb4\ \xca\xa7\xb0\xe2\xf8\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ \x82\ +\x00\x00\x03\xb2\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\ +\x0d\xd7\x01\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xdd\x06\x0f\x0d\x23\x21\x4f\x4f\xb4\xcb\x00\x00\x03\x3f\x49\x44\ +\x41\x54\x38\xcb\x5d\x93\xdd\x4f\x5b\x75\x18\xc7\x3f\xbf\xdf\x39\ +\x3d\x9e\xd3\xd3\x42\x5f\x06\x05\x47\x69\x0b\x59\x64\x60\xe2\x5b\ +\x04\x83\x59\xc2\x96\x78\x61\x62\xd8\xdc\x82\x66\xbc\x28\x1a\xbd\ +\x20\xcc\x5b\xff\x01\x6f\x66\xf4\xce\x31\xf0\x72\x89\xe2\xa2\xde\ +\xa8\x31\xdc\xe8\x86\xee\x45\x69\xc1\x10\xb7\x69\x02\xab\xa4\x44\ +\x0a\x85\xc2\x66\x05\x7a\xda\x9e\x9e\xe3\x85\xeb\x42\xf6\x5c\x3e\ +\x79\x3e\xdf\xe7\xf9\xe6\xc9\x57\xf0\x48\x8d\x9f\x1b\xeb\x01\x46\ +\x81\x3e\x20\xf1\xa0\xbd\x02\xcc\xaa\xab\xf5\xdf\x7a\x66\xa3\xb7\ +\x3e\x2e\x8c\xff\x5d\x9b\x57\x1f\x81\x27\xc3\xa1\x70\xef\xc8\xc8\ +\x9b\x3b\xcd\xcd\xcd\x8f\x1b\x86\xa1\x01\x14\x8b\xc5\x96\x6c\x76\ +\xed\xe9\x4b\xef\x7d\x73\xb2\xdc\x56\x58\x67\x91\xe7\x6a\x8c\x38\ +\x00\xcf\x0c\x0e\x0e\xdd\xef\xe9\x7e\xe1\xf5\xa5\xa5\x25\x61\x59\ +\x16\x8e\xe3\x60\xe5\x6c\x7c\x01\x1f\x8d\x87\x0f\xf1\xc5\xe9\x2b\ +\xec\xde\xdb\x2f\x6e\x9d\x58\xbc\x61\x7e\xd9\xf9\x96\x14\x8a\x57\ +\xd4\x36\x0f\x0e\x0e\x07\x8f\x76\x1c\x1d\x58\x5c\x5c\x94\xae\xeb\ +\xe2\xf1\x78\xd0\x34\x8d\xed\xdf\xf6\x48\x7e\xf0\x17\x52\x55\x28\ +\xef\x55\xfe\x3f\x5b\x97\xd5\x8a\x55\xc5\xad\xba\x03\x72\xfc\xdc\ +\x58\x4f\x38\x14\xee\xed\xe9\xee\x79\x2d\x93\xc9\x48\x8f\xc7\x83\ +\x65\x15\xd9\xde\xc9\xb3\x91\xcb\x42\x7c\x9f\x63\x9f\x1c\x41\x0f\ +\xa9\xe0\x02\x2e\xf8\xc2\x5e\x45\x9e\xd9\xc8\x14\x46\x7e\xcf\x4a\ +\x60\x74\x68\x78\x44\xe4\x72\x39\x21\xa5\x44\x4a\x81\xcf\xe7\xa7\ +\xb1\x21\x42\x22\xde\x4e\x7b\xdb\x11\x9e\xe8\x69\x27\xd0\xe5\x7d\ +\xe8\x38\xf2\x6c\x80\xfe\x77\x5f\xd6\x81\x51\x15\xe8\x6b\x39\xdc\ +\x92\xc8\xe7\xf3\x68\x9a\x46\xc5\xae\xe0\x33\xfd\xa4\xd3\x69\xe6\ +\x92\xbf\x00\xb8\xaf\x1c\x7b\x55\xec\xa5\x2b\x74\xbc\xdd\x88\x3f\ +\xe4\x67\xe9\xeb\x2c\xcf\x58\xed\x0d\x40\x9f\x0a\x24\x74\x5d\xd7\ +\x34\x4d\xc3\x71\x1c\x84\x80\x60\x30\xc8\x5c\xf2\x57\xd7\x6b\x98\ +\x4f\xfd\xbb\x5b\xc8\xce\x7c\x37\xb3\xf9\xce\xf9\x31\xf9\x67\x61\ +\x81\xb6\x44\x03\xc1\x76\x13\x6c\xa1\x02\x09\x59\xfb\x86\xa6\x69\ +\x68\x9a\x86\xaa\xaa\x98\xa6\x89\x94\xc2\xb9\x77\x7f\x67\xd5\xb6\ +\x6d\xbb\x1a\xb0\xca\xf1\xde\x26\x2a\x95\x0a\xb6\x6d\x13\xea\x32\ +\x09\x77\xf8\x05\x20\x24\xb0\x52\x2a\x95\xec\x9a\x40\x7d\x7d\x80\ +\x52\xa9\xc4\x99\xd3\x03\x52\xd3\xb4\x0d\x55\x55\xd3\x27\xfb\x4f\ +\x69\x99\x4c\x06\x4d\x7b\xec\xa1\xc8\xee\xee\x6e\x15\x58\x51\x81\ +\xd9\xf5\xf5\xf5\x70\x6b\x6b\x6b\x03\x40\x53\xa4\x89\xcd\xad\x1c\ +\x9d\x9d\x9d\xe2\xc3\xf3\x1f\xe9\x80\xbe\xba\xba\xca\x95\xab\x3f\ +\x90\x88\xb7\x51\xa9\x54\xf0\x7a\xbd\xe4\x36\x73\x45\xe0\xaa\xd2\ +\xdd\xfd\x7c\xfe\x6e\x7a\xb9\xff\x78\xdf\x89\xa0\xa2\x28\xa8\xaa\ +\x8a\xa1\x1b\x6c\xe5\xb7\x58\x58\x48\x71\xfb\xf6\x2d\xf2\xdb\x79\ +\x1a\x1b\x22\x08\x21\x91\x52\x12\x8d\x46\xb9\x7c\x79\xba\x50\xb4\ +\x8a\xef\x2b\xa9\xe4\xfc\x5a\xd7\x93\x9d\x1d\x81\x40\xa0\xb9\x35\ +\xda\x5a\x27\x84\x40\x4a\x05\x55\xf5\xe0\xf3\xf9\x09\x06\x43\x98\ +\xa6\x0f\x29\x25\x96\x65\x11\x8f\xc7\x49\xa5\x92\xff\xdc\xf9\xe3\ +\xce\x57\x17\x27\xa6\x3e\x55\x00\x52\xc9\xf9\xef\x0d\xaf\xfe\x92\ +\x69\xfa\x0e\xc5\x62\x31\x5d\x4a\x49\xb9\x5c\xc6\x71\x1c\x5c\xd7\ +\x05\xc0\x30\x0c\x62\xb1\x18\x37\x6f\xde\x28\xff\xf4\xf3\xec\xb5\ +\x8b\x13\x53\x67\x01\x45\xa9\x65\x21\x95\x9c\xff\xdc\xe7\xf7\xc6\ +\xaf\x5f\xbf\x96\x88\x44\x9a\x34\x5d\xd7\x65\x5d\x5d\x1d\x86\x61\ +\x00\xb0\x96\x5d\xab\x4e\x4f\x7f\xb6\xbf\x7c\x77\xf9\xd2\xc4\x85\ +\xc9\xb3\x0f\x30\x57\x1c\x08\xa3\x00\xd4\xa1\xe1\xc1\x17\x4d\xd3\ +\xfb\x86\xaa\xa8\xc7\x85\x14\x51\x21\x84\x70\x1c\x67\xcd\xb6\xed\ +\x1f\x3d\x1e\xcf\xd4\xc4\x85\xc9\xb9\x83\x09\xfe\x0f\x0e\x6f\x35\ +\xe4\xf1\x24\xb6\x8b\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ +\x82\ \x00\x00\x01\xb8\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -2022,6 +2084,68 @@ qt_resource_data = "\ \x3f\x5f\xda\x68\xfd\xbf\x00\x00\x38\x7c\x45\xbd\x0f\x00\xc3\x37\ \xc5\xd9\x7f\xf5\xfc\x02\x73\x4a\x12\x2a\x86\x68\x1c\xcb\x00\x00\ \x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x03\xb8\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xff\x61\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\ +\x0d\xd7\x01\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xdd\x06\x0f\x0d\x22\x3a\xdc\x31\x4c\x66\x00\x00\x03\x45\x49\x44\ +\x41\x54\x38\xcb\x5d\x93\x5f\x68\x5b\x75\x14\xc7\x3f\xbf\xdf\xbd\ +\xf7\xd7\x7b\x93\x26\x25\xcd\xda\x35\x69\xec\x7a\xd3\x0d\x3b\x58\ +\x99\x6c\x93\xba\x96\x41\x05\xc5\x17\x57\x95\x6e\x8e\xb5\xb5\xab\ +\x08\x7b\x51\x2a\xbe\x6c\x82\x82\x74\x0f\x32\x8a\x0c\xec\xd0\x39\ +\xd8\xcb\x90\x39\x65\xe0\xc3\x70\xee\xa1\x6a\xbb\x4a\xc1\x20\x53\ +\x99\x52\x85\x26\xad\xdd\xba\xd6\xd6\xad\x49\x4b\xfe\xdf\x9b\xc4\ +\x07\xdb\x51\x76\x1e\x0f\xe7\xc3\x39\x5f\x0e\x1f\xc1\x63\x35\x32\ +\x64\xb5\x03\x83\x40\x17\x60\x6f\xb4\xe7\x80\x89\xe2\x7c\xdb\xf5\ +\xd4\xf8\x89\xdf\x3f\x5a\x7f\x73\x61\x73\x5e\x7f\x0c\xbe\x50\x5b\ +\x1b\xec\x18\x18\x18\x58\x35\xb6\x1f\x0b\x63\xee\x52\x00\xe4\x67\ +\x22\x85\xa5\xab\x4f\x8d\xbe\x75\xf7\xa5\xea\x96\xdf\x96\xf8\x95\ +\xfd\x9b\x8c\xd8\x02\xdf\xec\xed\xed\x4b\x35\x1e\x38\x77\x2c\x9b\ +\x38\x23\xf2\xf9\x3c\xe5\x72\x85\xf4\xb2\x07\x5f\x4d\x15\xde\xf0\ +\xd3\x5c\x78\x25\x4d\x36\xb9\x9a\xd3\x9f\x1b\x9e\x4a\x7e\x79\xe6\ +\x75\x29\x34\x8f\xd8\xdc\xdc\xdb\xdb\x17\x68\x7c\xb2\xe7\xe8\xec\ +\x9d\x2b\xb2\x52\xa9\x60\x18\x06\x4a\x29\x16\x6e\xd7\x72\x63\x78\ +\x07\x9a\x2e\x29\x64\x36\xce\x36\xcb\x25\x27\x5f\xa1\x5c\x12\x47\ +\xf5\x91\x21\xab\xbd\xb6\x36\xd8\xd1\x78\xe0\x5c\x5b\x72\xfa\x7d\ +\x61\x18\x8a\xb5\xb5\x14\xeb\xeb\xeb\x08\x21\xb0\x9a\x57\x79\xf5\ +\x93\x1c\x37\x87\x77\x92\x4f\xff\x9f\xc8\x1b\xf4\x69\xd6\xc1\x8f\ +\x67\xb3\xe6\x1f\x8b\x12\x18\xec\xef\xef\x17\xfc\x7b\x59\x48\x29\ +\x11\x02\x7c\x3e\x3f\xf5\xf5\xdb\xb1\xed\x28\x2d\x2d\x51\x76\xb7\ +\x47\x68\xd8\x93\x7e\x94\xb8\x69\x5f\x91\x23\x27\xf7\x9b\xc0\xa0\ +\x0e\x74\x55\x85\x5e\xb3\x59\xfd\x1a\xa5\x14\xae\xeb\x52\x5d\xed\ +\x23\x91\x48\x10\x8b\xc5\x00\x2a\x2f\x1c\xea\x13\x0f\xe3\x7b\x39\ +\xf8\xc6\xdf\xf8\x83\x26\xb7\xaf\x05\x10\xb9\xd6\x3a\xa0\x4b\x07\ +\x6c\x4c\x5b\xa1\x1a\x30\x2b\x0e\x42\x48\x02\x81\x00\xb1\x58\xac\ +\x62\x59\xe6\xde\x74\x3a\xb3\xf8\xed\xf5\x1f\x56\x5e\x3e\x7b\x52\ +\xae\xa5\xbf\x21\x1a\xb5\xa9\x6f\x71\x70\xdd\xa8\x0e\xd8\xf2\xd1\ +\x37\x8c\x06\x84\x51\x87\xae\xeb\x68\x9e\x16\xa4\x14\xe5\x54\x2a\ +\x75\xd7\x75\x1d\x57\x0b\x2c\x15\xed\x8e\x6d\x38\x8e\x83\xeb\xba\ +\x84\xf7\xa4\x09\xef\xce\x09\x40\x48\x60\x8e\xc2\x3d\x17\x15\x06\ +\xa3\x81\x9a\x1a\x3f\x14\x16\xe8\xe9\x39\x22\x95\x52\xff\xe8\xba\ +\x91\xe8\xee\x3e\xac\x8a\x0b\xa3\x28\x65\xe0\x38\x2e\xae\xeb\x52\ +\xcc\xcc\x97\x80\x39\x09\x4c\x94\x56\x3e\x4f\x52\x65\x83\x19\xa5\ +\xba\xfe\x10\xd9\xec\x1a\xa1\xd6\x13\xe2\xed\x0f\x67\xcc\x77\xce\ +\xc6\x83\x91\x48\x44\x4e\x4c\xdc\xc2\xb2\x2c\x1c\xa7\x88\xae\xeb\ +\x2c\x2f\xaf\xe4\x80\x71\x31\x32\x64\xb5\xd7\xd5\xd5\x5d\x1d\x7c\ +\x2f\x6e\x53\x4a\x83\x9b\x82\x42\x02\x67\x75\x9c\x78\x7c\x86\x4c\ +\x26\x83\xae\xeb\x78\x3c\x1e\xa4\x94\x28\xa5\x68\xda\xd9\xc9\xa5\ +\x4f\x3f\x78\x90\x4c\x26\x5f\xd4\xc6\x62\xee\xfd\xce\xb6\x62\xeb\ +\x8e\xc0\x7c\xc8\x1f\xe9\xf6\x23\x74\x90\x1e\x34\xbd\x8a\xa0\x2f\ +\x4f\x20\x10\xc0\xeb\xf5\x22\xa5\x24\x9f\xcf\xd3\xdc\x6c\xf3\xcb\ +\xcf\xe3\x6b\xd3\xd3\x7f\x5e\x3b\x7d\x3e\x77\x51\x03\x18\x8b\xb9\ +\x37\x42\xde\xbf\x9e\x0f\xfb\x66\xb7\x05\x9a\x0e\x9b\x48\x05\xce\ +\x12\xa2\x9c\x45\x50\x00\x04\x96\x65\x12\x6a\xda\xc7\x4f\x53\xdf\ +\x15\x27\x27\x27\x7f\x3c\x7d\x3e\x77\x1c\xd0\xb4\x4d\x17\xc6\x62\ +\xee\x95\x46\xdf\x4c\xf3\x9d\xa9\x8b\xf6\xae\xd0\x03\x65\x9a\x96\ +\xc4\xf7\x0c\xd2\x6c\x42\xf1\x90\xc5\xc5\x7b\xa5\xaf\xbe\xb8\x94\ +\x8d\xc7\x67\x2e\x9f\x1a\xcd\x1d\xdf\xc0\x2a\x62\x8b\x8c\x02\xd0\ +\xdf\x1d\xa0\xd3\xe7\xd5\x07\x34\x4d\x7b\x56\x4a\xf9\x84\x10\x42\ +\x94\xcb\x95\xfb\xae\xeb\x7c\x6f\x18\xc6\x67\xa7\x46\x73\xb1\xad\ +\x06\xff\x07\xd9\xf3\x34\xb2\x81\x3a\x3d\x2c\x00\x00\x00\x00\x49\ +\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x03\x8f\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -2818,6 +2942,97 @@ qt_resource_data = "\ \xd1\xa4\xf0\xe7\x6f\x64\xe7\x61\x16\x79\xf3\xdf\xc4\x6f\x97\x71\ \x39\x10\x00\x75\x1a\x77\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ \x60\x82\ +\x00\x00\x05\x8c\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x16\x00\x00\x00\x16\x08\x06\x00\x00\x00\xc4\xb4\x6c\x3b\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\ +\x0d\xd7\x01\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xdd\x06\x0f\x0d\x20\x0b\xbf\xd9\x2e\xde\x00\x00\x05\x19\x49\x44\ +\x41\x54\x38\xcb\xa5\x95\x4b\x6c\x9c\x57\x19\x86\x9f\x73\xf9\xff\ +\x7f\xfe\xcb\x8c\xa7\x8e\xed\xda\x0d\x8a\x90\x9d\x38\xa9\x3b\x69\ +\x69\x4d\x1b\x1b\xa2\x06\x15\x82\x7b\x51\x15\x56\x61\x11\x10\x34\ +\x2c\xd2\xa8\x69\x17\x80\x9a\x05\x12\x0a\x15\x05\xa1\x52\x01\x51\ +\x10\x02\x56\xb4\xd9\x20\x45\xd0\x92\x52\x4a\x16\x58\xd4\x71\x71\ +\x23\x42\x8b\x94\x0a\xd5\xa9\x63\x5c\x5a\xc7\xf1\x25\x33\x99\x78\ +\x3c\xf3\xdf\xce\x61\x11\xdb\x32\x11\x97\x05\x67\x73\xa4\x23\x7d\ +\x8f\xbe\xf3\xbe\xef\xf9\x0e\xfc\x9f\xeb\x1b\xa5\x9f\x75\x7e\xad\ +\x74\xa2\xe7\xe6\x73\xfd\xdf\x8a\x9e\x38\x72\xf8\x4b\xc0\xa3\xc0\ +\x2e\xa0\x1b\x10\xc0\x15\xe0\x1c\xf0\xd2\x8f\x4f\xfc\xe4\x17\x99\ +\xb2\x23\x05\xeb\x96\x81\x13\xff\x13\xbc\x0a\xfc\xd6\x7d\xbb\x86\ +\x37\x8d\xec\xfd\x6c\x50\x6a\x2b\x4b\xc7\xd1\x80\x20\x4d\xe3\xdb\ +\x6a\xd5\xea\xbe\x89\x89\x89\x11\xe0\x59\xf9\xeb\xa4\xc6\x8a\xdb\ +\xb8\x19\x2c\xfe\x0d\xf4\xf8\x7d\xbb\x86\x0f\xec\x7b\x74\xdf\x2d\ +\x16\xc4\xc2\xe2\x22\xb5\x5a\x1d\x6b\x0d\x9e\xeb\x50\x9f\x6c\x12\ +\x68\x9f\xa8\x14\x91\xb4\x32\x7e\xf9\x85\x51\x10\x02\x91\x99\x87\ +\x8c\x10\x2b\x16\xa3\x05\x32\x11\x37\x43\x1f\x7c\xf0\xe1\xc7\x46\ +\x46\x1e\x8a\x2e\x4d\x4f\x33\x37\x77\x85\x2c\xb7\x28\xad\xf1\x3c\ +\x8f\xa0\xe0\x52\x7d\xa7\xce\x1b\xdf\x7c\x97\xe6\x52\x0a\x80\x05\ +\xb0\x16\x21\x6e\xa0\xac\xb5\x8b\xc6\x98\xfd\x6a\xe3\xf5\xfb\x7a\ +\xfb\x9e\x3e\x70\xe0\x8b\x6d\x17\x2e\xbc\xc3\x3f\x3e\xf8\x90\xdc\ +\x40\x6e\xc1\x0a\x89\xeb\x16\x70\x5c\x4d\xd0\xa9\xd8\xb9\xbf\x87\ +\xf8\x5a\xc6\xe2\xdf\x96\x11\x42\x20\x84\xc0\x5a\x8b\x2d\xb5\xde\ +\xa4\xa5\xee\xfd\xee\xb5\xc3\xef\xca\x0d\x0d\x3f\xfb\xe5\xc7\xbe\ +\x52\xbe\x7c\xf9\x32\xcd\x56\x93\x52\xa9\x84\xd6\x1a\x84\x20\x37\ +\x96\xd4\x18\x92\x24\xc5\x64\x19\x42\x25\x0c\x3d\xd9\x43\xd4\xed\ +\xad\x17\xfb\xed\x2e\x8d\x87\xa7\x07\xbe\x53\x7b\xbc\x0e\x20\xd7\ +\xba\x7d\xe0\x81\x4f\x77\x16\x3c\x4f\xd4\xaa\x35\x1c\xad\x70\x1d\ +\x85\xef\xbb\x68\x29\xe8\xe8\xd8\x44\xb1\x18\x11\x46\x11\xae\xab\ +\xb0\xd9\x0a\xd7\x66\xea\x2c\xcf\x27\x48\x25\x51\x9e\xa0\x79\x35\ +\xa1\xaf\xb4\x23\x7a\xf2\xa9\x23\x4f\xad\xa7\x42\x08\xf1\xf9\xa1\ +\xa1\x61\xb7\x5e\xaf\x23\x84\x45\x0a\x83\xc9\x33\x1c\x01\xb7\x76\ +\xde\xc2\xfb\xef\x4f\x33\xf6\xc7\x51\x0c\x86\x8f\xdf\x7d\x27\x9f\ +\xb8\x67\x0b\x6f\xbd\xb6\x48\xfb\x47\x0b\xec\x79\x6e\x0b\x5e\xb1\ +\xc8\xe8\xd7\x2f\xe2\x4d\x6b\x11\xf6\x06\x5f\x05\x8e\x4b\x00\xa5\ +\xd4\xee\x8e\x4d\x1d\xa4\x69\x8c\xa3\x24\x9a\x14\x9b\x5c\xc7\x77\ +\x32\xe6\x67\x67\xf8\xeb\x5b\xe7\x6d\x6f\x6f\xdf\xa5\xdb\xb7\x0f\ +\xbc\x7a\x65\xbe\x9a\xff\xe5\xc2\x87\xb4\xf5\x94\xb8\xff\xfb\xed\ +\x04\xed\x2e\x5e\xe4\xf2\xc8\xcf\x07\xd9\x72\x77\x37\xf1\x4a\xba\ +\x79\x5d\x8a\x2c\xcb\x43\xad\x35\x5a\x29\x5c\x6d\x71\x55\x0b\x65\ +\x6b\xb8\xd4\x79\xed\xcc\xef\xe9\xee\xee\xf9\xdd\xa1\x43\x87\xfa\ +\x0e\x1e\x3c\xf8\x48\x9e\x9b\xbd\x6f\x9c\x7b\x9b\xfb\x8f\xec\xc4\ +\x6f\x2b\x13\x44\x65\x0a\x05\x1f\xe9\x28\x86\x1e\xbf\x83\xd4\xc6\ +\x7a\x1d\x2c\xc4\x8d\x3c\x2b\xa5\xd0\x0a\xb4\x68\x22\x4d\x03\x4c\ +\x4c\x14\x15\x99\x9d\x9d\xfd\xde\x9a\x49\xef\x4d\x4d\x8e\x81\xb5\ +\x68\x1f\x2b\x3d\x0c\xea\x46\x72\x8c\x45\x4a\xf9\xaf\x2f\x4f\x6b\ +\xa7\x95\xe7\xb9\xef\x3a\x0e\xb9\xeb\x93\xb9\xed\x38\x05\xc8\xac\ +\xe6\x8e\xdb\xfb\x99\x9a\x9e\xfd\xe9\x13\x47\x0e\xef\x04\x88\xa2\ +\xe2\x0f\x3f\xb5\x67\x48\xc4\xe9\x0a\x8d\x66\x8c\x61\x05\x84\x44\ +\x4a\x49\x31\x89\x71\x1d\x37\x5f\x07\x7b\xae\xfb\x41\xb5\x5a\xdd\ +\x16\x85\x3e\x59\x1a\x90\x79\x65\xdc\x50\x91\xa4\x39\x1f\x1b\xec\ +\xa5\xbd\x6b\x76\x87\xb1\x26\xb6\x58\xf3\xc9\xdd\xc3\x6a\x47\xff\ +\x66\x16\x16\xfe\x8e\xd6\x8a\x56\x1c\x23\x84\xa4\x5c\x2e\xb3\xb8\ +\xb8\x88\x1f\x04\x4d\x00\x05\x30\x38\x78\x8f\x14\x52\x8e\x0c\x0c\ +\x0c\x88\xdc\x58\x84\x72\x70\x3d\x1f\xa1\x7d\x1a\x89\x21\x68\xeb\ +\xa2\xbf\x32\x28\xb6\x57\x06\x65\x54\x2c\x8b\xea\xd5\x2b\x98\x34\ +\xc1\x71\x43\x92\xc4\x90\xe7\x39\x5d\x5d\x5d\x8c\x8d\xbd\x4e\x63\ +\xa5\x71\x76\xf4\x0f\xa3\x2f\x68\x80\xe3\xc7\x4f\xfc\x48\x3b\xfa\ +\xb9\xbd\x9f\xd9\xeb\x78\x05\x1f\xa1\x34\x71\x1c\x23\x6d\x8a\x36\ +\x29\xf3\x57\xaf\xb1\xdc\x68\x21\xa5\x44\xd8\x8c\xc0\xb5\x84\xae\ +\xc4\xda\x65\x84\x70\x29\x97\xcb\xa4\x69\xc2\xd4\xa5\x29\x2b\x85\ +\xda\xbf\x6e\x1e\x80\x31\xf6\xe8\x8b\x27\x5f\xb0\x52\x69\xb4\x76\ +\xd0\x8e\x87\xe3\x78\xab\x33\xc2\xa7\xdc\xd6\x46\xa9\x54\x24\x0a\ +\x43\xc0\x21\x4e\x04\x8d\x46\x93\x30\x0c\xe9\xe8\xe8\xe0\x37\xa7\ +\x5f\x46\x4a\xf9\xd2\xb1\x63\xc7\x96\xd6\xa5\x00\x98\x98\x78\xf3\ +\xfc\x5d\x77\xdd\x29\x56\x9a\x8d\x3d\xdb\xfb\x77\xac\x47\xc5\x02\ +\x71\x12\x83\x00\x47\x49\xa4\xb0\x60\x32\x4c\x6e\x28\x97\x3b\xe8\ +\xec\xec\xe2\xcc\x99\x33\xcc\xcc\xcc\x54\x9f\x79\xe6\xdb\xc3\xab\ +\x25\x56\xad\x8e\xce\x02\xe0\x9f\x3d\x3b\xfe\xf6\xd6\xad\x7d\x95\ +\x8b\x53\x17\xb7\xf6\x6f\xdb\x46\x18\x84\xe4\x79\x86\x31\x39\x4a\ +\x0a\x5c\x47\xa1\xb0\x44\x81\x47\x77\xf7\xad\x64\x59\xc6\x2b\xaf\ +\x9c\x66\x6a\xea\xbd\xd6\xa9\x53\xbf\xda\xbd\xb4\xb4\x14\x03\xd9\ +\x46\xb0\x5c\xdd\x9d\xb1\xd7\xc7\x46\x3f\xb2\x79\xb3\xfc\xf3\xf9\ +\x73\xf7\xa6\x69\x2a\xfc\x42\x81\x28\x0c\x28\x46\x21\x61\xa1\x80\ +\xef\x39\xc4\x71\x8b\x3f\x4d\x8c\xf3\xdb\xd3\xa7\x59\x5e\x5e\x3e\ +\xf5\xfc\xf3\x3f\x38\xb8\xb0\xb0\x50\x07\x1a\xab\x60\xb3\x71\x1e\ +\xcb\xd5\xf8\x15\x01\x1d\x04\x41\x78\xf4\xe8\xd3\xaf\x1a\x93\xf7\ +\x36\x56\x1a\x3a\x8e\x63\x01\xdc\xd0\xdc\x0f\x8c\x76\xdc\xd9\x93\ +\x2f\x9e\xfc\xdc\xe4\xe4\x64\x15\xa8\x01\x2d\x20\x5e\x95\xc2\x88\ +\xff\xf0\xdd\xad\x99\xaa\x00\x5d\xa9\x54\xca\x95\x4a\xe5\xb6\x42\ +\xa1\x10\xce\xcd\xcd\xcd\x8d\x8f\x8f\xcf\x5f\xbf\x7e\xbd\xb5\xd6\ +\xdd\x9a\xae\x1b\x01\xff\x04\x9d\x04\x0e\x02\x12\xbc\x82\x6c\x00\ +\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x02\x00\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -3126,6 +3341,95 @@ qt_resource_data = "\ \xe7\x71\xf0\x78\xea\x85\x02\x74\xdc\xff\xff\x35\xfe\x01\xc0\x62\ \xff\xd0\xdd\x98\xb2\xab\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ \x60\x82\ +\x00\x00\x05\x70\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x16\x00\x00\x00\x16\x08\x06\x00\x00\x00\xc4\xb4\x6c\x3b\ +\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\ +\x0d\xd7\x01\x42\x28\x9b\x78\x00\x00\x00\x07\x74\x49\x4d\x45\x07\ +\xdd\x06\x0f\x0d\x21\x24\x0d\x13\x22\xc6\x00\x00\x04\xfd\x49\x44\ +\x41\x54\x38\xcb\xa5\x95\x5b\x6c\x54\x45\x18\xc7\x7f\x33\x73\xce\ +\xd9\xdd\xb3\x67\x2f\x85\xb6\x94\xaa\x41\x5b\x8b\x85\x6a\x90\xaa\ +\xad\x4a\x83\xa8\x41\x82\x06\xf5\xc1\x1b\xd1\x78\x7b\x42\x1f\x30\ +\x62\xd4\x18\xa2\x89\x31\x60\x7c\xd0\x28\xe1\x45\x13\x8d\xfa\x42\ +\xa2\xc6\x20\x16\x90\x80\x20\x82\x56\x8c\x8a\x26\x90\x80\x01\x01\ +\xa5\xa5\x2d\x65\xbb\xbd\xec\xe5\xec\x39\x67\xc6\x07\xb6\x9b\x4a\ +\xbc\x3c\x38\x2f\x93\x99\x64\x7e\xf3\xcd\xf7\xff\x7f\xdf\xc0\xff\ +\x1c\x6b\xd3\xef\x34\x3c\x93\xde\x38\xfb\xc2\x7d\xeb\xdf\x0e\xbd\ +\xb2\x46\x3d\x02\xac\x00\xba\x81\x26\x40\x00\x43\xc0\xf7\xc0\xe6\ +\x17\xdf\x88\x3e\x08\x95\x59\x16\x37\x4e\x16\xd8\xf8\x9f\xe0\x2a\ +\xf0\xe5\xee\xee\xee\x99\x4b\x6f\x5b\xe1\xe2\xae\x90\xa8\xd9\xe7\ +\xb9\x51\x7f\xb3\x99\xfc\xf8\xae\xef\xbe\xeb\x5b\x06\xbb\xd7\x95\ +\x3f\xca\xe5\x29\xcc\x28\x5c\x08\x16\x7f\x03\xdd\xd0\xdd\xdd\xfd\ +\xe0\xd2\x3b\x9e\xaf\x03\x2d\x0a\xa3\xdf\x90\xcf\xe7\x31\x46\xe3\ +\x38\x0e\x23\x47\xb2\xc4\xec\x99\x48\xb7\x9b\xc8\x1f\xe5\xfd\x95\ +\x83\x20\x04\x22\xd4\xcb\xb5\x10\x45\x83\xb6\x04\xb2\x22\x2e\x84\ +\x2e\x5f\xbe\xfc\xb1\x6b\x6e\x7e\xd7\x9b\xe8\x7f\x8d\xc1\xc1\x41\ +\xa2\x28\x44\x29\x8b\x58\xcc\x21\x91\x70\x19\x3c\xe4\xb2\xf5\x85\ +\xcb\x28\x8c\xd8\x00\x18\x00\xa3\x11\x42\x9e\x5f\x1b\x33\xa2\xb5\ +\xbe\x4f\x4d\x7f\x7e\x6b\x6b\xcb\x73\xb7\xdd\xb3\x35\x33\xf8\xeb\ +\x5a\x4e\x9f\xfe\x03\xad\x35\x5a\x6b\x84\x00\xc7\x89\x61\xdb\x36\ +\x5e\x63\xc0\xb5\x0f\x4c\x52\x1e\xb7\x38\x73\xd8\x41\x08\x81\x10\ +\x02\x63\x0c\x2a\x73\xe6\x80\x2e\x7b\xd7\xbd\x3a\xf6\xc4\x51\x39\ +\x2d\xe0\x75\x2b\x1f\x5e\x97\x0d\x73\xaf\x53\x2e\x97\x49\xa7\xd3\ +\x58\x96\x85\x10\xb2\x76\x41\xa5\xe2\x13\x86\x21\xc2\xf6\x59\xf2\ +\xf4\x10\xe9\xd9\xba\x76\x38\x39\x23\x22\x76\xe7\x5b\xf3\xd7\xe7\ +\x57\x8d\x03\xc8\xa9\x68\x6f\xbd\xf5\x96\x06\xec\xb9\x62\x74\x34\ +\x8f\x65\x59\xd8\xb6\x4d\x3c\x1e\x47\x4a\x41\x7d\x7d\x3d\x9e\xe7\ +\xe1\x79\x1e\x8e\xe3\x10\x86\x21\x23\x27\x14\xe3\x43\x0a\xa9\xc0\ +\x8a\x47\x14\x72\x8a\x39\xe9\x1b\xbd\xf5\xcf\x3a\xab\x6b\xae\x10\ +\x82\xfb\xaf\xbf\xfe\x06\x87\xd2\xf6\xea\xd3\x24\x51\x14\x22\xa5\ +\xa4\xb1\x71\x16\xbf\xff\x7e\x8a\xbd\x7b\xf7\x62\x0c\x74\x76\x2e\ +\xa4\xb3\xf3\x1a\xf6\x6f\x4d\xd1\xd0\xe2\x73\xe7\x5b\x47\x49\x78\ +\x8a\xcd\x4f\x5d\x8e\x3e\x7e\xa3\x70\xe7\xee\x5f\x03\x95\x0d\x12\ +\x40\x29\xab\x47\xa4\x1e\x23\x0c\x26\x50\xea\x7c\xda\x83\x20\xc0\ +\xb6\x1d\x06\x06\xfa\x39\x78\xf0\x67\xd3\xd2\xd2\xfa\x5b\x7b\x7b\ +\xfb\xb6\xe1\xe1\xb3\xd1\xa1\x43\x87\x99\xd1\xac\xb8\x7d\xc3\x0f\ +\x24\x67\x44\x24\x52\x92\x87\xde\x3f\xcd\x65\x9d\x31\xfc\xa2\xbe\ +\xa8\x96\x8a\x28\x0a\x93\xa8\x06\x94\xb2\xb0\x6d\x0b\xa5\x24\x5a\ +\x1b\x00\x76\xec\xd8\x41\x53\x53\xd3\xf6\xbb\x1e\x3d\xd8\x7a\xfb\ +\x83\xdf\xdf\x11\x45\xd1\xd2\x03\x07\xfa\x58\xb2\xfa\x52\x52\x99\ +\x38\x9e\xe7\x11\x8f\xc7\xb1\x6c\xe8\x59\x35\x87\x40\x17\xad\x1a\ +\x78\xca\xcf\x42\xa6\x90\x52\x56\x05\x8b\xd0\x5a\x93\x4a\xa5\x18\ +\x18\xe8\x7f\x6d\x4a\xa4\x63\xc7\x8e\xed\x03\x0c\xb2\x0e\x21\x24\ +\xc6\x50\x13\x17\x99\xfc\x6b\xe5\xd9\xb6\x53\x46\xe7\x13\x58\x17\ +\x13\x8b\x8d\x51\xa9\x54\x48\x24\x12\x00\xcc\x9b\x37\x9f\x13\x27\ +\x7e\x7b\xfb\x95\x35\xea\x2a\x80\x54\xca\x7b\x73\xf1\xe2\xc5\x82\ +\xf0\x24\xa5\x52\x91\xaa\x46\x48\xa9\x70\x53\x27\x71\x1c\x27\x82\ +\xd2\x79\xb0\xe3\xd8\xa7\x99\xdc\xd4\x46\x7c\x11\xca\x1e\x24\x16\ +\xab\x90\x4c\x6a\x82\xa0\x42\x67\xe7\x42\x1a\x1b\x1b\xda\x8d\x31\ +\xbe\x31\xe8\x9e\x9e\x1e\xd5\xd6\xd6\x46\xee\xec\x61\x2c\xcb\xc2\ +\xf7\xcb\x08\x21\xc9\x66\xb3\xe4\xcf\x1d\xc1\x75\xdd\x12\x94\x50\ +\x00\x37\x75\x19\xa9\x64\xb4\xec\x92\xb9\x8f\x08\xcc\x38\xb6\x0a\ +\x88\xc7\x04\x4a\x29\x7c\xdf\x27\x9d\xce\xd0\xd1\xd1\x21\x3a\x3a\ +\xe6\xcb\x54\xca\x13\xb9\x5c\x8e\x20\x08\x70\x9c\x18\x41\x10\x10\ +\x45\x21\x8d\x8d\xb3\xd8\xb7\x6f\x1f\xc5\x62\x71\xff\x96\x9d\xe3\ +\x1f\x2a\x80\x2f\xbf\x8d\x0e\xb4\xcf\x39\xb3\x76\xd1\xa2\x25\x0a\ +\xbb\x0d\x44\x02\x41\x80\xa0\x88\xd6\x86\x5c\xee\x1c\x23\x23\x23\ +\x4c\x4c\x8c\x33\x36\x36\x86\xd6\x1a\x29\x15\xc6\x68\x8c\xd1\x64\ +\x32\x59\x94\x92\xec\xde\xbd\xc7\x18\xa3\xbb\x7a\xbf\x2c\x94\x6a\ +\x95\x67\x0c\xcf\x7f\xba\xe9\x39\x83\xcc\x80\x6a\x04\xab\x19\xcb\ +\x4e\xd5\x7a\x44\x26\x9b\x26\x9d\xce\xe0\x79\x1e\x00\x95\x8a\x4f\ +\xa1\x50\x24\x99\xf4\xa8\x9b\xd9\xca\x96\x2d\x9f\x23\xa5\xdc\xbc\ +\xea\x85\xa1\x73\x00\xb5\x5e\xb1\x6b\x7f\xf8\xe3\xa2\xce\x40\x50\ +\xf9\xe9\xa6\x8b\x5b\xef\xad\x1a\x46\xa2\xc4\x18\x95\x4a\x09\x10\ +\xd5\x12\x17\x35\x17\xd4\xd5\xd5\x51\xdf\x70\x11\xbb\x76\x6e\xe6\ +\xd4\xa9\x93\xa3\x4f\xae\x1d\xbe\xa1\xda\x97\x8c\xaa\x5a\x2d\x0e\ +\x24\xbe\xf8\xaa\xfc\xf3\xd5\xed\xa3\x57\xf6\x9f\xe8\xbd\x7c\xde\ +\xdc\x66\x88\x77\x81\xce\x81\x99\x44\x4a\x81\x6d\xdb\x20\x20\xe9\ +\x26\x69\x6a\x9a\x45\x18\x46\x6c\xed\xfd\x8c\xe3\xc7\x8f\x95\xdf\ +\xdd\x34\xda\x33\x34\x82\x0f\x84\xd3\xc1\xb2\x3a\xdb\xdb\xf6\x94\ +\xf6\x5c\x31\x67\x58\xfe\xf4\xe3\xd7\xd7\x59\xfa\x88\x70\x13\x9a\ +\x64\xd2\x25\xee\x5e\x42\x2c\x51\x8f\x1b\x07\xdf\x2f\xd3\xd7\xd7\ +\x47\x6f\xef\xe7\x4c\x4e\x4e\x7e\xf2\xec\xfa\xf1\xc7\xcf\x0c\x33\ +\x0e\x14\xaa\x60\x3d\xbd\x1f\xcb\xaa\xaf\x53\x80\xe5\xb9\x24\x5f\ +\x7f\x69\xf6\x36\xad\x75\x4b\xa1\x50\xb0\x7c\xbf\x2c\x00\x62\xb1\ +\x38\xae\xeb\x6a\xdb\xb6\x06\x36\xbe\x37\x70\xf7\x2f\x47\x18\x05\ +\xf2\x40\x19\xf0\xab\xa9\xd0\xe2\x1f\xbe\xbb\x29\x51\x15\x60\x75\ +\x2d\x20\xdb\xb5\x80\x66\x37\x41\xf2\x8f\x01\x06\xb7\x7f\xc5\x70\ +\x7e\x82\xf2\x54\x74\x53\x79\x9d\x0e\xf8\x13\xcb\xef\x0c\x40\x37\ +\xea\x17\x2a\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x02\xd3\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -9240,6 +9544,11 @@ qt_resource_name = "\ \x0a\xc8\xfb\x07\ \x00\x66\ \x00\x6f\x00\x6c\x00\x64\x00\x65\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x1a\ +\x04\x78\x27\x87\ +\x00\x6d\ +\x00\x65\x00\x64\x00\x69\x00\x61\x00\x2d\x00\x6f\x00\x70\x00\x74\x00\x69\x00\x63\x00\x61\x00\x6c\x00\x2d\x00\x6d\x00\x6f\x00\x64\ +\x00\x69\x00\x66\x00\x69\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x17\ \x06\x8b\xba\x67\ \x00\x70\ @@ -9259,6 +9568,11 @@ qt_resource_name = "\ \x00\x61\ \x00\x63\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2d\x00\x67\x00\x6f\x00\x2d\x00\x64\x00\x6f\x00\x77\x00\x6e\x00\x2d\x00\x31\x00\x36\ \x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x20\ +\x0a\x57\x7b\xa7\ +\x00\x6d\ +\x00\x65\x00\x64\x00\x69\x00\x61\x00\x2d\x00\x6f\x00\x70\x00\x74\x00\x69\x00\x63\x00\x61\x00\x6c\x00\x2d\x00\x73\x00\x61\x00\x76\ +\x00\x65\x00\x64\x00\x2d\x00\x6d\x00\x6f\x00\x64\x00\x69\x00\x66\x00\x69\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x11\ \x0f\xe3\xd5\x67\ \x00\x64\ @@ -9322,8 +9636,8 @@ qt_resource_struct = "\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x1a\x00\x00\x00\x02\ \x00\x00\x02\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x57\x19\ -\x00\x00\x02\x8c\x00\x02\x00\x00\x00\x0e\x00\x00\x00\x30\ -\x00\x00\x01\xce\x00\x02\x00\x00\x00\x0f\x00\x00\x00\x21\ +\x00\x00\x02\x8c\x00\x02\x00\x00\x00\x10\x00\x00\x00\x32\ +\x00\x00\x01\xce\x00\x02\x00\x00\x00\x11\x00\x00\x00\x21\ \x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x00\xe0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1f\ \x00\x00\x00\xf0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1e\ @@ -9347,40 +9661,44 @@ qt_resource_struct = "\ \x00\x00\x01\xde\x00\x00\x00\x00\x00\x01\x00\x00\x55\x9d\ \x00\x00\x01\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x52\x58\ \x00\x00\x01\x20\x00\x00\x00\x00\x00\x01\x00\x00\x50\xdf\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xe6\xfd\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x01\xd2\x4a\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xc9\x1d\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\xdc\xb0\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x02\x29\x87\ -\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\xa1\x47\ -\x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x99\x2c\ -\x00\x00\x06\x42\x00\x00\x00\x00\x00\x01\x00\x00\xb4\x93\ -\x00\x00\x06\x8e\x00\x00\x00\x00\x00\x01\x00\x00\xbc\x5f\ -\x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x86\x5b\ -\x00\x00\x05\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x95\xd4\ -\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xa9\xe3\ -\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\xab\xe7\ -\x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x8c\xaa\ -\x00\x00\x06\x18\x00\x00\x00\x00\x00\x01\x00\x00\xb1\x7d\ -\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\xa6\xbc\ -\x00\x00\x05\x0a\x00\x00\x00\x00\x00\x01\x00\x00\xc3\xb8\ -\x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x00\x9d\x19\ -\x00\x00\x06\x66\x00\x00\x00\x00\x00\x01\x00\x00\xb7\x68\ -\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\xbf\x36\ -\x00\x00\x04\x90\x00\x00\x00\x00\x00\x01\x00\x00\x76\xfc\ +\x00\x00\x04\x7c\x00\x00\x00\x00\x00\x01\x00\x00\xf9\x73\ +\x00\x00\x04\x7c\x00\x00\x00\x00\x00\x01\x00\x01\xe4\xc0\ +\x00\x00\x04\x7c\x00\x00\x00\x00\x00\x01\x00\x00\xdb\x93\ +\x00\x00\x04\x7c\x00\x00\x00\x00\x00\x01\x00\x00\xef\x26\ +\x00\x00\x04\x7c\x00\x00\x00\x00\x00\x01\x00\x02\x3b\xfd\ +\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\xa8\xb9\ +\x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\xa0\x9e\ +\x00\x00\x06\xc2\x00\x00\x00\x00\x00\x01\x00\x00\xc1\x95\ +\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xce\xd5\ +\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\xb1\x55\ +\x00\x00\x05\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x8d\xcd\ +\x00\x00\x06\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x9d\x46\ +\x00\x00\x04\x48\x00\x00\x00\x00\x00\x01\x00\x00\xb6\xe5\ +\x00\x00\x04\x96\x00\x00\x00\x00\x00\x01\x00\x00\xb8\xe9\ +\x00\x00\x06\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x94\x1c\ +\x00\x00\x06\x98\x00\x00\x00\x00\x00\x01\x00\x00\xbe\x7f\ +\x00\x00\x04\xfa\x00\x00\x00\x00\x00\x01\x00\x00\xc9\x61\ +\x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\xae\x2e\ +\x00\x00\x05\x8a\x00\x00\x00\x00\x00\x01\x00\x00\xd6\x2e\ +\x00\x00\x06\x6a\x00\x00\x00\x00\x00\x01\x00\x00\xa4\x8b\ +\x00\x00\x06\xe6\x00\x00\x00\x00\x00\x01\x00\x00\xc4\x6a\ +\x00\x00\x05\x40\x00\x00\x00\x00\x00\x01\x00\x00\xd1\xac\ +\x00\x00\x04\xca\x00\x00\x00\x00\x00\x01\x00\x00\x7a\xb2\ \x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x68\x62\ \x00\x00\x03\x60\x00\x00\x00\x00\x00\x01\x00\x00\x60\x89\ \x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x6d\xff\ +\x00\x00\x04\x48\x00\x00\x00\x00\x00\x01\x00\x00\x71\xb5\ \x00\x00\x03\xae\x00\x00\x00\x00\x00\x01\x00\x00\x65\x37\ -\x00\x00\x05\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x82\xc7\ -\x00\x00\x04\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x73\x34\ -\x00\x00\x04\x42\x00\x00\x00\x00\x00\x01\x00\x00\x6f\xbb\ +\x00\x00\x05\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x8a\x39\ +\x00\x00\x04\x96\x00\x00\x00\x00\x00\x01\x00\x00\x76\xea\ +\x00\x00\x04\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x73\x71\ +\x00\x00\x04\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x7d\x61\ \x00\x00\x03\xf4\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x09\ -\x00\x00\x04\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x7d\x3e\ +\x00\x00\x05\x68\x00\x00\x00\x00\x00\x01\x00\x00\x84\xb0\ \x00\x00\x03\x2c\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x39\ -\x00\x00\x05\x0a\x00\x00\x00\x00\x00\x01\x00\x00\x7f\x73\ +\x00\x00\x05\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x86\xe5\ \x00\x00\x03\x88\x00\x00\x00\x00\x00\x01\x00\x00\x62\xa6\ -\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x79\xab\ +\x00\x00\x05\x40\x00\x00\x00\x00\x00\x01\x00\x00\x81\x1d\ " def qInitResources(): diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 752b63977..afbaa2aef 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -136,7 +136,10 @@ class MainPanel(QtGui.QSplitter): else: ClusterItem.icon_dir = icontheme.lookup('folder', icontheme.ICON_SIZE_MENU) AlbumItem.icon_cd = icontheme.lookup('media-optical', icontheme.ICON_SIZE_MENU) + AlbumItem.icon_cd_modified = icontheme.lookup('media-optical-modified', icontheme.ICON_SIZE_MENU) AlbumItem.icon_cd_saved = icontheme.lookup('media-optical-saved', icontheme.ICON_SIZE_MENU) + AlbumItem.icon_cd_saved_modified = icontheme.lookup('media-optical-saved-modified', + icontheme.ICON_SIZE_MENU) AlbumItem.icon_error = icontheme.lookup('media-optical-error', icontheme.ICON_SIZE_MENU) TrackItem.icon_note = QtGui.QIcon(":/images/note.png") FileItem.icon_file = QtGui.QIcon(":/images/file.png") @@ -639,9 +642,15 @@ class AlbumItem(TreeItem): if album.errors: self.setIcon(0, AlbumItem.icon_error) elif album.is_complete(): - self.setIcon(0, AlbumItem.icon_cd_saved) + if album.is_modified(): + self.setIcon(0, AlbumItem.icon_cd_saved_modified) + else: + self.setIcon(0, AlbumItem.icon_cd_saved) else: - self.setIcon(0, AlbumItem.icon_cd) + if album.is_modified(): + self.setIcon(0, AlbumItem.icon_cd_modified) + else: + self.setIcon(0, AlbumItem.icon_cd) for i, column in enumerate(MainPanel.columns): self.setText(i, album.column(column[1])) if self.isSelected(): diff --git a/resources/images/16x16/media-optical-modified.png b/resources/images/16x16/media-optical-modified.png new file mode 100644 index 0000000000000000000000000000000000000000..872957847c5935e360e1f0ee4a4e0255c67467db GIT binary patch literal 946 zcmV;j15NyiP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy000McNliru-3AX0BOy;uw95bh13yVb zK~y-)U6b8UTXh)6Kfm8OJ)YCkLSF_2M`;ULWMJZ31cO<^mUv-e*xZ6<yeJyIAk16; z0dHpX&N1+EiQ=N(s4?8=hVDga!4S7;0;{A%3WdUE1$x?^p5uk<LiSufc|PCg`R2)2 z@JNlH8|wi^fj%JdpuGajs;l+idS;`yJ}!*^U7J@QfhWVEaPP>-n>)?T&5s*~p#c<& z#g=S#?dePJeRHzhR#<0|=W2{N0M85#4&CqRe&O|{r6plm7LMa!<!pQbAB~3(@x|#Y z?B3fiZk<@XVSd^9mK2It)HV+e4hN5R935C(T$HZs;_-OUG>z@w_DFv47gALy?^XUk zTbI>}RmH8k19JS_SWh?<?(OOBc`cL4NRP*ZWfj@p$+MBmLVTZNpBzCCso(-G_`+UA zp4rG0Mt;vqV037BMC5WgA*Do0fzRisu^~bt-cEbl5uWI2C(z}6=s5Chfc|%1)`3wK z=xaILlE~-tXqraFuHZBMq|<5UlD`1BuN-?-?4>JoyxoXD<Y(#YEYDc&4M3j?By?TZ zG)=>C9E5;iFvwi;SJ!KpPyM@F$j*E>v-!^XG5Kd<fwn{g!FCgDp#lk6`-Y}zXqtwq zs+gvUl)|}pe`nRUZMzz<%JJSNDwPVhZ4>G;33vDfASAF-DwXV-Kz)6HQmMq5(*sh| zv<+2N(<l2+X_-t0O?&L2$nNfL6<AS$+4c4HaBFL813*)xiOsDXot>TH!}A~MfWErA z%9X1>kchWYsZ{WKz2r7?Mc}H^-Ti$2ZhE=@c;AVjq9~}UilG~9<+oT^NOAl2E%MuW z8XF=AA(2uNjYheAdAd-vitj6_<osIKlbs!bK%lub+FB=sKuQHw_2BdQ2?j%$rVlA4 zmSqu-$4RA<|J?cG&Q}*Fran~wQpx%64X^%^Y5ESuVliDxNx59caU5LN1;8*2VzC%E zZ{8^X^z-bsixX340Y#~0A(fo}+~@bkuV25Gh(ww+UDst@T^)vD0I-%_t4>dUw!eIL z`O?IN$+HhIR~!hV0Mw!3!52-_`-ZBh$AuJ8A%t)oXU(?lA3Yw=)Wn6!xq}J*4-RiN U<nbi7ivR!s07*qoM6N<$f(De$^Z)<= literal 0 HcmV?d00001 diff --git a/resources/images/16x16/media-optical-saved-modified.png b/resources/images/16x16/media-optical-saved-modified.png new file mode 100644 index 0000000000000000000000000000000000000000..bce57cf601a4d88699aae4bc8be86be3e65ff719 GIT binary patch literal 952 zcmV;p14sOcP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy000McNliru-3AX0B0AhLOlAN814T(h zK~y-)U6WsETXhu2Kfm9-_t$%qCMC_<HEHa6(+xXVnQW7~mO%x@7gv>Tj<vO`2zyZ~ zzHEYmbPqC$4D8T3*vpVPW#Geb?xAYCO2HsgnNo!&t=+oT)~!iP{@<I#2isA09uMck zIbRMR!DBTtWVHi>Ko?+dwC8|{;(Xin)cA?tKU#ltVRK%848BlX8|)Yu7+5v79}8pd zQUK&<B88=^PmR8H_oeEV-<J55{hN%~0^jWI?MpQtICm_YI3x1;JmqqQ^s)zEO%>ko z(_DBZO*Xlj)1MojN`86fbtyED*xcOP+ZSkjvghdRoh!0ZsbCleN+}j@*K%Wc2bwM^ zWH!z=%OxjYp<EJ2^^qZ~y|y;k(RkooTXOn+VHk?FwG`{?>j)vRnpb)ClN>jPcal&4 zpU4~ZY1YAyXS3!XixL>@@9!7<ds#>+5dwTZKlSz7vF#`=Em3y2M`+lSew4UrUy&mx z_nW|=4s=z8Ub9*K8l@Dj>r!3qBauiDi^TvcFC6L<5950|_{QJ(gC@7H1rWK`I-pAj zY)o6B8ZavkLP!FE0I^uCVp-<ibUL&6?e!m4UOszLuBE>r8nvl!ad6$J4%qV1H;e{^ zQHQSUXr30N6y;PZb=P$rR|_l^?T&3Yj)UvEg!iNg@6HK8NMMe_Jr@-Qqk)<lKNJ?| z={X{mQvc|>k?8I|tcnYxC}rR{F0SiR%*>a7ISEXZRz6QsWrG=|y8aN^>>8og6XNX; zXUuob#)FYaL{3cH!m=zJr--iWEHAI*fN?Q0WVP4T)m<HYFK$yxgPVdv0%vucczlLT zCWEf)csw4YlqjWW+S$paPd|8&OeSB{#$xXM&bDIfj=+4#AL;fZbdVl2y^5eOPaqJ$ z>-8d~B%jaI+-&p9&*N*;(|=w&eLi<l0~m|BH$vXOo)7zGw*{IGn@9!D5~7?%Pyhs$ zWfE%I$4^t=6(=Vre>{CYcO1~PO&7*u?v+O0O!J+oi}tS217(_)#P<x+Y(nvn#l?H2 zufDjHjmKv$pBl{_-@qzji;M!$-yNXS=hX)^O?y^K`GgQcl*^U-uIqen7{+I(Msu;P a2LA`y^E9%7Iz21^0000<MNUMnLSTXfiMo&g literal 0 HcmV?d00001 diff --git a/resources/images/22x22/media-optical-modified.png b/resources/images/22x22/media-optical-modified.png new file mode 100644 index 0000000000000000000000000000000000000000..0e8c5620ef894e491fd73df99b3d43df5964cf55 GIT binary patch literal 1420 zcmV;71#|j|P)<h;3K|Lk000e1NJLTq000&M000&U1^@s6#I$TX00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy000McNliru-3AX0APc|QF5UnD1sO?1 zK~y-)rIkx;oL3o!pL6;DfBwsir;hE~4T_MRIH@~nX-yj&q6QU$dr=iuVG$5CEYhfH z7l4`t5(*Usp;7@+5CT@T*&s#Gl2S?*Sk!TGBSMRm3e~A&Tw2HRB{P|LJoDeqVG-Lh z5tjvLbEG4EkG}K1@A(e=pX(c?pLKq%bfV|xbN%0no;Y&&OTeSRE}$C_z!l&e@Y0V@ z{NxvzvLglSmVp!h6TAxi)_uE%H;wH5Y*1<|OUKcGAWh@jYSrq#iHV63;92?Wq{fQd zxEU<|4fObZyM_<!dvxC|7R18hBDGo_YYm?3P@ilPXrHJQk+d>?`Grvs0+E@Aj1Vgp zqXjY%H$xwM@ZrZsMjnbTO;0m7cZDpsD6R24kD%aEeW%VF-+Y(mQVM_tuofY1psclv z#+dzT<MsP``<^~<;EQb+F1*7(KKPK_KyJYbiR%_{Tusoa*tx%l_*%x|-<A<VAcVkL zi!HU@q^0it?zO}3$_))a`{l>JQoeloGR;<#QmKU28iXJ>med%MB*kPILM046*+bOr ztrz?4E{%t$2fkB#ybd7Ox_#h*Pj?nPPt>Y49IbF2h3~s)Dd_0fM6nnVMiH*7u-OXN zX6h`@CrBkxo}hU(p|7+fdh)3wPpzjy2=V!$p<%aPuOoy-3WLcr909j>-opFuPc!z% zQ4EHU-+d>axMvG*y|zgEM+@wEZVO(q$mrKE;!SH2_6A=Cj!OWg)b5T=9i(ZDqa>OX zn_S0tGUjJz`0HEeZEtVirQ5d;yn1E6n*Zg(hqU#Sxc~d@1nn+f<nqYR2ibCWH}Oik zd0mPu%R{X-S}9y@aa9XtYq->T?ac4#?(X^Bp+krI4jw%CNS>R$Z=8FZ`;Y9zZz~f- zWeNo!=_rOCzk}4qdL1mpjXW!*&<eC@B25EKOcWKFot^#Onn~|XO^$)JXdf#*4C+R5 zj73WMuP;w)r<Lcq?>Y{->yx?dI0eY8=8oI@nVO#c=@Ul|?*xdV;txNyXGp~93XNur zVFiRlN=Y$^aUC~b2YasjLA6@l7KJ`p8jyKq+)$CEIUgJBrM+u*hp{HMSo6u<!)izW zW)>Fyj@F7+93zCJTrRV?xQHJFO#lTjICzf~QjQD^42axVgmQ2_AEA93i6Lm~qQ5dI zwpRvaR4j|?)hn1Z!Er;9#E|DXU0q#_jlE7|rEzxj52G(=faAwc{75_cxxJs+>v#np zp)_$EBW;Q{Df3savD|1Or9{|_z_o;~#M)(qa4DC|q-ny`r70_g+P`idfHC&yizi>O zQfahy(2j@Wc&jtQr`*;?sZ=BiL%<<U1dT?MFbwJF=-@Y}Uq(v#(y?R5me!>Jn3#C; z{9Sk5DOQ?|J=^<ttVhKHaT0?7M@gixU@}Z@D3?3v?Cj#qnKR7H%v7Iw=G()oC04DD z&H~_{Jv;vP)~$V&i&Gc3_HWxp7=+|`hA}xx3S38_utb4JclWJiS;i}`oMvk3z1FEy zzuLXDv=jr`MzCDnow3))MnAH7v;6b<bN8lcD*Qr$C=4h@Az`6_?>WSAi$6__^V`#> zSzcZ~_5AZcIJmH|PzM^TV6$-^%hmW50UZQEc=YJguNss0HdY!sj$;Ab(A*D<cHG&M zFP{9|<m6-(r~xe?UX{X#|M1<dnW_L?sZ`39O69gfp%Bi^&CQLEkI!Gfe!aEU-I}f& a0sjP@1P%fcyn<{10000<MNUMnLSTZF(6IFY literal 0 HcmV?d00001 diff --git a/resources/images/22x22/media-optical-saved-modified.png b/resources/images/22x22/media-optical-saved-modified.png new file mode 100644 index 0000000000000000000000000000000000000000..39499f7914d5aa4588706ebf613c5916fadab43a GIT binary patch literal 1392 zcmV-$1&{iPP)<h;3K|Lk000e1NJLTq000&M000&U1^@s6#I$TX00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy000McNliru-3AX0AtVhGBE|p!1pP@w zK~y-)rIlN3R7Duae=~E=+1;~eFNL<0szF<eg=&zhtxAKaL4pSL!5h(ddqN*DV$>L- zi7{Y&&?w<W6OH;pqQ)Q=kboe9R*Z@!kbnUJrKK%pyS?n?>^Wz~2ez9^ygWEBlbK|G z^Ue4Ff8W6W9Bb3>G(3~uIQznV>)#H&vPL}utN^-!CO`l~!1uuB7vG3J2$fkDH%=DV z_@Ce^@a69A?wLz(TjAoakf_<`xlw<!nf&;!@4o9>2D{glKg#7P%qUz47JmcX(B0kr z;L<yuuLCU#qu-Fv=P^d(I1VF6vc&9}NVl8FALad(gCGPV)XTLHMT52kvLY^oE?>U< zv1@LA*PG~njlsb|q9{Zui&)Gdk#HG2<g)dJ%P5T402lzH5kj6{8#AJ{zE4emers#X zbGO~IH8c3@T27z-1Fbb$YlHyDiDBC|USq(u4@^=Xw>WdyK?s2m0%Hs+bLN09d)K`> z_QX*+E#P%4A6}OYbFWh_mr1A7SeAtlvUWkG;uD4;!uDD6^bqOUx^_4@BO-R^Tl3fF zSB(S6sc3J#^_B+gxnguQk7ZfdwoN=9M@m6`eLbG%;dvg8;}C`+BPSH&Lkg+DiboW3 ziaF^Uz4gyJt81<hg8Q$({sxEA_Uh9lQ4}Jjq_J@pzyE%Uy?gg!40Lubq_guH_HRwm z(BgCFTSrNFip@`5jy`^)aOdt{1C<RDKq;$7q#h#-CQ#~p+Jhj#wjBlr`Z;*;XVcQs z`dfQ@`?lfXv(ceLhndl=xP8M9Bxgh<Qj&+>Kh0&GG5n&wsFsQ-3X`Y?l(MjG3#BAl z8vu6f*g;cM)AqX_J=l8tgWul~MbXj&2l`mD`chJvIG*PbkH@iW=vg&~Ko_kVc*@U0 zrjSxr1&h#HlS-u+80dd(N~B}Qj_m^svJN36#!wR%nfz<z*|t+g=Mz{L6N`;esZ>ZL z5&+DbH=h$HetY|sHR>vWRLXmE@#4jT@Fb;Dk*Y>Wsc=&#aU3Ut(s{7s*r%D?)JA*} z${r*ZtB_1;f}lcY=Rz7A8`_OAzA@;Yo*vcK)<*8^VJyqS_sa+&$!4?U&mF;a-4c{g z05^3RsbsY5lDUrv%s93Rh{ps<DSY22ozBqF(IGlI=F3#d6S-WDAP8`r7(oyaMInui zv)H$9AH`yE|CXKOA1Z(^zl{#G&pESp(V`^^yA4DFL4cr$)`nc}93vwmOiYY3Ha3RV z8YvaVXpGThGFg<8?B2b{7^Ay-zbuq$m1hjkf4b>8gUo<x1gja#PStuuCYvUm&fs|- z0F{bQp-?25^r)NJ%9bskBc<HD>V=_m0JUDRYk&CTqRv22`RS&MTkowJMxw+Rl}ZT^ z)e;vq7u40&QQvS8yLN8o)TxuBPp%!lq1u;>s#+}$B)<5n{PWfAqgVEy=)HX2+-8V( zfzE-ML`s2e+aMt6CTVJ#MHohG?fs17$B&iY-89-WG=dL=)4{Sj+uQb(_FOS%SpM|m z*VkG4h;S2{WYQ(>UP3HUj~fTyFVol8M{n=vOioUI^6dKY$IlEi4iu_jJ-r{xs;^Uk z<+&tZe`)qMt#wPGP_TTzEC6D$IIinz+qMQazBh39FGm;!@<18ztEtf9AHKUOQ59fy yEg;*qfMz#A@{a)qw|~WOeuDB;bW?eq4)_zx?+idU>K7^i0000<MNUMnLSTX-B$~(o literal 0 HcmV?d00001 diff --git a/resources/picard.qrc b/resources/picard.qrc index e2efe8ef9..32662e25b 100644 --- a/resources/picard.qrc +++ b/resources/picard.qrc @@ -9,6 +9,8 @@ <file>images/16x16/edit-paste.png</file> <file>images/16x16/folder.png</file> <file>images/16x16/media-optical-error.png</file> + <file>images/16x16/media-optical-modified.png</file> + <file>images/16x16/media-optical-saved-modified.png</file> <file>images/16x16/media-optical-saved.png</file> <file>images/16x16/media-optical.png</file> <file>images/16x16/picard.png</file> @@ -20,6 +22,8 @@ <file>images/22x22/list-remove.png</file> <file>images/22x22/lookup-musicbrainz.png</file> <file>images/22x22/media-optical-error.png</file> + <file>images/22x22/media-optical-modified.png</file> + <file>images/22x22/media-optical-saved-modified.png</file> <file>images/22x22/media-optical-saved.png</file> <file>images/22x22/media-optical.png</file> <file>images/22x22/picard-analyze.png</file> From 31a928966ad1bb9e4c5c8f813dd2e86abae14818 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Sun, 16 Jun 2013 11:49:07 +0200 Subject: [PATCH 49/75] Display cover art errors associated with album if any --- picard/coverart.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/picard/coverart.py b/picard/coverart.py index cae09474a..9d523123b 100644 --- a/picard/coverart.py +++ b/picard/coverart.py @@ -83,13 +83,15 @@ AMAZON_SERVER = { AMAZON_IMAGE_PATH = '/images/P/%s.%s.%sZZZZZZZ.jpg' +def _coverart_http_error(album, http): + album.error_append(u'Coverart error: %s' % (unicode(http.errorString()))) def _coverart_downloaded(album, metadata, release, try_list, coverinfos, data, http, error): album._requests -= 1 if error or len(data) < 1000: if error: - log.error(str(http.errorString())) + _coverart_http_error(album, http) else: QObject.tagger.window.set_statusbar_message(N_("Coverart %s downloaded"), http.url().toString()) @@ -115,7 +117,7 @@ def _caa_json_downloaded(album, metadata, release, try_list, data, http, error): album._requests -= 1 caa_front_found = False if error: - log.error(str(http.errorString())) + _coverart_http_error(album, http) else: try: caa_data = json.loads(data) @@ -237,8 +239,8 @@ def _fill_try_list(album, release, try_list): and (relation.type == 'amazon asin' or relation.type == 'has_Amazon_ASIN'): _process_asin_relation(try_list, relation) - except AttributeError, e: - log.error(traceback.format_exc()) + except AttributeError: + album.error_append(traceback.format_exc()) def _walk_try_list(album, metadata, release, try_list): @@ -309,5 +311,5 @@ def _try_list_append_image_url(try_list, parsedUrl, extras=None): } if extras is not None: coverinfos.update(extras) - log.debug("Adding %s image %s", coverinfos['type'], parsedUrl) + log.debug("Adding %s image %s", coverinfos['type'], parsedUrl.toString()) try_list.append(coverinfos) From ebceb8aa0569cacb7ec805463868d7d9f55207ae Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Thu, 27 Jun 2013 21:21:51 +0200 Subject: [PATCH 50/75] Switch tab title to Errors when needed. --- picard/ui/infodialog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/picard/ui/infodialog.py b/picard/ui/infodialog.py index 39ab7e7ab..3c5ed71f5 100644 --- a/picard/ui/infodialog.py +++ b/picard/ui/infodialog.py @@ -117,7 +117,10 @@ class AlbumInfoDialog(InfoDialog): def _display_info_tab(self): tab = self.ui.info_tab album = self.obj + tabWidget = self.ui.tabWidget + tab_index = tabWidget.indexOf(tab) if album.errors: + tabWidget.setTabText(tab_index, _("&Errors")) text = '<br />'.join(map(lambda s: '<font color="darkred">%s</font>' % '<br />'.join(unicode(QtCore.Qt.escape(s)) .replace('\t', ' ') @@ -127,4 +130,5 @@ class AlbumInfoDialog(InfoDialog): ) self.ui.info.setText(text + '<hr />') else: + tabWidget.setTabText(tab_index, _("&Info")) self.tab_hide(tab) From f742a46846bfbab433ffef2f145db03787ca3c26 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Mon, 17 Jun 2013 20:43:55 +0200 Subject: [PATCH 51/75] Use python-discid instead of libdiscid. Python-discid (http://python-discid.readthedocs.org) is a Python binding of libdiscid. It resolves http://tickets.musicbrainz.org/browse/PICARD-503 --- picard/disc.py | 127 ++++--------------------------------- picard/ui/options/about.py | 8 +-- 2 files changed, 16 insertions(+), 119 deletions(-) diff --git a/picard/disc.py b/picard/disc.py index c0fab2815..460669872 100644 --- a/picard/disc.py +++ b/picard/disc.py @@ -3,6 +3,7 @@ # Picard, the next-generation MusicBrainz tagger # Copyright (C) 2007 Lukáš Lalinský # Copyright (C) 2006 Matthias Friedrich +# 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 @@ -18,21 +19,14 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -import ctypes -import sys +# use python-discid (http://python-discid.readthedocs.org/en/latest/) +import discid import traceback from PyQt4 import QtCore from picard import log from picard.ui.cdlookup import CDLookupDialog -_libdiscid = None - - -class DiscError(IOError): - pass - - class Disc(QtCore.QObject): def __init__(self): @@ -40,21 +34,13 @@ class Disc(QtCore.QObject): self.id = None self.submission_url = None - def read(self, device): - global _libdiscid - if _libdiscid is None: - _libdiscid = _openLibrary() - handle = _libdiscid.discid_new() - assert handle != 0, "libdiscid: discid_new() returned NULL" - try: - res = _libdiscid.discid_read_sparse(handle, device or None, 0) - except AttributeError: - res = _libdiscid.discid_read(handle, device or None) - if res == 0: - raise DiscError(_libdiscid.discid_get_error_msg(handle)) - self.id = _libdiscid.discid_get_id(handle) - self.submission_url = _libdiscid.discid_get_submission_url(handle) - _libdiscid.discid_free(handle) + def read(self, device=None): + if device is None: + device = discid.get_default_device() + log.debug(u"Reading CD using device: %r", device) + disc = discid.read(device) + self.id = disc.id + self.submission_url = disc.submission_url def lookup(self): self.tagger.xmlws.lookup_discid(self.id, self._lookup_finished) @@ -74,94 +60,5 @@ class Disc(QtCore.QObject): dialog.exec_() -def libdiscid_version(): - global _libdiscid - try: - if _libdiscid is None: - _libdiscid = _openLibrary() - except NotImplementedError: - return "" - try: - return _libdiscid.discid_get_version_string() - except AttributeError: - return "libdiscid" - - -def _openLibrary(): - """Tries to open libdiscid. - - @return: a C{ctypes.CDLL} object, representing the opened library - - @raise NotImplementedError: if the library can't be opened - """ - - # Check to see if we're running in a Mac OS X bundle. - if sys.platform == 'darwin': - try: - libDiscId = ctypes.cdll.LoadLibrary('../Frameworks/libdiscid.0.dylib') - _setPrototypes(libDiscId) - return libDiscId - except OSError, e: - pass - - # This only works for ctypes >= 0.9.9.3. Any libdiscid is found, - # no matter how it's called on this platform. - try: - if hasattr(ctypes.cdll, 'find'): - libDiscId = ctypes.cdll.find('discid') - _setPrototypes(libDiscId) - return libDiscId - except OSError, e: - raise NotImplementedError(str(e)) - - # For compatibility with ctypes < 0.9.9.3 try to figure out the library - # name without the help of ctypes. We use cdll.LoadLibrary() below, - # which isn't available for ctypes == 0.9.9.3. - # - if sys.platform == 'linux2': - libName = 'libdiscid.so.0' - elif sys.platform == 'darwin': - libName = 'libdiscid.0.dylib' - elif sys.platform == 'win32': - libName = 'discid.dll' - else: - # This should at least work for Un*x-style operating systems - libName = 'libdiscid.so.0' - - try: - libDiscId = ctypes.cdll.LoadLibrary(libName) - _setPrototypes(libDiscId) - return libDiscId - except OSError, e: - raise NotImplementedError('Error opening library: ' + str(e)) - - assert False # not reached - - -def _setPrototypes(libDiscId): - ct = ctypes - libDiscId.discid_new.argtypes = () - libDiscId.discid_new.restype = ct.c_void_p - - libDiscId.discid_free.argtypes = (ct.c_void_p, ) - - libDiscId.discid_read.argtypes = (ct.c_void_p, ct.c_char_p) - try: - libDiscId.discid_read_sparse.argtypes = (ct.c_void_p, ct.c_char_p, - ct.c_uint) - except AttributeError: - pass - - libDiscId.discid_get_error_msg.argtypes = (ct.c_void_p, ) - libDiscId.discid_get_error_msg.restype = ct.c_char_p - - libDiscId.discid_get_id.argtypes = (ct.c_void_p, ) - libDiscId.discid_get_id.restype = ct.c_char_p - - libDiscId.discid_get_submission_url.argtypes = (ct.c_void_p, ) - libDiscId.discid_get_submission_url.restype = ct.c_char_p - - try: - libDiscId.discid_get_version_string.restype = ct.c_char_p - except AttributeError: - pass +disc_version = 'discid %s, %s' % (discid.__version__, + discid.LIBDISCID_VERSION_STRING) diff --git a/picard/ui/options/about.py b/picard/ui/options/about.py index 9e8fea09a..61afafc9e 100644 --- a/picard/ui/options/about.py +++ b/picard/ui/options/about.py @@ -23,7 +23,7 @@ from picard import PICARD_VERSION_STR_SHORT from picard.formats import supported_formats from picard.ui.options import OptionsPage, register_options_page from picard.ui.ui_options_about import Ui_AboutOptionsPage -from picard.disc import libdiscid_version +from picard.disc import disc_version class AboutOptionsPage(OptionsPage): @@ -44,8 +44,8 @@ class AboutOptionsPage(OptionsPage): "version": PICARD_VERSION_STR_SHORT, "mutagen-version": mutagen_version, "pyqt-version": pyqt_version, - "libdiscid-version": libdiscid_version() - } + "disc-version": disc_version + } formats = [] for exts, name in supported_formats(): @@ -65,7 +65,7 @@ Version %(version)s</p> <p align="center"><small> PyQt %(pyqt-version)s<br/> Mutagen %(mutagen-version)s<br/> -%(libdiscid-version)s +%(disc-version)s </small></p> <p align="center"><strong>Supported formats</strong><br/>%(formats)s</p> <p align="center"><strong>Please donate</strong><br/> From b968b44152b023ae4fb3fc8cf517fb7030c9bc74 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Mon, 17 Jun 2013 20:49:02 +0200 Subject: [PATCH 52/75] Use discid/libdiscid drive auto detection, especially on first start It will add default drive as returned by libdiscid to the list of known drives Ie. on linux, list is : /dev/cdrom, /dev/sr0, /dev/sr1 (default link + 2 sata cd readers) On first start it will populate the list with default drive provided by libdiscid, which means Picard is now able to scan CD without going to Options (to set cd reader) first, which was not the case before. --- picard/tagger.py | 5 ++++- picard/ui/options/cdlookup.py | 9 +++++++-- picard/util/cdrom.py | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/picard/tagger.py b/picard/tagger.py index 884612dac..4b57d855a 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -492,8 +492,11 @@ class Tagger(QtGui.QApplication): """Reads CD from the selected drive and tries to lookup the DiscID on MusicBrainz.""" if isinstance(action, QtGui.QAction): device = unicode(action.text()) - else: + elif config.setting["cd_lookup_device"] != '': device = config.setting["cd_lookup_device"].split(",", 1)[0] + else: + #rely on python-discid auto detection + device = None disc = Disc() self.set_wait_cursor() diff --git a/picard/ui/options/cdlookup.py b/picard/ui/options/cdlookup.py index cc05ca480..956ac25a9 100644 --- a/picard/ui/options/cdlookup.py +++ b/picard/ui/options/cdlookup.py @@ -20,7 +20,11 @@ from picard import config from picard.ui.options import OptionsPage, register_options_page -from picard.util.cdrom import get_cdrom_drives, AUTO_DETECT_DRIVES +from picard.util.cdrom import ( + get_cdrom_drives, + AUTO_DETECT_DRIVES, + DEFAULT_DRIVES +) if AUTO_DETECT_DRIVES: from picard.ui.ui_options_cdlookup_select import Ui_CDLookupOptionsPage @@ -37,7 +41,8 @@ class CDLookupOptionsPage(OptionsPage): ACTIVE = True options = [ - config.TextOption("setting", "cd_lookup_device", ""), + config.TextOption("setting", "cd_lookup_device", + ",".join(DEFAULT_DRIVES)), ] def __init__(self, parent=None): diff --git a/picard/util/cdrom.py b/picard/util/cdrom.py index ce8ec351b..3d6a3da4c 100644 --- a/picard/util/cdrom.py +++ b/picard/util/cdrom.py @@ -21,6 +21,16 @@ import sys from PyQt4.QtCore import QFile, QRegExp +DEFAULT_DRIVES = [] +try: + import discid + device = discid.get_default_device() + if device: + DEFAULT_DRIVES = [device] +except: + import traceback + print(traceback.format_exc()) + pass LINUX_CDROM_INFO = '/proc/sys/dev/cdrom/info' @@ -32,7 +42,7 @@ if sys.platform == 'win32': DRIVE_CDROM = 5 def get_cdrom_drives(): - drives = [] + drives = list(DEFAULT_DRIVES) mask = GetLogicalDrives() for i in range(26): if mask >> i & 1: @@ -47,7 +57,7 @@ elif sys.platform == 'linux2' and QFile.exists(LINUX_CDROM_INFO): # Read info from /proc/sys/dev/cdrom/info def get_cdrom_drives(): - drives = [] + drives = list(DEFAULT_DRIVES) cdinfo = QFile(LINUX_CDROM_INFO) if cdinfo.open(QIODevice.ReadOnly | QIODevice.Text): drive_names = [] From b4378041e78e0523f4ed5deecaede135c5f56a3c Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Mon, 17 Jun 2013 22:31:54 +0200 Subject: [PATCH 53/75] Add uniqify() helper function to uniqify a list From http://www.peterbe.com/plog/uniqifiers-benchmark --- picard/util/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/picard/util/__init__.py b/picard/util/__init__.py index 3d6729c1a..161ff7a37 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -376,3 +376,11 @@ def throttle(interval): decorator.prev = 0 decorator.is_ticking = False return decorator + + +def uniqify(seq): + """Uniqify a list, preserving order""" + # Courtesy of Dave Kirby + # See http://www.peterbe.com/plog/uniqifiers-benchmark + seen = set() + return [x for x in seq if x not in seen and not seen.add(x)] From 2fae933a6c2500e99ba06955b296b014e0d22387 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Mon, 17 Jun 2013 22:33:27 +0200 Subject: [PATCH 54/75] Ensure each drive is present at most once in the list. When Picard legacy method to list drives returns one that is already set by python-discid (which is very likely on Windows systems). --- picard/util/cdrom.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/picard/util/cdrom.py b/picard/util/cdrom.py index 3d6a3da4c..b9f923610 100644 --- a/picard/util/cdrom.py +++ b/picard/util/cdrom.py @@ -20,6 +20,7 @@ import sys from PyQt4.QtCore import QFile, QRegExp +from picard.util import uniqify DEFAULT_DRIVES = [] try: @@ -49,7 +50,7 @@ if sys.platform == 'win32': drive = chr(i + ord("A")) + ":\\" if GetDriveType(drive) == DRIVE_CDROM: drives.append(drive) - return drives + return sorted(uniqify(drives)) elif sys.platform == 'linux2' and QFile.exists(LINUX_CDROM_INFO): AUTO_DETECT_DRIVES = True @@ -80,7 +81,7 @@ elif sys.platform == 'linux2' and QFile.exists(LINUX_CDROM_INFO): if symlink_target != '': device = symlink_target drives.append(device) - return sorted(drives) + return sorted(uniqify(drives)) else: AUTO_DETECT_DRIVES = False From 529cc4e49553c52c7a97c777f97a7f77c95087f1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher <sebastian+dev@ramacher.at> Date: Mon, 1 Jul 2013 15:38:28 +0200 Subject: [PATCH 55/75] Also try to use python-libdiscid python-libdiscid is another libdiscid binding that also provides a python-discid compatible interface in libdiscid.compat.discid. Signed-off-by: Sebastian Ramacher <sebastian+dev@ramacher.at> --- picard/disc.py | 8 ++++++-- picard/util/cdrom.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/picard/disc.py b/picard/disc.py index 460669872..b5c7f7378 100644 --- a/picard/disc.py +++ b/picard/disc.py @@ -19,8 +19,12 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# use python-discid (http://python-discid.readthedocs.org/en/latest/) -import discid +try: + # use python-libdiscid (http://pythonhosted.org/python-libdiscid/) + from libdiscid.compat import discid +except ImportError: + # use python-discid (http://python-discid.readthedocs.org/en/latest/) + import discid import traceback from PyQt4 import QtCore from picard import log diff --git a/picard/util/cdrom.py b/picard/util/cdrom.py index b9f923610..58c56f012 100644 --- a/picard/util/cdrom.py +++ b/picard/util/cdrom.py @@ -24,7 +24,10 @@ from picard.util import uniqify DEFAULT_DRIVES = [] try: - import discid + try: + from libdiscid.compat import discid + except ImportError: + import discid device = discid.get_default_device() if device: DEFAULT_DRIVES = [device] From 07424f2b7598e1be7f14c1c548771e95a2cb9ab0 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Wed, 3 Jul 2013 00:34:24 +0200 Subject: [PATCH 56/75] Fix duplicate CD drives in the drop-down: "D:" and "D:\", on Windows --- picard/util/cdrom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/util/cdrom.py b/picard/util/cdrom.py index 58c56f012..a70a5996d 100644 --- a/picard/util/cdrom.py +++ b/picard/util/cdrom.py @@ -50,7 +50,7 @@ if sys.platform == 'win32': mask = GetLogicalDrives() for i in range(26): if mask >> i & 1: - drive = chr(i + ord("A")) + ":\\" + drive = chr(i + ord("A")) + ":" if GetDriveType(drive) == DRIVE_CDROM: drives.append(drive) return sorted(uniqify(drives)) From b9477ed92b295f0c8532da668cf3344497202681 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Wed, 3 Jul 2013 01:28:17 +0200 Subject: [PATCH 57/75] Unused file, only one function never called, test part broken since 2006 --- picard/parsefilename.py | 82 ----------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 picard/parsefilename.py diff --git a/picard/parsefilename.py b/picard/parsefilename.py deleted file mode 100644 index 871857ce1..000000000 --- a/picard/parsefilename.py +++ /dev/null @@ -1,82 +0,0 @@ -import re - -_patterns = [ - # AlbumArtist/1999 - Album/01-TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)((?P<year>\d{4}) - )(?P<album>.*)(/|\\)(?P<tracknum>\d{2})-(?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist - Album/01 - TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2}) - (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist - Album/01-TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2})-(?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist - Album/01. TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2})\. (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist - Album/01 TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2}) (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist - Album/01_Artist_-_TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*) - (?P<album>.*)(/|\\)(?P<tracknum>\d{2})_(?P<artist>.*)_-_(?P<title>.*)\.(?:\w{2,5})$"), - # Album/Artist - Album - 01 - TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P=artist) - (?P<album>.*) - (?P<tracknum>\d{2}) - (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist/Album/Artist - 01 - TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<artist>.*) - (?P<tracknum>\d{2}) - (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist/Album/01. Artist - TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2})\. (?P<artist>.*) - (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist/Album/01 - Artist - TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2}) - (?P<artist>.*) - (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist/Album/01 - TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2}) - (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist/Album/01. TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2})\. (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist/Album/01 TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<tracknum>\d{2}) (?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist/Album/Album-01-TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P=album)-(?P<tracknum>\d{2})-(?P<artist>.*)-(?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist/Album/Album-01-Artist-TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<artist>.*)(/|\\)(?P<album>.*)(/|\\)(?P=album)-(?P<tracknum>\d{2})-(?P<title>.*)\.(?:\w{2,5})$"), - # AlbumArtist/Album/Artist-01-TrackTitle.ext - re.compile(r"(?:.*(/|\\))?(?P<albumartist>.*)(/|\\)(?P<album>.*)(/|\\)(?P<artist>.*)-(?P<tracknum>\d{2})-(?P<title>.*)\.(?:\w{2,5})$"), -] - - -def parseFileName(filename, metadata): - for pattern in _patterns: - match = pattern.match(filename) - if match: - metadata["artist"] = match.group("artist") - metadata["title"] = match.group("title") - metadata["album"] = match.group("album") - -if __name__ == "__main__": - # Thanks to folks at http://www.last.fm/group/Get%2BYour%2BDamn%2BTags%2BRight/forum/13179/_/99927 :) - testCases = [ - (u"F:\\Hudba\\2 Unlimited\\No Limit\\01 No Limit.mp3", u"2 Unlimited", u"No Limit", u"No Limit"), - (u"F:\\Hudba\\2 Unlimited\\No Limit\\No Limit-01-No Limit.mp3", u"2 Unlimited", u"No Limit", u"No Limit"), - (u"F:\\Hudba\\2 Unlimited\\No Limit\\No Limit-01-Test-No Limit.mp3", u"Test", u"No Limit", u"No Limit"), - (u"F:\\grooves\\Brian Eno - Another Green World (1975)\\08 - Sombre Reptiles.ogg", u"Brian Eno", u"Another Green World (1975)", u"Sombre Reptiles"), - (u"My Documents/Music/Various Artists/Album/01 - Artist - Track.ogg", u"Artist", u"Album", u"Track"), - (u"M:\\Albums\\Artist\\Album\\artist - 01 - title.mp3", u"artist", u"Album", u"title"), - (u"F:\\artist\\(year) album\\01 - title.mp3", u"artist", u"(year) album", u"title"), - (u"/home/blaster/Data/Audio/Music/Deep Purple/[2003] Bananas/01 - Deep Purple - House of Pain.ogg", u"Deep Purple", u"[2003] Bananas", u"House of Pain"), - (u"\\A\\A Perfect Circle\\(2000) Mer De Noms\\01 - The Hollow.mp3", u"A Perfect Circle", u"(2000) Mer De Noms", u"The Hollow"), - (u"..\\My Music\\Metal\\Sonata Arctica\\Sonata Arctica - Successor - 05 - Shy.mp3", u"Sonata Arctica", u"Successor", u"Shy"), - (u"D:\\Music\\Artist - Album (year)\\01_artist_-_trackname.mp3", u"artist", u"Album (year)", u"trackname"), - (u"root/MP3/Band/Album/band-01-name.mp3", u"band", u"Album", u"name"), - #(u"D:\\Music\\accPlus 64kb\\Artist\\Year - Album\\00 - Title - Artist.acc", u"Artist", u"Year - Album", u"Title"), - (u"C:\\My Documents\\Media\\Audio\\A\\Autolux\\Future Perfect\\01. Turnstile Blues.mp3", u"Autolux", u"Future Perfect", u"Turnstile Blues"), - (u"music\\artist\\1999 - Album Name\\01-TrackName.mp3", u"artist", u"Album Name", u"TrackName"), - ] - ok = 0 - for testCase in testCases: - mdata = parseFileName(testCase[0]) - print testCase[0] - if not mdata: - print "Error" - else: - if mdata.artist != testCase[1]: - print "Error", "-%s-" % mdata.artist, "-%s-" % testCase[1] - elif mdata.album != testCase[2]: - print "Error", "-%s-" % mdata.album, "-%s-" % testCase[2] - elif mdata.title != testCase[3]: - print "Error", "-%s-" % mdata.title, "-%s-" % testCase[3] - else: - ok += 1 - print "OK" - print len(testCases), ok From c6494fa18f941e578a2985af1c916b411a5a13c3 Mon Sep 17 00:00:00 2001 From: Michael Wiencek <mwtuea@gmail.com> Date: Tue, 2 Jul 2013 18:40:40 -0500 Subject: [PATCH 58/75] Move the VA file naming upgrade to the new hook system --- picard/__init__.py | 1 + picard/config.py | 27 +++++++++++--------- picard/tagger.py | 62 ++++++++++++++++++++++++---------------------- 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/picard/__init__.py b/picard/__init__.py index e7761e469..ce079df66 100644 --- a/picard/__init__.py +++ b/picard/__init__.py @@ -39,6 +39,7 @@ def version_from_string(version_str): g = re.match(r"^(\d+).(\d+).(\d+)(dev|final)(\d+)$", version_str).groups() return (int(g[0]), int(g[1]), int(g[2]), g[3], int(g[4])) + __version__ = PICARD_VERSION_STR = version_to_string(PICARD_VERSION) PICARD_VERSION_STR_SHORT = version_to_string(PICARD_VERSION, short=True) diff --git a/picard/config.py b/picard/config.py index 9eea8ce28..dd0cf6d57 100644 --- a/picard/config.py +++ b/picard/config.py @@ -17,6 +17,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +from operator import itemgetter from PyQt4 import QtCore from picard import PICARD_VERSION, version_to_string, version_from_string, log from picard.util import LockableObject, rot13 @@ -108,31 +109,33 @@ class Config(QtCore.QSettings): return if self._version >= PICARD_VERSION: if self._version > PICARD_VERSION: - m = "Warning: config file %r was created by a more recent version of Picard (current is %r)" - print(m % (version_to_string(self._version), - version_to_string(PICARD_VERSION))) + print("Warning: config file %r was created by a more recent " + "version of Picard (current is %r)" % ( + version_to_string(self._version), + version_to_string(PICARD_VERSION) + )) return - #remove executed hooks if any, and sort - self._upgrade_hooks = [item for item in self._upgrade_hooks if not item['done']] - self._upgrade_hooks.sort(key=lambda k: (k['to'])) + # sort upgrade hooks by version + self._upgrade_hooks.sort(key=itemgetter("to")) for hook in self._upgrade_hooks: if self._version < hook['to']: try: hook['func'](*hook['args']) except Exception as e: - raise ConfigUpgradeError("Error during config upgrade from version %d to %d using %s(): %s" % - (self._version, hook['to'], hook['func'].__name__, e)) + raise ConfigUpgradeError( + "Error during config upgrade from version %d to %d " + "using %s(): %s" % ( + self._version, hook['to'], hook['func'].__name__, e + )) else: hook['done'] = True self._version = hook['to'] self._write_version() else: - #hook is not applicable, mark as done + # hook is not applicable, mark as done hook['done'] = True - # remove executed hooks - self._upgrade_hooks = [item for item in self._upgrade_hooks if not item['done']] - if not self._upgrade_hooks: + if all(map(itemgetter("done"), self._upgrade_hooks)): # all hooks were executed, ensure config is marked with latest version self._version = PICARD_VERSION self._write_version() diff --git a/picard/tagger.py b/picard/tagger.py index 884612dac..70d57a111 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -168,42 +168,44 @@ class Tagger(QtGui.QApplication): self.nats = None self.window = MainWindow() - def remove_va_file_naming_format(merge=True): - if merge: - config.setting["file_naming_format"] = \ - "$if($eq(%compilation%,1),\n$noop(Various Artist albums)\n" + \ - "%s,\n$noop(Single Artist Albums)\n%s)" %\ - (config.setting["va_file_naming_format"].toString(), - config.setting["file_naming_format"]) - config.setting.remove("va_file_naming_format") - config.setting.remove("use_va_format") + def _upgrade_config(self): + cfg = config._config + + # In version 1.0, the file naming formats for single and various + # artist releases were merged. + def upgrade_to_v1_0(): + def remove_va_file_naming_format(merge=True): + if merge: + config.setting["file_naming_format"] = ( + "$if($eq(%compilation%,1),\n$noop(Various Artist " + "albums)\n%s,\n$noop(Single Artist Albums)\n%s)" % ( + config.setting["va_file_naming_format"].toString(), + config.setting["file_naming_format"] + )) + config.setting.remove("va_file_naming_format") + config.setting.remove("use_va_format") + + if ("va_file_naming_format" in config.setting and + "use_va_format" in config.setting): + + if config.setting["use_va_format"].toBool(): + remove_va_file_naming_format() + self.window.show_va_removal_notice() + + elif (config.setting["va_file_naming_format"].toString() != + r"$if2(%albumartist%,%artist%)/%album%/$if($gt(%totaldis" + "cs%,1),%discnumber%-,)$num(%tracknumber%,2) %artist% - " + "%title%"): - if "va_file_naming_format" in config.setting\ - and "use_va_format" in config.setting: - if config.setting["use_va_format"].toBool(): - remove_va_file_naming_format() - self.window.show_va_removal_notice() - elif config.setting["va_file_naming_format"].toString() !=\ - r"$if2(%albumartist%,%artist%)/%album%/$if($gt(%totaldiscs%,1),%discnumber%-,)$num(%tracknumber%,2) %artist% - %title%": if self.window.confirm_va_removal(): remove_va_file_naming_format(merge=False) else: remove_va_file_naming_format() - else: - # default format, disabled - remove_va_file_naming_format(merge=False) + else: + # default format, disabled + remove_va_file_naming_format(merge=False) - def _upgrade_config(self): - cfg = config._config - - def upgrade_conf_test(*args): - """dummy function to test config upgrades, print its arguments""" - print(args[0]) - - #upgrade from config format without version to first version - cfg.register_upgrade_hook('1.0.0final0', - upgrade_conf_test, - "Add version to config file") + cfg.register_upgrade_hook("1.0.0final0", upgrade_to_v1_0) cfg.run_upgrade_hooks() From 28b1a0a05b557aaff271771960da51bc09e53921 Mon Sep 17 00:00:00 2001 From: Michael Wiencek <mwtuea@gmail.com> Date: Tue, 2 Jul 2013 19:03:18 -0500 Subject: [PATCH 59/75] Use uniqify in get_files_from_objects --- picard/tagger.py | 8 +++----- picard/util/__init__.py | 3 ++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/picard/tagger.py b/picard/tagger.py index 35108ff1d..ff30c0407 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -67,7 +67,8 @@ from picard.util import ( queue, thread, mbid_validate, - check_io_encoding + check_io_encoding, + uniqify ) from picard.webservice import XmlWebService @@ -384,10 +385,7 @@ class Tagger(QtGui.QApplication): def get_files_from_objects(self, objects, save=False): """Return list of files from list of albums, clusters, tracks or files.""" - files = chain(*[obj.iterfiles(save) for obj in objects]) - seen_files = set() - add_seen = seen_files.add - return [f for f in files if f not in seen_files and not add_seen(f)] + return uniqify(chain(*[obj.iterfiles(save) for obj in objects])) def _file_saved(self, result=None, error=None): if error is None: diff --git a/picard/util/__init__.py b/picard/util/__init__.py index 161ff7a37..b08deed90 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -383,4 +383,5 @@ def uniqify(seq): # Courtesy of Dave Kirby # See http://www.peterbe.com/plog/uniqifiers-benchmark seen = set() - return [x for x in seq if x not in seen and not seen.add(x)] + add_seen = seen.add + return [x for x in seq if x not in seen and not add_seen(x)] From 157df1544350a0a999cfa60ae3697c5e40274e45 Mon Sep 17 00:00:00 2001 From: Michael Wiencek <mwtuea@gmail.com> Date: Tue, 2 Jul 2013 19:24:24 -0500 Subject: [PATCH 60/75] Don't crash if python-discid is missing --- picard/disc.py | 15 +++++++++++---- picard/ui/options/about.py | 6 +++--- picard/util/cdrom.py | 14 +++++++++----- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/picard/disc.py b/picard/disc.py index b5c7f7378..51f87af32 100644 --- a/picard/disc.py +++ b/picard/disc.py @@ -23,8 +23,12 @@ try: # use python-libdiscid (http://pythonhosted.org/python-libdiscid/) from libdiscid.compat import discid except ImportError: - # use python-discid (http://python-discid.readthedocs.org/en/latest/) - import discid + try: + # use python-discid (http://python-discid.readthedocs.org/en/latest/) + import discid + except ImportError: + discid = None + import traceback from PyQt4 import QtCore from picard import log @@ -64,5 +68,8 @@ class Disc(QtCore.QObject): dialog.exec_() -disc_version = 'discid %s, %s' % (discid.__version__, - discid.LIBDISCID_VERSION_STRING) +if discid is not None: + discid_version = "discid %s, %s" % (discid.__version__, + discid.LIBDISCID_VERSION_STRING) +else: + discid_version = "" diff --git a/picard/ui/options/about.py b/picard/ui/options/about.py index 61afafc9e..df2c62ff4 100644 --- a/picard/ui/options/about.py +++ b/picard/ui/options/about.py @@ -23,7 +23,7 @@ from picard import PICARD_VERSION_STR_SHORT from picard.formats import supported_formats from picard.ui.options import OptionsPage, register_options_page from picard.ui.ui_options_about import Ui_AboutOptionsPage -from picard.disc import disc_version +from picard.disc import discid_version class AboutOptionsPage(OptionsPage): @@ -44,7 +44,7 @@ class AboutOptionsPage(OptionsPage): "version": PICARD_VERSION_STR_SHORT, "mutagen-version": mutagen_version, "pyqt-version": pyqt_version, - "disc-version": disc_version + "discid-version": discid_version } formats = [] @@ -65,7 +65,7 @@ Version %(version)s</p> <p align="center"><small> PyQt %(pyqt-version)s<br/> Mutagen %(mutagen-version)s<br/> -%(disc-version)s +%(discid-version)s </small></p> <p align="center"><strong>Supported formats</strong><br/>%(formats)s</p> <p align="center"><strong>Please donate</strong><br/> diff --git a/picard/util/cdrom.py b/picard/util/cdrom.py index a70a5996d..64ae157d3 100644 --- a/picard/util/cdrom.py +++ b/picard/util/cdrom.py @@ -27,14 +27,18 @@ try: try: from libdiscid.compat import discid except ImportError: - import discid - device = discid.get_default_device() - if device: - DEFAULT_DRIVES = [device] + try: + import discid + except ImportError: + discid = None + if discid is not None: + device = discid.get_default_device() + if device: + DEFAULT_DRIVES = [device] except: import traceback print(traceback.format_exc()) - pass + LINUX_CDROM_INFO = '/proc/sys/dev/cdrom/info' From 7325a735075365938825327dc4111ce81d0e8bef Mon Sep 17 00:00:00 2001 From: Michael Wiencek <mwtuea@gmail.com> Date: Tue, 2 Jul 2013 19:28:25 -0500 Subject: [PATCH 61/75] Update INSTALL.txt --- INSTALL.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/INSTALL.txt b/INSTALL.txt index dedcc5371..392254e2e 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -16,11 +16,11 @@ Before installing Picard, you need to have these libraries: * Mutagen 1.20 or newer http://code.google.com/p/mutagen/ - * libdiscid (optional) - Required for CD lookups. + * python-discid or python-libdiscid (optional) + Required for CD lookups. Depends on libdiscid: http://musicbrainz.org/doc/libdiscid - Due to slowdowns in reading the CD TOC, using versions 0.3.0 - 0.4.1 is not - recommended + Due to slowdowns in reading the CD TOC, using libdiscid versions + 0.3.0 - 0.4.1 is not recommended. Installation ------------ From 7dd311697f8bb0652e91012f042fb27f6d7e4589 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Wed, 3 Jul 2013 03:37:24 +0200 Subject: [PATCH 62/75] tests: PEP8 fixes --- test/test_bytes2human.py | 7 +- test/test_compatid3.py | 6 +- test/test_formats.py | 743 ++++++++++++++++++++------------------- test/test_mbxml.py | 3 + test/test_script.py | 14 +- test/test_similarity.py | 2 +- test/test_utils.py | 16 +- 7 files changed, 400 insertions(+), 391 deletions(-) diff --git a/test/test_bytes2human.py b/test/test_bytes2human.py index 5da804f62..7a37737c2 100644 --- a/test/test_bytes2human.py +++ b/test/test_bytes2human.py @@ -9,6 +9,7 @@ import unittest from picard.i18n import setup_gettext from picard.util import bytes2human + class Testbytes2human(unittest.TestCase): def setUp(self): # we are using temporary locales for tests @@ -41,7 +42,7 @@ class Testbytes2human(unittest.TestCase): except Exception as e: self.fail('Unexpected exception: %s' % e) - def run_test(self, lang = 'C', create_test_data=False): + def run_test(self, lang='C', create_test_data=False): """ Compare data generated with sample files Setting create_test_data to True will generated sample files @@ -62,10 +63,10 @@ class Testbytes2human(unittest.TestCase): values = [0, 1] for n in [1000, 1024]: p = 1 - for e in range(0,6): + for e in range(0, 6): p *= n for x in [0.1, 0.5, 0.99, 0.9999, 1, 1.5]: - values.append(int(p*x)) + values.append(int(p * x)) l = [] for x in sorted(values): l.append(";".join([str(x), bytes2human.decimal(x), diff --git a/test/test_compatid3.py b/test/test_compatid3.py index c497286ec..d9d0aa572 100644 --- a/test/test_compatid3.py +++ b/test/test_compatid3.py @@ -4,11 +4,12 @@ import unittest from mutagen import id3 from picard.formats.mutagenext import compatid3 + class UpdateToV23Test(unittest.TestCase): def test_multiple_text_values(self): tags = compatid3.CompatID3() - tags.add(id3.TALB(encoding=0, text=["123","abc"])) + tags.add(id3.TALB(encoding=0, text=["123", "abc"])) tags.update_to_v23() self.assertEqual(tags["TALB"].text, ["123/abc"]) @@ -36,7 +37,7 @@ class UpdateToV23Test(unittest.TestCase): def test_genre_from_v24_1(self): tags = compatid3.CompatID3() - tags.add(id3.TCON(encoding=1, text=["4","Rock"])) + tags.add(id3.TCON(encoding=1, text=["4", "Rock"])) tags.update_to_v23() self.assertEqual(tags["TCON"].text, ["Disco/Rock"]) @@ -57,4 +58,3 @@ class UpdateToV23Test(unittest.TestCase): tags.add(id3.TCON(encoding=1, text=["(RX)(3)(CR)"])) tags.update_to_v23() self.assertEqual(tags["TCON"].text, ["Remix/Dance/Cover"]) - diff --git a/test/test_formats.py b/test/test_formats.py index ed3f53e74..2c0cfa052 100644 --- a/test/test_formats.py +++ b/test/test_formats.py @@ -34,7 +34,6 @@ class FakeTagger(QtCore.QObject): QtCore.QObject.log = log self.tagger_stats_changed.connect(self.emit) - def emit(self, *args): pass @@ -96,404 +95,404 @@ class FLACTest(FormatsTest): original = os.path.join('test', 'data', 'test.flac') supports_ratings = True tags = { - 'album' : 'Foo Bar', - 'album' : '1', - 'title' : 'Foo', - 'artist' : 'Foo', - 'albumartist' : 'Foo', - 'date' : '2004', - 'originaldate' : '1980', - 'artist' : 'Foo', - 'composer' : 'Foo', - 'lyricist' : 'Foo', - 'conductor' : 'Foo', - 'performer:guest vocal' : 'Foo', - 'remixer' : 'Foo', - 'arranger' : 'Foo', - 'engineer' : 'Foo', - 'producer' : 'Foo', - 'djmixer' : 'Foo', - 'mixer' : 'Foo', - 'grouping' : 'Foo', - 'subtitle' : 'Foo', - 'discsubtitle' : 'Foo', - 'tracknumber' : '2', - 'totaltracks' : '10', - 'discnumber' : '1', - 'totaldiscs' : '2', - 'compilation' : '1', - 'comment:' : 'Foo', - 'comment:foo' : 'Foo', - 'genre' : 'Foo', - 'bpm' : '80', - 'mood' : 'Foo', - 'isrc' : 'Foo', - 'copyright' : 'Foo', - 'lyrics' : 'Foo', - 'media' : 'Foo', - 'label' : 'Foo', - 'catalognumber' : 'Foo', - 'barcode' : 'Foo', - 'encodedby' : 'Foo', - 'albumsort' : 'Foo', - 'albumartistsort' : 'Foo', - 'artistsort' : 'Foo', - 'titlesort' : 'Foo', - #'composersort' : 'Foo', - #'showsort' : 'Foo', - 'musicbrainz_trackid' : 'Foo', - 'musicbrainz_albumid' : 'Foo', - 'musicbrainz_artistid' : 'Foo', - 'musicbrainz_albumartistid' : 'Foo', - 'musicbrainz_trmid' : 'Foo', - 'musicbrainz_discid' : 'Foo', - 'musicip_puid' : 'Foo', - 'musicip_fingerprint' : 'Foo', - 'releasestatus' : 'Foo', - 'releasetype' : 'Foo', - 'asin' : 'Foo', - #'gapless' : '1', - #'podcast' : '1', - #'podcasturl' : 'Foo', - #'show' : 'Foo', - } + 'album': 'Foo Bar', + 'album': '1', + 'title': 'Foo', + 'artist': 'Foo', + 'albumartist': 'Foo', + 'date': '2004', + 'originaldate': '1980', + 'artist': 'Foo', + 'composer': 'Foo', + 'lyricist': 'Foo', + 'conductor': 'Foo', + 'performer:guest vocal': 'Foo', + 'remixer': 'Foo', + 'arranger': 'Foo', + 'engineer': 'Foo', + 'producer': 'Foo', + 'djmixer': 'Foo', + 'mixer': 'Foo', + 'grouping': 'Foo', + 'subtitle': 'Foo', + 'discsubtitle': 'Foo', + 'tracknumber': '2', + 'totaltracks': '10', + 'discnumber': '1', + 'totaldiscs': '2', + 'compilation': '1', + 'comment:': 'Foo', + 'comment:foo': 'Foo', + 'genre': 'Foo', + 'bpm': '80', + 'mood': 'Foo', + 'isrc': 'Foo', + 'copyright': 'Foo', + 'lyrics': 'Foo', + 'media': 'Foo', + 'label': 'Foo', + 'catalognumber': 'Foo', + 'barcode': 'Foo', + 'encodedby': 'Foo', + 'albumsort': 'Foo', + 'albumartistsort': 'Foo', + 'artistsort': 'Foo', + 'titlesort': 'Foo', + #'composersort': 'Foo', + #'showsort': 'Foo', + 'musicbrainz_trackid': 'Foo', + 'musicbrainz_albumid': 'Foo', + 'musicbrainz_artistid': 'Foo', + 'musicbrainz_albumartistid': 'Foo', + 'musicbrainz_trmid': 'Foo', + 'musicbrainz_discid': 'Foo', + 'musicip_puid': 'Foo', + 'musicip_fingerprint': 'Foo', + 'releasestatus': 'Foo', + 'releasetype': 'Foo', + 'asin': 'Foo', + #'gapless': '1', + #'podcast': '1', + #'podcasturl': 'Foo', + #'show': 'Foo', + } class WMATest(FormatsTest): original = os.path.join('test', 'data', 'test.wma') supports_ratings = True tags = { - 'album' : 'Foo Bar', - 'album' : '1', - 'title' : 'Foo', - 'artist' : 'Foo', - 'albumartist' : 'Foo', - 'date' : '2004', - 'originaldate' : '1980', - 'artist' : 'Foo', - 'composer' : 'Foo', - 'lyricist' : 'Foo', - 'conductor' : 'Foo', - #'performer:guest vocal' : 'Foo', - 'remixer' : 'Foo', - #'arranger' : 'Foo', - #'engineer' : 'Foo', - 'producer' : 'Foo', - #'djmixer' : 'Foo', - #'mixer' : 'Foo', - 'grouping' : 'Foo', - 'subtitle' : 'Foo', - 'discsubtitle' : 'Foo', - 'tracknumber' : '2', - #'totaltracks' : '10', - 'discnumber' : '1', - #'totaldiscs' : '2', - #'compilation' : '1', - 'comment:' : 'Foo', + 'album': 'Foo Bar', + 'album': '1', + 'title': 'Foo', + 'artist': 'Foo', + 'albumartist': 'Foo', + 'date': '2004', + 'originaldate': '1980', + 'artist': 'Foo', + 'composer': 'Foo', + 'lyricist': 'Foo', + 'conductor': 'Foo', + #'performer:guest vocal': 'Foo', + 'remixer': 'Foo', + #'arranger': 'Foo', + #'engineer': 'Foo', + 'producer': 'Foo', + #'djmixer': 'Foo', + #'mixer': 'Foo', + 'grouping': 'Foo', + 'subtitle': 'Foo', + 'discsubtitle': 'Foo', + 'tracknumber': '2', + #'totaltracks': '10', + 'discnumber': '1', + #'totaldiscs': '2', + #'compilation': '1', + 'comment:': 'Foo', # FIXME: comment:foo is unsupported in our WMA implementation - #'comment:foo' : 'Foo', - 'genre' : 'Foo', - 'bpm' : '80', - 'mood' : 'Foo', - 'isrc' : 'Foo', - 'copyright' : 'Foo', - 'lyrics' : 'Foo', - #'media' : 'Foo', - 'label' : 'Foo', - #'catalognumber' : 'Foo', - #'barcode' : 'Foo', - 'encodedby' : 'Foo', - 'albumsort' : 'Foo', - 'albumartistsort' : 'Foo', - 'artistsort' : 'Foo', - 'titlesort' : 'Foo', - #'composersort' : 'Foo', - #'showsort' : 'Foo', - 'musicbrainz_trackid' : 'Foo', - 'musicbrainz_albumid' : 'Foo', - 'musicbrainz_artistid' : 'Foo', - 'musicbrainz_albumartistid' : 'Foo', - 'musicbrainz_trmid' : 'Foo', - 'musicbrainz_discid' : 'Foo', - 'musicip_puid' : 'Foo', - #'musicip_fingerprint' : 'Foo', - 'releasestatus' : 'Foo', - 'releasetype' : 'Foo', - #'asin' : 'Foo', - #'gapless' : '1', - #'podcast' : '1', - #'podcasturl' : 'Foo', - #'show' : 'Foo', - } + #'comment:foo': 'Foo', + 'genre': 'Foo', + 'bpm': '80', + 'mood': 'Foo', + 'isrc': 'Foo', + 'copyright': 'Foo', + 'lyrics': 'Foo', + #'media': 'Foo', + 'label': 'Foo', + #'catalognumber': 'Foo', + #'barcode': 'Foo', + 'encodedby': 'Foo', + 'albumsort': 'Foo', + 'albumartistsort': 'Foo', + 'artistsort': 'Foo', + 'titlesort': 'Foo', + #'composersort': 'Foo', + #'showsort': 'Foo', + 'musicbrainz_trackid': 'Foo', + 'musicbrainz_albumid': 'Foo', + 'musicbrainz_artistid': 'Foo', + 'musicbrainz_albumartistid': 'Foo', + 'musicbrainz_trmid': 'Foo', + 'musicbrainz_discid': 'Foo', + 'musicip_puid': 'Foo', + #'musicip_fingerprint': 'Foo', + 'releasestatus': 'Foo', + 'releasetype': 'Foo', + #'asin': 'Foo', + #'gapless': '1', + #'podcast': '1', + #'podcasturl': 'Foo', + #'show': 'Foo', + } class MP3Test(FormatsTest): original = os.path.join('test', 'data', 'test.mp3') supports_ratings = True tags = { - 'album' : 'Foo Bar', - 'album' : '1', - 'title' : 'Foo', - 'artist' : 'Foo', - 'albumartist' : 'Foo', - 'date' : '2004', - 'originaldate' : '1980', - 'artist' : 'Foo', - 'composer' : 'Foo', - 'lyricist' : 'Foo', - 'conductor' : 'Foo', - 'performer:guest vocal' : 'Foo', - 'remixer' : 'Foo', - 'arranger' : 'Foo', - 'engineer' : 'Foo', - 'producer' : 'Foo', - 'djmixer' : 'Foo', - 'mixer' : 'Foo', - 'grouping' : 'Foo', - 'subtitle' : 'Foo', - 'discsubtitle' : 'Foo', - 'tracknumber' : '2', - 'totaltracks' : '10', - 'discnumber' : '1', - 'totaldiscs' : '2', - 'compilation' : '1', - 'comment:' : 'Foo', - 'comment:foo' : 'Foo', - 'genre' : 'Foo', - 'bpm' : '80', - 'mood' : 'Foo', - 'isrc' : 'Foo', - 'copyright' : 'Foo', - 'lyrics' : 'Foo', - 'media' : 'Foo', - 'label' : 'Foo', - 'catalognumber' : 'Foo', - 'barcode' : 'Foo', - 'encodedby' : 'Foo', - 'albumsort' : 'Foo', - 'albumartistsort' : 'Foo', - 'artistsort' : 'Foo', - 'titlesort' : 'Foo', - #'composersort' : 'Foo', - #'showsort' : 'Foo', - 'musicbrainz_trackid' : 'Foo', - 'musicbrainz_albumid' : 'Foo', - 'musicbrainz_artistid' : 'Foo', - 'musicbrainz_albumartistid' : 'Foo', - 'musicbrainz_trmid' : 'Foo', - 'musicbrainz_discid' : 'Foo', - 'musicip_puid' : 'Foo', - 'musicip_fingerprint' : 'Foo', - 'releasestatus' : 'Foo', - 'releasetype' : 'Foo', - 'asin' : 'Foo', - #'gapless' : '1', - #'podcast' : '1', - #'podcasturl' : 'Foo', - #'show' : 'Foo', - } + 'album': 'Foo Bar', + 'album': '1', + 'title': 'Foo', + 'artist': 'Foo', + 'albumartist': 'Foo', + 'date': '2004', + 'originaldate': '1980', + 'artist': 'Foo', + 'composer': 'Foo', + 'lyricist': 'Foo', + 'conductor': 'Foo', + 'performer:guest vocal': 'Foo', + 'remixer': 'Foo', + 'arranger': 'Foo', + 'engineer': 'Foo', + 'producer': 'Foo', + 'djmixer': 'Foo', + 'mixer': 'Foo', + 'grouping': 'Foo', + 'subtitle': 'Foo', + 'discsubtitle': 'Foo', + 'tracknumber': '2', + 'totaltracks': '10', + 'discnumber': '1', + 'totaldiscs': '2', + 'compilation': '1', + 'comment:': 'Foo', + 'comment:foo': 'Foo', + 'genre': 'Foo', + 'bpm': '80', + 'mood': 'Foo', + 'isrc': 'Foo', + 'copyright': 'Foo', + 'lyrics': 'Foo', + 'media': 'Foo', + 'label': 'Foo', + 'catalognumber': 'Foo', + 'barcode': 'Foo', + 'encodedby': 'Foo', + 'albumsort': 'Foo', + 'albumartistsort': 'Foo', + 'artistsort': 'Foo', + 'titlesort': 'Foo', + #'composersort': 'Foo', + #'showsort': 'Foo', + 'musicbrainz_trackid': 'Foo', + 'musicbrainz_albumid': 'Foo', + 'musicbrainz_artistid': 'Foo', + 'musicbrainz_albumartistid': 'Foo', + 'musicbrainz_trmid': 'Foo', + 'musicbrainz_discid': 'Foo', + 'musicip_puid': 'Foo', + 'musicip_fingerprint': 'Foo', + 'releasestatus': 'Foo', + 'releasetype': 'Foo', + 'asin': 'Foo', + #'gapless': '1', + #'podcast': '1', + #'podcasturl': 'Foo', + #'show': 'Foo', + } class OggVorbisTest(FormatsTest): original = os.path.join('test', 'data', 'test.ogg') supports_ratings = True tags = { - 'album' : 'Foo Bar', - 'album' : '1', - 'title' : 'Foo', - 'artist' : 'Foo', - 'albumartist' : 'Foo', - 'date' : '2004', - 'originaldate' : '1980', - 'artist' : 'Foo', - 'composer' : 'Foo', - 'lyricist' : 'Foo', - 'conductor' : 'Foo', - 'performer:guest vocal' : 'Foo', - 'remixer' : 'Foo', - 'arranger' : 'Foo', - 'engineer' : 'Foo', - 'producer' : 'Foo', - 'djmixer' : 'Foo', - 'mixer' : 'Foo', - 'grouping' : 'Foo', - 'subtitle' : 'Foo', - 'discsubtitle' : 'Foo', - 'tracknumber' : '2', - 'totaltracks' : '10', - 'discnumber' : '1', - 'totaldiscs' : '2', - 'compilation' : '1', - 'comment:' : 'Foo', - 'comment:foo' : 'Foo', - 'genre' : 'Foo', - 'bpm' : '80', - 'mood' : 'Foo', - 'isrc' : 'Foo', - 'copyright' : 'Foo', - 'lyrics' : 'Foo', - 'media' : 'Foo', - 'label' : 'Foo', - 'catalognumber' : 'Foo', - 'barcode' : 'Foo', - 'encodedby' : 'Foo', - 'albumsort' : 'Foo', - 'albumartistsort' : 'Foo', - 'artistsort' : 'Foo', - 'titlesort' : 'Foo', - #'composersort' : 'Foo', - #'showsort' : 'Foo', - 'musicbrainz_trackid' : 'Foo', - 'musicbrainz_albumid' : 'Foo', - 'musicbrainz_artistid' : 'Foo', - 'musicbrainz_albumartistid' : 'Foo', - 'musicbrainz_trmid' : 'Foo', - 'musicbrainz_discid' : 'Foo', - 'musicip_puid' : 'Foo', - 'musicip_fingerprint' : 'Foo', - 'releasestatus' : 'Foo', - 'releasetype' : 'Foo', - 'asin' : 'Foo', - #'gapless' : '1', - #'podcast' : '1', - #'podcasturl' : 'Foo', - #'show' : 'Foo', - } + 'album': 'Foo Bar', + 'album': '1', + 'title': 'Foo', + 'artist': 'Foo', + 'albumartist': 'Foo', + 'date': '2004', + 'originaldate': '1980', + 'artist': 'Foo', + 'composer': 'Foo', + 'lyricist': 'Foo', + 'conductor': 'Foo', + 'performer:guest vocal': 'Foo', + 'remixer': 'Foo', + 'arranger': 'Foo', + 'engineer': 'Foo', + 'producer': 'Foo', + 'djmixer': 'Foo', + 'mixer': 'Foo', + 'grouping': 'Foo', + 'subtitle': 'Foo', + 'discsubtitle': 'Foo', + 'tracknumber': '2', + 'totaltracks': '10', + 'discnumber': '1', + 'totaldiscs': '2', + 'compilation': '1', + 'comment:': 'Foo', + 'comment:foo': 'Foo', + 'genre': 'Foo', + 'bpm': '80', + 'mood': 'Foo', + 'isrc': 'Foo', + 'copyright': 'Foo', + 'lyrics': 'Foo', + 'media': 'Foo', + 'label': 'Foo', + 'catalognumber': 'Foo', + 'barcode': 'Foo', + 'encodedby': 'Foo', + 'albumsort': 'Foo', + 'albumartistsort': 'Foo', + 'artistsort': 'Foo', + 'titlesort': 'Foo', + #'composersort': 'Foo', + #'showsort': 'Foo', + 'musicbrainz_trackid': 'Foo', + 'musicbrainz_albumid': 'Foo', + 'musicbrainz_artistid': 'Foo', + 'musicbrainz_albumartistid': 'Foo', + 'musicbrainz_trmid': 'Foo', + 'musicbrainz_discid': 'Foo', + 'musicip_puid': 'Foo', + 'musicip_fingerprint': 'Foo', + 'releasestatus': 'Foo', + 'releasetype': 'Foo', + 'asin': 'Foo', + #'gapless': '1', + #'podcast': '1', + #'podcasturl': 'Foo', + #'show': 'Foo', + } class MP4Test(FormatsTest): original = os.path.join('test', 'data', 'test.m4a') supports_ratings = False tags = { - 'album' : 'Foo Bar', - 'album' : '1', - 'title' : 'Foo', - 'artist' : 'Foo', - 'albumartist' : 'Foo', - 'date' : '2004', - #'originaldate' : '1980', - 'artist' : 'Foo', - 'composer' : 'Foo', - 'lyricist' : 'Foo', - 'conductor' : 'Foo', - #'performer:guest vocal' : 'Foo', - 'remixer' : 'Foo', - #'arranger' : 'Foo', - 'engineer' : 'Foo', - 'producer' : 'Foo', - 'djmixer' : 'Foo', - 'mixer' : 'Foo', - 'grouping' : 'Foo', - 'subtitle' : 'Foo', - 'discsubtitle' : 'Foo', - 'tracknumber' : '2', - 'totaltracks' : '10', - 'discnumber' : '1', - 'totaldiscs' : '2', - 'compilation' : '1', - 'comment:' : 'Foo', + 'album': 'Foo Bar', + 'album': '1', + 'title': 'Foo', + 'artist': 'Foo', + 'albumartist': 'Foo', + 'date': '2004', + #'originaldate': '1980', + 'artist': 'Foo', + 'composer': 'Foo', + 'lyricist': 'Foo', + 'conductor': 'Foo', + #'performer:guest vocal': 'Foo', + 'remixer': 'Foo', + #'arranger': 'Foo', + 'engineer': 'Foo', + 'producer': 'Foo', + 'djmixer': 'Foo', + 'mixer': 'Foo', + 'grouping': 'Foo', + 'subtitle': 'Foo', + 'discsubtitle': 'Foo', + 'tracknumber': '2', + 'totaltracks': '10', + 'discnumber': '1', + 'totaldiscs': '2', + 'compilation': '1', + 'comment:': 'Foo', # FIXME: comment:foo is unsupported in our MP4 implementation - #'comment:foo' : 'Foo', - 'genre' : 'Foo', - 'bpm' : '80', - 'mood' : 'Foo', - 'isrc' : 'Foo', - 'copyright' : 'Foo', - 'lyrics' : 'Foo', - 'media' : 'Foo', - 'label' : 'Foo', - 'catalognumber' : 'Foo', - 'barcode' : 'Foo', - 'encodedby' : 'Foo', - 'albumsort' : 'Foo', - 'albumartistsort' : 'Foo', - 'artistsort' : 'Foo', - 'titlesort' : 'Foo', - 'composersort' : 'Foo', - 'showsort' : 'Foo', - 'musicbrainz_trackid' : 'Foo', - 'musicbrainz_albumid' : 'Foo', - 'musicbrainz_artistid' : 'Foo', - 'musicbrainz_albumartistid' : 'Foo', - 'musicbrainz_trmid' : 'Foo', - 'musicbrainz_discid' : 'Foo', - 'musicip_puid' : 'Foo', - 'musicip_fingerprint' : 'Foo', - 'releasestatus' : 'Foo', - 'releasetype' : 'Foo', - 'asin' : 'Foo', - 'gapless' : '1', - 'podcast' : '1', - 'podcasturl' : 'Foo', - 'show' : 'Foo', - } + #'comment:foo': 'Foo', + 'genre': 'Foo', + 'bpm': '80', + 'mood': 'Foo', + 'isrc': 'Foo', + 'copyright': 'Foo', + 'lyrics': 'Foo', + 'media': 'Foo', + 'label': 'Foo', + 'catalognumber': 'Foo', + 'barcode': 'Foo', + 'encodedby': 'Foo', + 'albumsort': 'Foo', + 'albumartistsort': 'Foo', + 'artistsort': 'Foo', + 'titlesort': 'Foo', + 'composersort': 'Foo', + 'showsort': 'Foo', + 'musicbrainz_trackid': 'Foo', + 'musicbrainz_albumid': 'Foo', + 'musicbrainz_artistid': 'Foo', + 'musicbrainz_albumartistid': 'Foo', + 'musicbrainz_trmid': 'Foo', + 'musicbrainz_discid': 'Foo', + 'musicip_puid': 'Foo', + 'musicip_fingerprint': 'Foo', + 'releasestatus': 'Foo', + 'releasetype': 'Foo', + 'asin': 'Foo', + 'gapless': '1', + 'podcast': '1', + 'podcasturl': 'Foo', + 'show': 'Foo', + } class WavPackTest(FormatsTest): original = os.path.join('test', 'data', 'test.wv') supports_ratings = False tags = { - 'album' : 'Foo Bar', - 'album' : '1', - 'title' : 'Foo', - 'artist' : 'Foo', - 'albumartist' : 'Foo', - 'date' : '2004', - #'originaldate' : '1980', - 'artist' : 'Foo', - 'composer' : 'Foo', - 'lyricist' : 'Foo', - 'conductor' : 'Foo', - 'performer:guest vocal' : 'Foo', - 'remixer' : 'Foo', - 'arranger' : 'Foo', - 'engineer' : 'Foo', - 'producer' : 'Foo', - 'djmixer' : 'Foo', - 'mixer' : 'Foo', - 'grouping' : 'Foo', - 'subtitle' : 'Foo', - 'discsubtitle' : 'Foo', - 'tracknumber' : '2', - 'totaltracks' : '10', - 'discnumber' : '1', - 'totaldiscs' : '2', - 'compilation' : '1', - 'comment:' : 'Foo', - 'comment:foo' : 'Foo', - 'genre' : 'Foo', - 'bpm' : '80', - 'mood' : 'Foo', - 'isrc' : 'Foo', - 'copyright' : 'Foo', - 'lyrics' : 'Foo', - 'media' : 'Foo', - 'label' : 'Foo', - 'catalognumber' : 'Foo', - 'barcode' : 'Foo', - 'encodedby' : 'Foo', - 'albumsort' : 'Foo', - 'albumartistsort' : 'Foo', - 'artistsort' : 'Foo', - 'titlesort' : 'Foo', - #'composersort' : 'Foo', - #'showsort' : 'Foo', - 'musicbrainz_trackid' : 'Foo', - 'musicbrainz_albumid' : 'Foo', - 'musicbrainz_artistid' : 'Foo', - 'musicbrainz_albumartistid' : 'Foo', - 'musicbrainz_trmid' : 'Foo', - 'musicbrainz_discid' : 'Foo', - 'musicip_puid' : 'Foo', - #'musicip_fingerprint' : 'Foo', - 'releasestatus' : 'Foo', - 'releasetype' : 'Foo', - 'asin' : 'Foo', - #'gapless' : '1', - #'podcast' : '1', - #'podcasturl' : 'Foo', - #'show' : 'Foo', - } + 'album': 'Foo Bar', + 'album': '1', + 'title': 'Foo', + 'artist': 'Foo', + 'albumartist': 'Foo', + 'date': '2004', + #'originaldate': '1980', + 'artist': 'Foo', + 'composer': 'Foo', + 'lyricist': 'Foo', + 'conductor': 'Foo', + 'performer:guest vocal': 'Foo', + 'remixer': 'Foo', + 'arranger': 'Foo', + 'engineer': 'Foo', + 'producer': 'Foo', + 'djmixer': 'Foo', + 'mixer': 'Foo', + 'grouping': 'Foo', + 'subtitle': 'Foo', + 'discsubtitle': 'Foo', + 'tracknumber': '2', + 'totaltracks': '10', + 'discnumber': '1', + 'totaldiscs': '2', + 'compilation': '1', + 'comment:': 'Foo', + 'comment:foo': 'Foo', + 'genre': 'Foo', + 'bpm': '80', + 'mood': 'Foo', + 'isrc': 'Foo', + 'copyright': 'Foo', + 'lyrics': 'Foo', + 'media': 'Foo', + 'label': 'Foo', + 'catalognumber': 'Foo', + 'barcode': 'Foo', + 'encodedby': 'Foo', + 'albumsort': 'Foo', + 'albumartistsort': 'Foo', + 'artistsort': 'Foo', + 'titlesort': 'Foo', + #'composersort': 'Foo', + #'showsort': 'Foo', + 'musicbrainz_trackid': 'Foo', + 'musicbrainz_albumid': 'Foo', + 'musicbrainz_artistid': 'Foo', + 'musicbrainz_albumartistid': 'Foo', + 'musicbrainz_trmid': 'Foo', + 'musicbrainz_discid': 'Foo', + 'musicip_puid': 'Foo', + #'musicip_fingerprint': 'Foo', + 'releasestatus': 'Foo', + 'releasetype': 'Foo', + 'asin': 'Foo', + #'gapless': '1', + #'podcast': '1', + #'podcasturl': 'Foo', + #'show': 'Foo', + } class TestCoverArt(unittest.TestCase): @@ -532,8 +531,14 @@ class TestCoverArt(unittest.TestCase): # This checks a mutagen error with ASF files. dummyload = "a" * 1024 * 128 tests = { - 'jpg': {'mime': 'image/jpeg', 'head': 'JFIF'}, - 'png': {'mime': 'image/png', 'head': 'PNG'}, + 'jpg': { + 'mime': 'image/jpeg', + 'head': 'JFIF' + }, + 'png': { + 'mime': 'image/png', + 'head': 'PNG' + }, } for t in tests: f = picard.formats.open(self.filename) diff --git a/test/test_mbxml.py b/test/test_mbxml.py index 97884dbfb..613a5b633 100644 --- a/test/test_mbxml.py +++ b/test/test_mbxml.py @@ -38,8 +38,10 @@ class TrackTest(unittest.TestCase): def test_1(self): config.setting = settings + class Track: pass + node = XmlNode(children={ 'title': [XmlNode(text='Foo')], 'length': [XmlNode(text='180000')], @@ -78,6 +80,7 @@ class TrackTest(unittest.TestCase): self.assertEqual('workid123', m['musicbrainz_workid']) self.assertEqual('eng', m['language']) + class ReleaseTest(unittest.TestCase): def test_1(self): diff --git a/test/test_script.py b/test/test_script.py index 831ca3084..c79095374 100644 --- a/test/test_script.py +++ b/test/test_script.py @@ -41,40 +41,40 @@ class ScriptParserTest(unittest.TestCase): context = Metadata() context["source"] = ["multi", "valued"] self.parser.eval("$set(test,%source%)", context) - self.assertEqual(context.getall("test"), ["multi; valued"]) # list has only a single value + self.assertEqual(context.getall("test"), ["multi; valued"]) # list has only a single value def test_cmd_setmulti_multi_valued(self): context = Metadata() context["source"] = ["multi", "valued"] - self.assertEqual("", self.parser.eval("$setmulti(test,%source%)", context)) # no return value + self.assertEqual("", self.parser.eval("$setmulti(test,%source%)", context)) # no return value self.assertEqual(context.getall("source"), context.getall("test")) def test_cmd_setmulti_multi_valued_wth_spaces(self): context = Metadata() context["source"] = ["multi, multi", "valued, multi"] - self.assertEqual("", self.parser.eval("$setmulti(test,%source%)", context)) # no return value + self.assertEqual("", self.parser.eval("$setmulti(test,%source%)", context)) # no return value self.assertEqual(context.getall("source"), context.getall("test")) def test_cmd_setmulti_not_multi_valued(self): context = Metadata() context["source"] = "multi, multi" - self.assertEqual("", self.parser.eval("$setmulti(test,%source%)", context)) # no return value + self.assertEqual("", self.parser.eval("$setmulti(test,%source%)", context)) # no return value self.assertEqual(context.getall("source"), context.getall("test")) def test_cmd_setmulti_will_remove_empty_items(self): context = Metadata() context["source"] = ["", "multi", ""] - self.assertEqual("", self.parser.eval("$setmulti(test,%source%)", context)) # no return value + self.assertEqual("", self.parser.eval("$setmulti(test,%source%)", context)) # no return value self.assertEqual(["multi"], context.getall("test")) def test_cmd_setmulti_custom_splitter_string(self): context = Metadata() - self.assertEqual("", self.parser.eval("$setmulti(test,multi##valued##test##,##)", context)) # no return value + self.assertEqual("", self.parser.eval("$setmulti(test,multi##valued##test##,##)", context)) # no return value self.assertEqual(["multi", "valued", "test"], context.getall("test")) def test_cmd_setmulti_empty_splitter_does_nothing(self): context = Metadata() - self.assertEqual("", self.parser.eval("$setmulti(test,multi; valued,)", context)) # no return value + self.assertEqual("", self.parser.eval("$setmulti(test,multi; valued,)", context)) # no return value self.assertEqual(["multi; valued"], context.getall("test")) def test_cmd_get(self): diff --git a/test/test_similarity.py b/test/test_similarity.py index 8e2eb1437..475e717dd 100644 --- a/test/test_similarity.py +++ b/test/test_similarity.py @@ -1,10 +1,10 @@ import unittest from picard.similarity import similarity + class SimilarityTest(unittest.TestCase): def test_correct(self): self.assertEqual(similarity(u"K!", u"K!"), 1.0) self.assertEqual(similarity(u"BBB", u"AAA"), 0.0) self.assertAlmostEqual(similarity(u"ABC", u"ABB"), 0.7, 1) - diff --git a/test/test_utils.py b/test/test_utils.py index 029226562..a298d7ee0 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -60,6 +60,7 @@ class SanitizeDateTest(unittest.TestCase): self.assertNotEqual(util.sanitize_date("2006--02"), "2006-02") self.assertNotEqual(util.sanitize_date("2006.03.02"), "2006-03-02") + class ShortFilenameTest(unittest.TestCase): def test_short(self): @@ -103,15 +104,15 @@ class TranslateArtistTest(unittest.TestCase): self.assertNotEqual(u"Tchaikovsky, Pyotr Ilyich", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) self.assertNotEqual(u"Пётр Ильич Чайковский", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich")) - + class FormatTimeTest(unittest.TestCase): - def test(self): - self.assertEqual("?:??", util.format_time(0)) - self.assertEqual("3:00", util.format_time(179750)) - self.assertEqual("3:00", util.format_time(179500)) - self.assertEqual("2:59", util.format_time(179499)) - + def test(self): + self.assertEqual("?:??", util.format_time(0)) + self.assertEqual("3:00", util.format_time(179750)) + self.assertEqual("3:00", util.format_time(179500)) + self.assertEqual("2:59", util.format_time(179499)) + class LoadReleaseTypeScoresTest(unittest.TestCase): @@ -141,4 +142,3 @@ class SaveReleaseTypeScoresTest(unittest.TestCase): self.assertTrue("Single 0.50" in saved_scores) self.assertTrue("Other 0.00" in saved_scores) self.assertEqual(6, len(saved_scores.split())) - From 8ba96e485166d80d4a7ef725ace10fc44aa00a9b Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann <themineo@gmail.com> Date: Wed, 3 Jul 2013 17:35:44 +0200 Subject: [PATCH 63/75] Unbreak moving WavPack correction files The `settings` argument to `File._rename` has been removed in 7c4fdf76. --- picard/formats/apev2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/formats/apev2.py b/picard/formats/apev2.py index d2a476f7f..85b5b0cc1 100644 --- a/picard/formats/apev2.py +++ b/picard/formats/apev2.py @@ -180,8 +180,8 @@ class WavPackFile(APEv2File): wvc_filename = old_filename.replace(".wv", ".wvc") if isfile(wvc_filename): if config.setting["rename_files"] or config.setting["move_files"]: - self._rename(wvc_filename, metadata, config.setting) - return File._save_and_rename(self, old_filename, metadata, config.setting) + self._rename(wvc_filename, metadata) + return File._save_and_rename(self, old_filename, metadata) class OptimFROGFile(APEv2File): From 93bb43ec6e607032ed5f380988407372d11bafca Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann <themineo@gmail.com> Date: Wed, 3 Jul 2013 17:44:37 +0200 Subject: [PATCH 64/75] File._rename: Return early if the new filename is the same as the old one This is mostly for readability. --- picard/file.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/picard/file.py b/picard/file.py index a97254a1e..a699086f4 100644 --- a/picard/file.py +++ b/picard/file.py @@ -298,23 +298,24 @@ class File(QtCore.QObject, Item): def _rename(self, old_filename, metadata): new_filename, ext = os.path.splitext( self._make_filename(old_filename, metadata)) - if old_filename != new_filename + ext: - new_dirname = os.path.dirname(new_filename) - if not os.path.isdir(encode_filename(new_dirname)): - os.makedirs(new_dirname) - tmp_filename = new_filename - i = 1 - while (not pathcmp(old_filename, new_filename + ext) and - os.path.exists(encode_filename(new_filename + ext))): - new_filename = "%s (%d)" % (tmp_filename, i) - i += 1 - new_filename = new_filename + ext - log.debug("Moving file %r => %r", old_filename, new_filename) - shutil.move(encode_filename(old_filename), encode_filename(new_filename)) - return new_filename - else: + + if old_filename == new_filename + ext: return old_filename + new_dirname = os.path.dirname(new_filename) + if not os.path.isdir(encode_filename(new_dirname)): + os.makedirs(new_dirname) + tmp_filename = new_filename + i = 1 + while (not pathcmp(old_filename, new_filename + ext) and + os.path.exists(encode_filename(new_filename + ext))): + new_filename = "%s (%d)" % (tmp_filename, i) + i += 1 + new_filename = new_filename + ext + log.debug("Moving file %r => %r", old_filename, new_filename) + shutil.move(encode_filename(old_filename), encode_filename(new_filename)) + return new_filename + def _make_image_filename(self, image_filename, dirname, metadata): image_filename = self._script_to_filename(image_filename, metadata) if not image_filename: From 5c6c7c7e3de07441ee10d20da69475dc0a5ff496 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Wed, 3 Jul 2013 22:56:59 +0200 Subject: [PATCH 65/75] Fix up format error introduced c6494fa18f941e578a2985af1c916b411a5a13c3 --- picard/config.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/picard/config.py b/picard/config.py index dd0cf6d57..4fbcc76ff 100644 --- a/picard/config.py +++ b/picard/config.py @@ -109,8 +109,8 @@ class Config(QtCore.QSettings): return if self._version >= PICARD_VERSION: if self._version > PICARD_VERSION: - print("Warning: config file %r was created by a more recent " - "version of Picard (current is %r)" % ( + print("Warning: config file %s was created by a more recent " + "version of Picard (current is %s)" % ( version_to_string(self._version), version_to_string(PICARD_VERSION) )) @@ -123,9 +123,11 @@ class Config(QtCore.QSettings): hook['func'](*hook['args']) except Exception as e: raise ConfigUpgradeError( - "Error during config upgrade from version %d to %d " + "Error during config upgrade from version %s to %s " "using %s(): %s" % ( - self._version, hook['to'], hook['func'].__name__, e + version_to_string(self._version), + version_to_string(hook['to']), + hook['func'].__name__, e )) else: hook['done'] = True From 3608612bd61f5b28e4dc6163aa3860acc4d84eab Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Thu, 4 Jul 2013 20:54:04 +0200 Subject: [PATCH 66/75] logview: remove log receiver when dialog is closed --- picard/log.py | 29 +++++++++++++++++------------ picard/ui/logview.py | 8 ++++++-- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/picard/log.py b/picard/log.py index 2dd353cb3..fcff0ebaf 100644 --- a/picard/log.py +++ b/picard/log.py @@ -23,12 +23,17 @@ from PyQt4 import QtCore from picard.util import thread -def _stderr_receiver(prefix, time, msg): - sys.stderr.write("%s %s %s %s%s" % (prefix, str(QtCore.QThread.currentThreadId()), time, msg, os.linesep)) - - _entries = [] -_receivers = [_stderr_receiver] +_receivers = [] +_log_debug_messages = False + + +def register_receiver(receiver): + _receivers.append(receiver) + + +def unregister_receiver(receiver): + _receivers.remove(receiver) def _message(prefix, message, args, kwargs): @@ -52,13 +57,6 @@ def _message(prefix, message, args, kwargs): traceback.print_exc() -def add_receiver(receiver): - _receivers.append(receiver) - - -_log_debug_messages = False - - def debug(message, *args, **kwargs): if _log_debug_messages: thread.proxy_to_main(_message, "D:", message, args, kwargs) @@ -74,3 +72,10 @@ def warning(message, *args, **kwargs): def error(message, *args, **kwargs): thread.proxy_to_main(_message, "E:", message, args, kwargs) + + +def _stderr_receiver(prefix, time, msg): + sys.stderr.write("%s %s %s %s%s" % (prefix, str(QtCore.QThread.currentThreadId()), time, msg, os.linesep)) + + +register_receiver(_stderr_receiver) diff --git a/picard/ui/logview.py b/picard/ui/logview.py index f8ee38f20..b9ce7236c 100644 --- a/picard/ui/logview.py +++ b/picard/ui/logview.py @@ -38,9 +38,9 @@ class LogView(QtGui.QDialog): self.browser.setDocument(self.doc) vbox = QtGui.QHBoxLayout(self) vbox.addWidget(self.browser) - for prefix, time, msg in log._entries: + for level, time, msg in log._entries: self.add_entry(prefix, time, msg) - log.add_receiver(self.add_entry) + log.register_receiver(self.add_entry) def add_entry(self, prefix, time, msg): self.textCursor.movePosition(QtGui.QTextCursor.End) @@ -48,3 +48,7 @@ class LogView(QtGui.QDialog): self.textCursor.insertBlock() sb = self.browser.verticalScrollBar() sb.setValue(sb.maximum()) + + def closeEvent(self, event): + log.unregister_receiver(self.add_entry) + return QtGui.QDialog.closeEvent(self, event) From b55976002d31468ceb4da23859c1c032c4a98d72 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Thu, 4 Jul 2013 20:55:34 +0200 Subject: [PATCH 67/75] logview: limit number of lines stored in memory to 50000 --- picard/log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/picard/log.py b/picard/log.py index fcff0ebaf..815c3cf34 100644 --- a/picard/log.py +++ b/picard/log.py @@ -19,11 +19,12 @@ import sys import os +from collections import deque from PyQt4 import QtCore from picard.util import thread -_entries = [] +_entries = deque(maxlen=50000) _receivers = [] _log_debug_messages = False From ea2a95eec409312aaef140b41cf8fd06add16353 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Thu, 4 Jul 2013 20:57:49 +0200 Subject: [PATCH 68/75] log, logview: use levels instead of prefixes and colorize log view output --- picard/log.py | 36 ++++++++++++++++++++++++------------ picard/ui/logview.py | 29 +++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/picard/log.py b/picard/log.py index 815c3cf34..764368d8c 100644 --- a/picard/log.py +++ b/picard/log.py @@ -24,6 +24,11 @@ from PyQt4 import QtCore from picard.util import thread +LOG_INFO = 1 +LOG_WARNING = 2 +LOG_ERROR = 4 +LOG_DEBUG = 8 + _entries = deque(maxlen=50000) _receivers = [] _log_debug_messages = False @@ -37,22 +42,19 @@ def unregister_receiver(receiver): _receivers.remove(receiver) -def _message(prefix, message, args, kwargs): +def _message(level, message, args, kwargs): if not (isinstance(message, str) or isinstance(message, unicode)): message = repr(message) if args: message = message % args - prefix = "%s" % (prefix,) time = str(QtCore.QTime.currentTime().toString()) message = "%s" % (message,) - if isinstance(prefix, unicode): - prefix = prefix.encode("utf-8", "replace") if isinstance(message, unicode): message = message.encode("utf-8", "replace") - _entries.append((prefix, time, message)) + _entries.append((level, time, message)) for func in _receivers: try: - func(prefix, time, message) + func(level, time, message) except Exception, e: import traceback traceback.print_exc() @@ -60,23 +62,33 @@ def _message(prefix, message, args, kwargs): def debug(message, *args, **kwargs): if _log_debug_messages: - thread.proxy_to_main(_message, "D:", message, args, kwargs) + thread.proxy_to_main(_message, LOG_DEBUG, message, args, kwargs) def info(message, *args, **kwargs): - thread.proxy_to_main(_message, "I:", message, args, kwargs) + thread.proxy_to_main(_message, LOG_INFO, message, args, kwargs) def warning(message, *args, **kwargs): - thread.proxy_to_main(_message, "W:", message, args, kwargs) + thread.proxy_to_main(_message, LOG_WARNING, message, args, kwargs) def error(message, *args, **kwargs): - thread.proxy_to_main(_message, "E:", message, args, kwargs) + thread.proxy_to_main(_message, LOG_ERROR, message, args, kwargs) -def _stderr_receiver(prefix, time, msg): - sys.stderr.write("%s %s %s %s%s" % (prefix, str(QtCore.QThread.currentThreadId()), time, msg, os.linesep)) +_log_prefixes = { + LOG_INFO: 'I:', + LOG_WARNING: 'W:', + LOG_ERROR: 'E:', + LOG_DEBUG: 'D:', +} + + +def _stderr_receiver(level, time, msg): + sys.stderr.write("%s %s %s %s%s" % (_log_prefixes[level], + str(QtCore.QThread.currentThreadId()), + time, msg, os.linesep)) register_receiver(_stderr_receiver) diff --git a/picard/ui/logview.py b/picard/ui/logview.py index b9ce7236c..8ab98d578 100644 --- a/picard/ui/logview.py +++ b/picard/ui/logview.py @@ -26,25 +26,42 @@ class LogView(QtGui.QDialog): def __init__(self, parent=None): QtGui.QDialog.__init__(self, parent) - self.resize(540, 340) + self.resize(740, 340) self.setWindowTitle(_("Log")) self.doc = QtGui.QTextDocument(self) self.textCursor = QtGui.QTextCursor(self.doc) font = QtGui.QFont() font.setFamily("Monospace") - self.textFormat = QtGui.QTextCharFormat() - self.textFormat.setFont(font) + self.textFormatInfo = QtGui.QTextCharFormat() + self.textFormatInfo.setFont(font) + self.textFormatInfo.setForeground(QtGui.QColor('black')) + self.textFormatDebug = QtGui.QTextCharFormat() + self.textFormatDebug.setFont(font) + self.textFormatDebug.setForeground(QtGui.QColor('purple')) + self.textFormatWarning = QtGui.QTextCharFormat() + self.textFormatWarning.setFont(font) + self.textFormatWarning.setForeground(QtGui.QColor('darkorange')) + self.textFormatError = QtGui.QTextCharFormat() + self.textFormatError.setFont(font) + self.textFormatError.setForeground(QtGui.QColor('red')) + self.formats = { + log.LOG_INFO: self.textFormatInfo, + log.LOG_WARNING: self.textFormatWarning, + log.LOG_ERROR: self.textFormatError, + log.LOG_DEBUG: self.textFormatDebug, + } + self.browser = QtGui.QTextBrowser(self) self.browser.setDocument(self.doc) vbox = QtGui.QHBoxLayout(self) vbox.addWidget(self.browser) for level, time, msg in log._entries: - self.add_entry(prefix, time, msg) + self.add_entry(level, time, msg) log.register_receiver(self.add_entry) - def add_entry(self, prefix, time, msg): + def add_entry(self, level, time, msg): self.textCursor.movePosition(QtGui.QTextCursor.End) - self.textCursor.insertText(prefix + ' ' + str(QtCore.QThread.currentThreadId()) + ' ' + time + ' ' + msg, self.textFormat) + self.textCursor.insertText(time + ' ' + msg, self.formats[level]) self.textCursor.insertBlock() sb = self.browser.verticalScrollBar() sb.setValue(sb.maximum()) From be7965dd6f7362911b12a7155f3fe00ca99bbf9c Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Thu, 4 Jul 2013 20:59:07 +0200 Subject: [PATCH 69/75] log: use bitwise operators for log levels matching --- picard/log.py | 7 ++++--- picard/tagger.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/picard/log.py b/picard/log.py index 764368d8c..c0acf0df0 100644 --- a/picard/log.py +++ b/picard/log.py @@ -31,7 +31,7 @@ LOG_DEBUG = 8 _entries = deque(maxlen=50000) _receivers = [] -_log_debug_messages = False +log_levels = LOG_INFO|LOG_WARNING|LOG_ERROR def register_receiver(receiver): @@ -43,6 +43,8 @@ def unregister_receiver(receiver): def _message(level, message, args, kwargs): + if not log_levels & level: + return if not (isinstance(message, str) or isinstance(message, unicode)): message = repr(message) if args: @@ -61,8 +63,7 @@ def _message(level, message, args, kwargs): def debug(message, *args, **kwargs): - if _log_debug_messages: - thread.proxy_to_main(_message, LOG_DEBUG, message, args, kwargs) + thread.proxy_to_main(_message, LOG_DEBUG, message, args, kwargs) def info(message, *args, **kwargs): diff --git a/picard/tagger.py b/picard/tagger.py index ff30c0407..d562d36d9 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -111,7 +111,8 @@ class Tagger(QtGui.QApplication): self.stopping = False # Setup logging - log._log_debug_messages = debug or "PICARD_DEBUG" in os.environ + if debug or "PICARD_DEBUG" in os.environ: + log.log_levels = log.log_levels|log.LOG_DEBUG log.debug("Starting Picard %s from %r", picard.__version__, os.path.abspath(__file__)) # TODO remove this before the final release From f380be442e5b9ceb8fbc9f7f82b9d26394b27a03 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Thu, 4 Jul 2013 21:00:18 +0200 Subject: [PATCH 70/75] _entries -> entries as it is used outside log module --- picard/log.py | 4 ++-- picard/ui/logview.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/picard/log.py b/picard/log.py index c0acf0df0..4b7880a22 100644 --- a/picard/log.py +++ b/picard/log.py @@ -29,7 +29,7 @@ LOG_WARNING = 2 LOG_ERROR = 4 LOG_DEBUG = 8 -_entries = deque(maxlen=50000) +entries = deque(maxlen=50000) _receivers = [] log_levels = LOG_INFO|LOG_WARNING|LOG_ERROR @@ -53,7 +53,7 @@ def _message(level, message, args, kwargs): message = "%s" % (message,) if isinstance(message, unicode): message = message.encode("utf-8", "replace") - _entries.append((level, time, message)) + entries.append((level, time, message)) for func in _receivers: try: func(level, time, message) diff --git a/picard/ui/logview.py b/picard/ui/logview.py index 8ab98d578..a01186bc0 100644 --- a/picard/ui/logview.py +++ b/picard/ui/logview.py @@ -55,7 +55,7 @@ class LogView(QtGui.QDialog): self.browser.setDocument(self.doc) vbox = QtGui.QHBoxLayout(self) vbox.addWidget(self.browser) - for level, time, msg in log._entries: + for level, time, msg in log.entries: self.add_entry(level, time, msg) log.register_receiver(self.add_entry) From 6e9078e188cc8ee26cb635c9f5bf370ea31ac5b8 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Wed, 3 Jul 2013 17:11:24 +0200 Subject: [PATCH 71/75] logview: enable minimize/maximize for log dialog windows --- picard/ui/logview.py | 1 + 1 file changed, 1 insertion(+) diff --git a/picard/ui/logview.py b/picard/ui/logview.py index a01186bc0..00bc5a1d4 100644 --- a/picard/ui/logview.py +++ b/picard/ui/logview.py @@ -26,6 +26,7 @@ class LogView(QtGui.QDialog): def __init__(self, parent=None): QtGui.QDialog.__init__(self, parent) + self.setWindowFlags(QtCore.Qt.Window) self.resize(740, 340) self.setWindowTitle(_("Log")) self.doc = QtGui.QTextDocument(self) From c31ffe12f172df4411686d7f6ca38067c612f322 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Wed, 3 Jul 2013 17:22:56 +0200 Subject: [PATCH 72/75] logview: code cleanup --- picard/ui/logview.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/picard/ui/logview.py b/picard/ui/logview.py index 00bc5a1d4..4cdca5d95 100644 --- a/picard/ui/logview.py +++ b/picard/ui/logview.py @@ -31,6 +31,13 @@ class LogView(QtGui.QDialog): self.setWindowTitle(_("Log")) self.doc = QtGui.QTextDocument(self) self.textCursor = QtGui.QTextCursor(self.doc) + self.browser = QtGui.QTextBrowser(self) + self.browser.setDocument(self.doc) + vbox = QtGui.QHBoxLayout(self) + vbox.addWidget(self.browser) + self._display() + + def _display(self): font = QtGui.QFont() font.setFamily("Monospace") self.textFormatInfo = QtGui.QTextCharFormat() @@ -51,16 +58,11 @@ class LogView(QtGui.QDialog): log.LOG_ERROR: self.textFormatError, log.LOG_DEBUG: self.textFormatDebug, } - - self.browser = QtGui.QTextBrowser(self) - self.browser.setDocument(self.doc) - vbox = QtGui.QHBoxLayout(self) - vbox.addWidget(self.browser) for level, time, msg in log.entries: - self.add_entry(level, time, msg) - log.register_receiver(self.add_entry) + self._add_entry(level, time, msg) + log.register_receiver(self._add_entry) - def add_entry(self, level, time, msg): + def _add_entry(self, level, time, msg): self.textCursor.movePosition(QtGui.QTextCursor.End) self.textCursor.insertText(time + ' ' + msg, self.formats[level]) self.textCursor.insertBlock() @@ -68,5 +70,5 @@ class LogView(QtGui.QDialog): sb.setValue(sb.maximum()) def closeEvent(self, event): - log.unregister_receiver(self.add_entry) + log.unregister_receiver(self._add_entry) return QtGui.QDialog.closeEvent(self, event) From bc0f7d74abddab94517f187dab3260d2e10d27b6 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Wed, 3 Jul 2013 17:33:52 +0200 Subject: [PATCH 73/75] Let the receiver convert time to string. --- picard/log.py | 4 ++-- picard/ui/logview.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/picard/log.py b/picard/log.py index 4b7880a22..3d4d12045 100644 --- a/picard/log.py +++ b/picard/log.py @@ -49,7 +49,7 @@ def _message(level, message, args, kwargs): message = repr(message) if args: message = message % args - time = str(QtCore.QTime.currentTime().toString()) + time = QtCore.QTime.currentTime() message = "%s" % (message,) if isinstance(message, unicode): message = message.encode("utf-8", "replace") @@ -89,7 +89,7 @@ _log_prefixes = { def _stderr_receiver(level, time, msg): sys.stderr.write("%s %s %s %s%s" % (_log_prefixes[level], str(QtCore.QThread.currentThreadId()), - time, msg, os.linesep)) + time.toString('hh:mm:ss.zzz'), msg, os.linesep)) register_receiver(_stderr_receiver) diff --git a/picard/ui/logview.py b/picard/ui/logview.py index 4cdca5d95..df508f8db 100644 --- a/picard/ui/logview.py +++ b/picard/ui/logview.py @@ -64,7 +64,7 @@ class LogView(QtGui.QDialog): def _add_entry(self, level, time, msg): self.textCursor.movePosition(QtGui.QTextCursor.End) - self.textCursor.insertText(time + ' ' + msg, self.formats[level]) + self.textCursor.insertText(time.toString() + ' ' + msg, self.formats[level]) self.textCursor.insertBlock() sb = self.browser.verticalScrollBar() sb.setValue(sb.maximum()) From 2fd2487373e0967d17149729e0e88638f3461ffb Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Wed, 3 Jul 2013 17:38:20 +0200 Subject: [PATCH 74/75] _stderr_receiver(): include ':' in format string instead of each dict entry --- picard/log.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/picard/log.py b/picard/log.py index 3d4d12045..b36653d41 100644 --- a/picard/log.py +++ b/picard/log.py @@ -79,15 +79,15 @@ def error(message, *args, **kwargs): _log_prefixes = { - LOG_INFO: 'I:', - LOG_WARNING: 'W:', - LOG_ERROR: 'E:', - LOG_DEBUG: 'D:', + LOG_INFO: 'I', + LOG_WARNING: 'W', + LOG_ERROR: 'E', + LOG_DEBUG: 'D', } def _stderr_receiver(level, time, msg): - sys.stderr.write("%s %s %s %s%s" % (_log_prefixes[level], + sys.stderr.write("%s: %s %s %s%s" % (_log_prefixes[level], str(QtCore.QThread.currentThreadId()), time.toString('hh:mm:ss.zzz'), msg, os.linesep)) From f10892ca726438f21b5a9bf96386afd6ae57e209 Mon Sep 17 00:00:00 2001 From: Laurent Monin <zas@norz.org> Date: Thu, 4 Jul 2013 20:38:34 +0200 Subject: [PATCH 75/75] Drop currentThreadId in stderr output --- picard/log.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/picard/log.py b/picard/log.py index b36653d41..111709fb9 100644 --- a/picard/log.py +++ b/picard/log.py @@ -87,9 +87,9 @@ _log_prefixes = { def _stderr_receiver(level, time, msg): - sys.stderr.write("%s: %s %s %s%s" % (_log_prefixes[level], - str(QtCore.QThread.currentThreadId()), - time.toString('hh:mm:ss.zzz'), msg, os.linesep)) + sys.stderr.write("%s: %s %s%s" % (_log_prefixes[level], + time.toString('hh:mm:ss'), msg, + os.linesep)) register_receiver(_stderr_receiver)