From fe2cc4d4d2b66eb73ea9858b47bc40eb40613a02 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 13 Jun 2011 12:31:16 -0500 Subject: [PATCH 01/79] Some compatibility fixes for the lastfm plugin, and probably others. (The changes were unintentional.) --- picard/album.py | 3 +++ picard/track.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/picard/album.py b/picard/album.py index 9679ec3f6..eab496d5f 100644 --- a/picard/album.py +++ b/picard/album.py @@ -421,3 +421,6 @@ class NatAlbum(Album): for file in track.linked_files: track.update_file_metadata(file) super(NatAlbum, self).update(update_tracks) + + def _finalize_loading(self, error): + self.update() diff --git a/picard/track.py b/picard/track.py index ca6c55e0c..eec22e6e4 100644 --- a/picard/track.py +++ b/picard/track.py @@ -156,7 +156,7 @@ class Track(DataObject): # Track metadata plugins try: - run_track_metadata_processors(self, tm, release, node) + run_track_metadata_processors(self.album, tm, release, node) except: self.log.error(traceback.format_exc()) From 830e445bfed4edbb8b4d15c10e79619ae475434a Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 14 Jun 2011 02:50:55 -0500 Subject: [PATCH 02/79] "format" tag mistakenly renamed to "media" --- picard/album.py | 4 ++-- picard/ui/itemviews.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/picard/album.py b/picard/album.py index eab496d5f..0e6559b91 100644 --- a/picard/album.py +++ b/picard/album.py @@ -131,7 +131,7 @@ class Album(DataObject, Item): tm['discnumber'] = discnumber tm['discsubtitle'] = discsubtitle tm['totaltracks'] = totaltracks - if format: tm['media'] = format + if format: tm['format'] = format track_to_metadata(node, config=self.config, track=t) t._customize_metadata(node, release_node, script, parser, ignore_tags) @@ -169,7 +169,7 @@ class Album(DataObject, Item): if f in formats: formats[f] += 1 else: formats[f] = 1 if formats: - version["media"] = " + ".join(["%s%s" % (str(j)+u"×" if j>1 else "", RELEASE_FORMATS[i]) + version["format"] = " + ".join(["%s%s" % (str(j)+u"×" if j>1 else "", RELEASE_FORMATS[i]) for i, j in formats.items()]) self.other_versions.append(version) self.other_versions.sort(key=lambda x: x["date"]) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 553693601..9880eb31d 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -379,8 +379,8 @@ class BaseTreeView(QtGui.QTreeWidget): if "country" in version: try: name.append(RELEASE_COUNTRIES[version["country"]]) except KeyError: name.append(version["country"]) - if "media" in version: - name.append(version["media"]) + if "format" in version: + name.append(version["format"]) version_name = " / ".join(name).replace('&', '&&') action = releases_menu.addAction(version_name or _('[no release info]')) action.setData(QtCore.QVariant(i)) From e1c8981fefde1bd1439f3ebdaad930ce450e9273 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Fri, 17 Jun 2011 01:17:27 -0500 Subject: [PATCH 03/79] Fix error when POPM frame has no count attribute. --- picard/formats/id3.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index c2dbe59b2..ad95f0f24 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -268,11 +268,12 @@ class ID3File(File): # Search for an existing POPM frame to get the current playcount for frame in tags.values(): if frame.FrameID == 'POPM' and frame.email == settings['rating_user_email']: - count = frame.count + try: count = frame.count + except AttributeError: count = 0 break else: count = 0 - + # Convert rating to range between 0 and 255 rating = int(values[0]) * 255 / (settings['rating_steps'] - 1) tags.add(id3.POPM(email=settings['rating_user_email'], rating=rating, count=count)) From e5ee0f817ce2bb78f0cd41a530de993039194a3c Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Fri, 17 Jun 2011 14:39:13 -0500 Subject: [PATCH 04/79] Slightly more compact way to fix the POPM issue --- picard/formats/id3.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index ad95f0f24..d1ea27dfd 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -268,8 +268,7 @@ class ID3File(File): # Search for an existing POPM frame to get the current playcount for frame in tags.values(): if frame.FrameID == 'POPM' and frame.email == settings['rating_user_email']: - try: count = frame.count - except AttributeError: count = 0 + count = getattr(frame, 'count', 0) break else: count = 0 From 29e6d0d0e96a505980a4c591272d462d9202e017 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Fri, 17 Jun 2011 17:20:46 -0500 Subject: [PATCH 05/79] Bring back the preferred release country option. --- picard/cluster.py | 12 +++++++- picard/file.py | 11 ++++++++ picard/ui/options/metadata.py | 10 +++++++ picard/ui/ui_options_metadata.py | 47 ++++++++++++++++++++------------ ui/options_metadata.ui | 27 ++++++++++++++++-- 5 files changed, 87 insertions(+), 20 deletions(-) diff --git a/picard/cluster.py b/picard/cluster.py index 1468d88f9..ba70f0724 100644 --- a/picard/cluster.py +++ b/picard/cluster.py @@ -43,7 +43,7 @@ class Cluster(QtCore.QObject, Item): self.lookup_queued = False # Weights for different elements when comparing a cluster to a release - self.comparison_weights = { 'title' : 17, 'artist' : 6, 'totaltracks' : 5 } + self.comparison_weights = { 'title' : 17, 'artist' : 6, 'totaltracks' : 5, 'country': 4 } def __repr__(self): return '' % self.metadata['album'] @@ -132,6 +132,7 @@ class Cluster(QtCore.QObject, Item): * title = 17 * artist name = 6 * number of tracks = 5 + * release country = 4 TODO: * prioritize official albums over compilations (optional?) @@ -156,6 +157,15 @@ class Cluster(QtCore.QObject, Item): score = 1.0 total += score * self.comparison_weights['totaltracks'] + preferred_country = self.config.setting["preferred_release_country"] + + if preferred_country: + if "country" in release.children and preferred_country == release.country[0].text: + score = 1.0 + else: + score = 0.0 + total += score * self.comparison_weights["country"] + return total / sum(self.comparison_weights.values()) def _lookup_finished(self, document, http, error): diff --git a/picard/file.py b/picard/file.py index f993794c1..4b3201b40 100644 --- a/picard/file.py +++ b/picard/file.py @@ -440,6 +440,7 @@ class File(LockableObject, Item): * length = 10 * number of tracks = 4 * album type = 20 + * release country = 4 """ total = 0.0 @@ -478,6 +479,8 @@ class File(LockableObject, Item): if not releases: return (reduce(lambda x, y: x + y[0] * y[1] / total, parts, 0.0), None) + preferred_country = self.config.setting["preferred_release_country"] + for release in releases: total_ = total parts_ = list(parts) @@ -487,6 +490,14 @@ class File(LockableObject, Item): parts_.append((similarity2(album, b), 5)) total_ += 5 + if preferred_country: + total_ += 4 + if "country" in release.children and preferred_country == release.country[0].text: + score = 1.0 + else: + score = 0.0 + parts_.append((score, 4)) + track_list = release.medium_list[0].medium[0].track_list[0] if totaltracks and 'count' in track_list.attribs: try: diff --git a/picard/ui/options/metadata.py b/picard/ui/options/metadata.py index 31120ff30..7218dc71f 100644 --- a/picard/ui/options/metadata.py +++ b/picard/ui/options/metadata.py @@ -21,6 +21,7 @@ from PyQt4 import QtCore, QtGui from picard.config import BoolOption, TextOption from picard.ui.options import OptionsPage, OptionsCheckError, register_options_page from picard.ui.ui_options_metadata import Ui_MetadataOptionsPage +from picard.const import RELEASE_COUNTRIES import operator import locale @@ -40,6 +41,7 @@ class MetadataOptionsPage(OptionsPage): BoolOption("setting", "release_ars", True), BoolOption("setting", "track_ars", False), BoolOption("setting", "folksonomy_tags", False), + TextOption("setting", "preferred_release_country", u""), BoolOption("setting", "convert_punctuation", False), BoolOption("setting", "standardize_tracks", False), BoolOption("setting", "standardize_releases", False), @@ -53,12 +55,19 @@ class MetadataOptionsPage(OptionsPage): self.connect(self.ui.va_name_default, QtCore.SIGNAL("clicked()"), self.set_va_name_default) self.connect(self.ui.nat_name_default, QtCore.SIGNAL("clicked()"), self.set_nat_name_default) + self.ui.preferred_release_country.addItem(_("None"), QtCore.QVariant("")) + country_list = [(c[0], _(c[1])) for c in RELEASE_COUNTRIES.items()] + for country, name in sorted(country_list, key=operator.itemgetter(1), cmp=locale.strcoll): + self.ui.preferred_release_country.addItem(name, QtCore.QVariant(country)) + def load(self): self.ui.translate_artist_names.setChecked(self.config.setting["translate_artist_names"]) self.ui.convert_punctuation.setChecked(self.config.setting["convert_punctuation"]) self.ui.release_ars.setChecked(self.config.setting["release_ars"]) self.ui.track_ars.setChecked(self.config.setting["track_ars"]) self.ui.folksonomy_tags.setChecked(self.config.setting["folksonomy_tags"]) + current_release_country = QtCore.QVariant(self.config.setting["preferred_release_country"]) + self.ui.preferred_release_country.setCurrentIndex(self.ui.preferred_release_country.findData(current_release_country)) self.ui.va_name.setText(self.config.setting["va_name"]) self.ui.nat_name.setText(self.config.setting["nat_name"]) self.ui.standardize_tracks.setChecked(self.config.setting["standardize_tracks"]) @@ -71,6 +80,7 @@ class MetadataOptionsPage(OptionsPage): self.config.setting["release_ars"] = self.ui.release_ars.isChecked() self.config.setting["track_ars"] = self.ui.track_ars.isChecked() self.config.setting["folksonomy_tags"] = self.ui.folksonomy_tags.isChecked() + self.config.setting["preferred_release_country"] = self.ui.preferred_release_country.itemData(self.ui.preferred_release_country.currentIndex()).toString() self.config.setting["va_name"] = self.ui.va_name.text() self.config.setting["nat_name"] = self.ui.nat_name.text() self.config.setting["standardize_tracks"] = self.ui.standardize_tracks.isChecked() diff --git a/picard/ui/ui_options_metadata.py b/picard/ui/ui_options_metadata.py index 267b57b00..c9ef2c9a0 100644 --- a/picard/ui/ui_options_metadata.py +++ b/picard/ui/ui_options_metadata.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'ui/options_metadata.ui' # -# Created: Sun Jun 5 13:56:33 2011 +# Created: Fri Jun 17 15:22:22 2011 # by: PyQt4 UI code generator 4.8.4 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,7 @@ from PyQt4 import QtCore, QtGui class Ui_MetadataOptionsPage(object): def setupUi(self, MetadataOptionsPage): MetadataOptionsPage.setObjectName("MetadataOptionsPage") - MetadataOptionsPage.resize(423, 507) + MetadataOptionsPage.resize(423, 553) self.verticalLayout = QtGui.QVBoxLayout(MetadataOptionsPage) self.verticalLayout.setContentsMargins(-1, 0, -1, -1) self.verticalLayout.setObjectName("verticalLayout") @@ -49,6 +49,18 @@ class Ui_MetadataOptionsPage(object): self.folksonomy_tags = QtGui.QCheckBox(self.rename_files) self.folksonomy_tags.setObjectName("folksonomy_tags") self.verticalLayout_3.addWidget(self.folksonomy_tags) + self.label_8 = QtGui.QLabel(self.rename_files) + self.label_8.setObjectName("label_8") + self.verticalLayout_3.addWidget(self.label_8) + self.preferred_release_country = QtGui.QComboBox(self.rename_files) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.preferred_release_country.sizePolicy().hasHeightForWidth()) + self.preferred_release_country.setSizePolicy(sizePolicy) + self.preferred_release_country.setMaximumSize(QtCore.QSize(373, 16777215)) + self.preferred_release_country.setObjectName("preferred_release_country") + self.verticalLayout_3.addWidget(self.preferred_release_country) self.verticalLayout_2.addWidget(self.rename_files) self.rename_files_2 = QtGui.QGroupBox(MetadataOptionsPage) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Maximum) @@ -59,13 +71,13 @@ class Ui_MetadataOptionsPage(object): self.rename_files_2.setMinimumSize(QtCore.QSize(397, 144)) self.rename_files_2.setFlat(False) self.rename_files_2.setObjectName("rename_files_2") - self.widget = QtGui.QWidget(self.rename_files_2) - self.widget.setGeometry(QtCore.QRect(16, 31, 301, 101)) - self.widget.setObjectName("widget") - self.gridLayout = QtGui.QGridLayout(self.widget) + self.layoutWidget = QtGui.QWidget(self.rename_files_2) + self.layoutWidget.setGeometry(QtCore.QRect(16, 31, 301, 101)) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout = QtGui.QGridLayout(self.layoutWidget) self.gridLayout.setMargin(0) self.gridLayout.setObjectName("gridLayout") - self.label = QtGui.QLabel(self.widget) + self.label = QtGui.QLabel(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -78,7 +90,7 @@ class Ui_MetadataOptionsPage(object): self.label.setAlignment(QtCore.Qt.AlignCenter) self.label.setObjectName("label") self.gridLayout.addWidget(self.label, 0, 1, 1, 3) - self.label_2 = QtGui.QLabel(self.widget) + self.label_2 = QtGui.QLabel(self.layoutWidget) font = QtGui.QFont() font.setWeight(75) font.setBold(True) @@ -86,20 +98,20 @@ class Ui_MetadataOptionsPage(object): self.label_2.setAlignment(QtCore.Qt.AlignCenter) self.label_2.setObjectName("label_2") self.gridLayout.addWidget(self.label_2, 0, 4, 1, 3) - self.label_3 = QtGui.QLabel(self.widget) + self.label_3 = QtGui.QLabel(self.layoutWidget) self.label_3.setLayoutDirection(QtCore.Qt.LeftToRight) self.label_3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.label_3.setObjectName("label_3") self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1) - self.label_4 = QtGui.QLabel(self.widget) + self.label_4 = QtGui.QLabel(self.layoutWidget) self.label_4.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.label_4.setObjectName("label_4") self.gridLayout.addWidget(self.label_4, 2, 0, 1, 1) - self.label_5 = QtGui.QLabel(self.widget) + self.label_5 = QtGui.QLabel(self.layoutWidget) self.label_5.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.label_5.setObjectName("label_5") self.gridLayout.addWidget(self.label_5, 3, 0, 1, 1) - self.standardize_tracks = QtGui.QRadioButton(self.widget) + self.standardize_tracks = QtGui.QRadioButton(self.layoutWidget) self.standardize_tracks.setEnabled(True) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) @@ -113,7 +125,7 @@ class Ui_MetadataOptionsPage(object): self.buttonGroup.setObjectName("buttonGroup") self.buttonGroup.addButton(self.standardize_tracks) self.gridLayout.addWidget(self.standardize_tracks, 1, 5, 1, 1) - self.standardize_releases = QtGui.QRadioButton(self.widget) + self.standardize_releases = QtGui.QRadioButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -126,7 +138,7 @@ class Ui_MetadataOptionsPage(object): self.buttonGroup_2.setObjectName("buttonGroup_2") self.buttonGroup_2.addButton(self.standardize_releases) self.gridLayout.addWidget(self.standardize_releases, 2, 5, 1, 1) - self.standardize_artists = QtGui.QRadioButton(self.widget) + self.standardize_artists = QtGui.QRadioButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -139,7 +151,7 @@ class Ui_MetadataOptionsPage(object): self.buttonGroup_3.setObjectName("buttonGroup_3") self.buttonGroup_3.addButton(self.standardize_artists) self.gridLayout.addWidget(self.standardize_artists, 3, 5, 1, 1) - self.tracks_on_cover = QtGui.QRadioButton(self.widget) + self.tracks_on_cover = QtGui.QRadioButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -153,7 +165,7 @@ class Ui_MetadataOptionsPage(object): self.tracks_on_cover.setObjectName("tracks_on_cover") self.buttonGroup.addButton(self.tracks_on_cover) self.gridLayout.addWidget(self.tracks_on_cover, 1, 2, 1, 1) - self.releases_on_cover = QtGui.QRadioButton(self.widget) + self.releases_on_cover = QtGui.QRadioButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -165,7 +177,7 @@ class Ui_MetadataOptionsPage(object): self.releases_on_cover.setObjectName("releases_on_cover") self.buttonGroup_2.addButton(self.releases_on_cover) self.gridLayout.addWidget(self.releases_on_cover, 2, 2, 1, 1) - self.artists_on_cover = QtGui.QRadioButton(self.widget) + self.artists_on_cover = QtGui.QRadioButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -253,6 +265,7 @@ class Ui_MetadataOptionsPage(object): self.release_ars.setText(_("Use release relationships")) self.track_ars.setText(_("Use track relationships")) self.folksonomy_tags.setText(_("Use folksonomy tags as genre")) + self.label_8.setText(_("Preferred release country:")) self.rename_files_2.setTitle(_("Standardization")) self.label.setText(_("As on cover")) self.label_2.setText(_("Standardized")) diff --git a/ui/options_metadata.ui b/ui/options_metadata.ui index 650a8f220..f994f1ff2 100644 --- a/ui/options_metadata.ui +++ b/ui/options_metadata.ui @@ -7,7 +7,7 @@ 0 0 423 - 507 + 553 @@ -86,6 +86,29 @@ + + + + Preferred release country: + + + + + + + + 0 + 0 + + + + + 373 + 16777215 + + + + @@ -109,7 +132,7 @@ false - + 16 From aac98e0e7777141abfc866ceba23381393e1c10a Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sat, 18 Jun 2011 00:47:54 -0500 Subject: [PATCH 06/79] If files/clusters are removed, also remove them from the lookup queue. --- picard/file.py | 3 +++ picard/tagger.py | 6 +++++- picard/util/queue.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/picard/file.py b/picard/file.py index 4b3201b40..5b44283b2 100644 --- a/picard/file.py +++ b/picard/file.py @@ -530,6 +530,9 @@ class File(LockableObject, Item): def _lookup_finished(self, lookuptype, document, http, error): self._signal_lookup_finished() + if self.state == File.REMOVED: + return + try: m = document.metadata[0] if lookuptype == "metadata": diff --git a/picard/tagger.py b/picard/tagger.py index 96d3acf01..fb2346572 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -498,6 +498,7 @@ class Tagger(QtGui.QApplication): """Remove files from the tagger.""" for file in files: if self.files.has_key(file.filename): + self.lookup_queue.remove(file) self.analyze_queue.remove(file.filename) del self.files[file.filename] file.remove(from_parent) @@ -513,7 +514,10 @@ class Tagger(QtGui.QApplication): """Remove the specified cluster.""" if not cluster.special: self.log.debug("Removing %r", cluster) - self.remove_files(cluster.files, from_parent=False) + files = list(cluster.files) + cluster.files = [] + self.lookup_queue.remove(cluster) + self.remove_files(files, from_parent=False) self.clusters.remove(cluster) self.emit(QtCore.SIGNAL("cluster_removed"), cluster) diff --git a/picard/util/queue.py b/picard/util/queue.py index 0c2f215ed..2efa04152 100644 --- a/picard/util/queue.py +++ b/picard/util/queue.py @@ -50,7 +50,7 @@ class Queue: self.not_empty.wakeOne() finally: self.mutex.unlock() - + def remove(self,item): """Remove an item into the queue.""" self.mutex.lock() From 6fa4d20c781cae3a8ae54dd8aa1a701262f4b211 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 20 Jun 2011 17:19:40 -0500 Subject: [PATCH 07/79] Display label, catalog number, and barcode info in the CD lookup dialog. --- picard/mbxml.py | 20 +++++++++++--------- picard/ui/cdlookup.py | 12 ++++++++++-- picard/ui/ui_cdlookup.py | 2 +- ui/cdlookup.ui | 4 ++-- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index b00146cf0..e0873ba00 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -130,6 +130,16 @@ def artist_credit_to_metadata(node, m=None, release=None, config=None): _set_artist_item(m, release, 'albumartistsort', 'artistsort', artistsort) +def label_info_from_node(node): + labels = [] + catalog_numbers = [] + for label_info in node.label_info: + if 'label' in label_info.children: + labels.append(label_info.label[0].name[0].text) + if 'catalog_number' in label_info.children: + catalog_numbers.append(label_info.catalog_number[0].text) + return (labels, catalog_numbers) + def track_to_metadata(node, track, config=None): m = track.metadata recording_to_metadata(node.recording[0], track, config) @@ -206,15 +216,7 @@ def release_to_metadata(node, m, config=None, album=None): elif name == 'relation_list': _relations_to_metadata(nodes, m, config) elif name == 'label_info_list' and nodes[0].count != '0': - labels = [] - catalog_numbers = [] - for label_info in nodes[0].label_info: - if 'label' in label_info.children: - labels.append(label_info.label[0].name[0].text) - if 'catalog_number' in label_info.children: - catalog_numbers.append(label_info.catalog_number[0].text) - m['label'] = labels - m['catalognumber'] = catalog_numbers + m['label'], m['catalognumber'] = label_info_from_node(nodes[0]) elif name == 'text_representation': if 'language' in nodes[0].children: m['language'] = nodes[0].language[0].text diff --git a/picard/ui/cdlookup.py b/picard/ui/cdlookup.py index 7eb155e80..3510ad612 100644 --- a/picard/ui/cdlookup.py +++ b/picard/ui/cdlookup.py @@ -19,7 +19,7 @@ from PyQt4 import QtCore, QtGui from picard.ui.ui_cdlookup import Ui_Dialog -from picard.mbxml import artist_credit_from_node +from picard.mbxml import artist_credit_from_node, label_info_from_node class CDLookupDialog(QtGui.QDialog): @@ -29,16 +29,24 @@ class CDLookupDialog(QtGui.QDialog): self.disc = disc self.ui = Ui_Dialog() self.ui.setupUi(self) - self.ui.release_list.setHeaderLabels([_(u"Album"), _(u"Artist")]) + self.ui.release_list.setHeaderLabels([_(u"Album"), _(u"Artist"), + _(u"Labels"), _(u"Catalog #s"), _(u"Barcode")]) if self.releases: for release in self.releases: + labels, catalog_numbers = label_info_from_node(release.label_info_list[0]) + barcode = release.barcode[0].text if "barcode" in release.children else "" item = QtGui.QTreeWidgetItem(self.ui.release_list) item.setText(0, release.title[0].text) item.setText(1, artist_credit_from_node(release.artist_credit[0], self.config)[0]) + item.setText(2, ", ".join(labels)) + item.setText(3, ", ".join(catalog_numbers)) + item.setText(4, barcode) item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(release.id)) self.ui.release_list.setCurrentItem(self.ui.release_list.topLevelItem(0)) self.ui.ok_button.setEnabled(True) self.ui.release_list.resizeColumnToContents(0) + self.ui.release_list.resizeColumnToContents(1) + self.ui.release_list.resizeColumnToContents(4) self.connect(self.ui.lookup_button, QtCore.SIGNAL("clicked()"), self.lookup) def accept(self): diff --git a/picard/ui/ui_cdlookup.py b/picard/ui/ui_cdlookup.py index 1d6385aec..acf666adb 100644 --- a/picard/ui/ui_cdlookup.py +++ b/picard/ui/ui_cdlookup.py @@ -12,7 +12,7 @@ from PyQt4 import QtCore, QtGui class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(QtCore.QSize(QtCore.QRect(0,0,385,232).size()).expandedTo(Dialog.minimumSizeHint())) + Dialog.resize(QtCore.QSize(QtCore.QRect(0,0,480,240).size()).expandedTo(Dialog.minimumSizeHint())) self.vboxlayout = QtGui.QVBoxLayout(Dialog) self.vboxlayout.setMargin(9) diff --git a/ui/cdlookup.ui b/ui/cdlookup.ui index a6fb55e3a..b4fdc8bbb 100644 --- a/ui/cdlookup.ui +++ b/ui/cdlookup.ui @@ -8,8 +8,8 @@ 0 0 - 385 - 232 + 480 + 240 From f2db1b33ce2fea2464b45763cff77463cfc39a32 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sat, 25 Jun 2011 20:24:13 -0500 Subject: [PATCH 08/79] Add the ability to install plugins while Picard is running. --- picard/plugin.py | 128 +++++++++++------ picard/tagger.py | 12 +- picard/ui/options/plugins.py | 83 ++++++++--- picard/ui/ui_options_plugins.py | 243 +++++++++++++++----------------- ui/options_plugins.ui | 39 ++--- 5 files changed, 294 insertions(+), 211 deletions(-) diff --git a/picard/plugin.py b/picard/plugin.py index 841bdea9c..a71cfb15d 100644 --- a/picard/plugin.py +++ b/picard/plugin.py @@ -20,15 +20,27 @@ from PyQt4 import QtCore import imp import os.path +import shutil import picard.plugins import traceback +_suffixes = [s[0] for s in imp.get_suffixes()] +_package_entries = ["__init__.py", "__init__.pyc", "__init__.pyo"] -def plugin_name_from_module(module): - name = module.__name__ - if name.startswith("picard.plugins"): - return name[15:] + +def plugin_name_from_path(path): + path = os.path.normpath(path) + file = os.path.basename(path) + if os.path.isdir(path): + for entry in _package_entries: + if os.path.isfile(os.path.join(path, entry)): + return file else: + if file in _package_entries: + return None + name, ext = os.path.splitext(file) + if ext in _suffixes: + return name return None @@ -41,6 +53,10 @@ class ExtensionPoint(QtCore.QObject): def register(self, module, item): if module.startswith("picard.plugins"): module = module[15:] + for i, (module_, item_) in enumerate(self.__items): + if module == module_: + self.__items[i] = (module, item) + return else: module = None self.__items.append((module, item)) @@ -56,15 +72,23 @@ class PluginWrapper(object): def __init__(self, module, plugindir): self.module = module + self.compatible = False self.dir = plugindir def __get_name(self): try: return self.module.PLUGIN_NAME except AttributeError: - return self.module.__name__ + return self.module_name name = property(__get_name) + def __get_module_name(self): + name = self.module.__name__ + if name.startswith("picard.plugins"): + name = name[15:] + return name + module_name = property(__get_module_name) + def __get_author(self): try: return self.module.PLUGIN_AUTHOR @@ -104,57 +128,67 @@ class PluginManager(QtCore.QObject): QtCore.QObject.__init__(self) self.plugins = [] - def load(self, plugindir): + def load_plugindir(self, plugindir): if not os.path.isdir(plugindir): self.log.debug("Plugin directory %r doesn't exist", plugindir) return - names = set() - suffixes = [s[0] for s in imp.get_suffixes()] - package_entries = ["__init__.py", "__init__.pyc", "__init__.pyo"] - for name in os.listdir(plugindir): - if name in package_entries: - continue - path = os.path.join(plugindir, name) - if os.path.isdir(path): - for entry in package_entries: - if os.path.isfile(os.path.join(path, entry)): - break - else: - continue - else: - name, suffix = os.path.splitext(name) - if suffix not in suffixes: - continue - if hasattr(picard.plugins, name): - self.log.debug("Plugin %r already loaded!", name) - else: + for path in [os.path.join(plugindir, file) for file in os.listdir(plugindir)]: + name = plugin_name_from_path(path) + if name: names.add(name) - for name in names: - self.log.debug("Loading plugin %r", name) - info = imp.find_module(name, [plugindir]) - try: - plugin_module = imp.load_module('picard.plugins.' + name, *info) - plugin = PluginWrapper(plugin_module, plugindir) - for version in list(plugin.api_versions): - found = False - for api_version in picard.api_versions: - if api_version.startswith(version): - setattr(picard.plugins, name, plugin_module) + self.load_plugin(name, plugindir) + + def load_plugin(self, name, plugindir): + self.log.debug("Loading plugin %r", name) + info = imp.find_module(name, [plugindir]) + plugin = None + try: + plugin_module = imp.load_module("picard.plugins." + name, *info) + plugin = PluginWrapper(plugin_module, plugindir) + for version in list(plugin.api_versions): + for api_version in picard.api_versions: + if api_version.startswith(version): + plugin.compatible = True + setattr(picard.plugins, name, plugin_module) + for i, p in enumerate(self.plugins): + if name == p.module_name: + self.plugins[i] = plugin + break + else: self.plugins.append(plugin) - found = True - break - if found: break else: - self.log.info("Plugin '%s' from '%s' is not compatible " - "with this version of Picard." % - (plugin.name, plugin.file)) - except: - self.log.error(traceback.format_exc()) - if info[0] is not None: - info[0].close() + continue + break + else: + self.log.info("Plugin '%s' from '%s' is not compatible" + " with this version of Picard." % (plugin.name, plugin.file)) + except: + self.log.error(traceback.format_exc()) + if info[0] is not None: + info[0].close() + return plugin + + def install_plugin(self, path, dest): + plugin_name = plugin_name_from_path(path) + plugin_dir = self.tagger.user_plugin_dir + if plugin_name: + try: + dest_exists = os.path.exists(dest) + same_file = os.path.samefile(path, dest) if dest_exists else False + if os.path.isfile(path) and not (dest_exists and same_file): + shutil.copy(path, dest) + elif os.path.isdir(path) and not same_file: + if dest_exists: + shutil.rmtree(dest) + shutil.copytree(path, dest) + plugin = self.load_plugin(plugin_name, plugin_dir) + if plugin is not None: + self.emit(QtCore.SIGNAL("plugin_installed"), plugin, False) + except OSError, IOError: + self.tagger.log.debug("Unable to copy %s to plugin folder %s" % (path, plugin_dir)) def enabled(self, name): return True diff --git a/picard/tagger.py b/picard/tagger.py index fb2346572..1eaa667c7 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -185,14 +185,14 @@ class Tagger(QtGui.QApplication): # Load plugins self.pluginmanager = PluginManager() - user_plugin_dir = os.path.join(self.userdir, "plugins") - if not os.path.exists(user_plugin_dir): - os.makedirs(user_plugin_dir) - self.pluginmanager.load(user_plugin_dir) + self.user_plugin_dir = os.path.join(self.userdir, "plugins") + if not os.path.exists(self.user_plugin_dir): + os.makedirs(self.user_plugin_dir) + self.pluginmanager.load_plugindir(self.user_plugin_dir) if hasattr(sys, "frozen"): - self.pluginmanager.load(os.path.join(os.path.dirname(sys.argv[0]), "plugins")) + self.pluginmanager.load_plugindir(os.path.join(os.path.dirname(sys.argv[0]), "plugins")) else: - self.pluginmanager.load(os.path.join(os.path.dirname(__file__), "plugins")) + self.pluginmanager.load_plugindir(os.path.join(os.path.dirname(__file__), "plugins")) self.puidmanager = PUIDManager() diff --git a/picard/ui/options/plugins.py b/picard/ui/options/plugins.py index 71512f397..ee0a02f0f 100644 --- a/picard/ui/options/plugins.py +++ b/picard/ui/options/plugins.py @@ -22,7 +22,6 @@ import os.path import sys from PyQt4 import QtCore, QtGui from picard.config import TextOption -from picard.plugin import plugin_name_from_module from picard.ui.options import OptionsPage, register_options_page from picard.ui.ui_options_plugins import Ui_PluginsOptionsPage @@ -47,40 +46,65 @@ class PluginsOptionsPage(OptionsPage): super(PluginsOptionsPage, self).__init__(parent) self.ui = Ui_PluginsOptionsPage() self.ui.setupUi(self) + self.items = {} self.connect(self.ui.plugins, QtCore.SIGNAL("itemSelectionChanged()"), self.change_details) - self.user_plugin_dir = os.path.join(self.tagger.userdir, "plugins") + self.ui.plugins.__class__.mimeTypes = self.mimeTypes + self.ui.plugins.__class__.dropEvent = self.dropEvent if sys.platform == "win32": - self.loader="file:///%s" + self.loader="file:///%s" else: self.loader="file://%s" + self.connect(self.ui.install_plugin, QtCore.SIGNAL("clicked()"), self.open_plugins) self.connect(self.ui.folder_open, QtCore.SIGNAL("clicked()"), self.open_plugin_dir) self.connect(self.ui.plugin_download, QtCore.SIGNAL("clicked()"), self.open_plugin_site) + self.connect(self.tagger.pluginmanager, QtCore.SIGNAL("plugin_installed"), self.plugin_installed) def load(self): plugins = sorted(self.tagger.pluginmanager.plugins, cmp=cmp_plugins) enabled_plugins = self.config.setting["enabled_plugins"].split() - self.items = {} firstitem = None for plugin in plugins: - item = QtGui.QTreeWidgetItem(self.ui.plugins) - item.setText(0, plugin.name) - if plugin_name_from_module(plugin.module) in enabled_plugins: - item.setCheckState(0, QtCore.Qt.Checked) - else: - item.setCheckState(0, QtCore.Qt.Unchecked) - item.setText(1, plugin.version) - item.setText(2, plugin.author) + enabled = plugin.module_name in enabled_plugins + item = self.add_plugin_item(plugin, enabled=enabled) if not firstitem: firstitem = item - self.items[item] = plugin - self.ui.plugins.header().resizeSections(QtGui.QHeaderView.ResizeToContents) self.ui.plugins.setCurrentItem(firstitem) + def plugin_installed(self, plugin): + if not plugin.compatible: + msgbox = QtGui.QMessageBox(self) + msgbox.setText(u"The plugin ‘%s’ is not compatible with this version of Picard." % plugin.name) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.setDefaultButton(QtGui.QMessageBox.Ok) + msgbox.exec_() + return + for i, p in self.items.items(): + if plugin.module_name == p.module_name: + enabled = i.checkState(0) == QtCore.Qt.Checked + self.add_plugin_item(plugin, enabled=enabled, item=i) + break + else: + self.add_plugin_item(plugin) + + def add_plugin_item(self, plugin, enabled=False, item=None): + if item is None: + item = QtGui.QTreeWidgetItem(self.ui.plugins) + item.setText(0, plugin.name) + if enabled: + item.setCheckState(0, QtCore.Qt.Checked) + else: + item.setCheckState(0, QtCore.Qt.Unchecked) + item.setText(1, plugin.version) + item.setText(2, plugin.author) + self.ui.plugins.header().resizeSections(QtGui.QHeaderView.ResizeToContents) + self.items[item] = plugin + return item + def save(self): enabled_plugins = [] for item, plugin in self.items.iteritems(): if item.checkState(0) == QtCore.Qt.Checked: - enabled_plugins.append(plugin_name_from_module(plugin.module)) + enabled_plugins.append(plugin.module_name) self.config.setting["enabled_plugins"] = " ".join(enabled_plugins) def change_details(self): @@ -99,11 +123,38 @@ class PluginsOptionsPage(OptionsPage): text.append("" + _("File") + ": " + plugin.file[len(plugin.dir)+1:]) self.ui.details.setText("

%s

" % "
\n".join(text)) + def open_plugins(self): + files = QtGui.QFileDialog.getOpenFileNames(self, "", "/", "Picard plugin (*.py *.pyc)") + if files: + files = map(unicode, files) + for path in files: + self.install_plugin(path) + + def install_plugin(self, path): + file = os.path.basename(path) + dest = os.path.join(self.tagger.user_plugin_dir, file) + if os.path.exists(dest): + msgbox = QtGui.QMessageBox(self) + msgbox.setText("A plugin named %s is already installed." % file) + msgbox.setInformativeText("Do you want to overwrite the existing plugin?") + msgbox.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) + msgbox.setDefaultButton(QtGui.QMessageBox.No) + if msgbox.exec_() == QtGui.QMessageBox.No: + return + self.tagger.pluginmanager.install_plugin(path, dest) + def open_plugin_dir(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.loader % self.user_plugin_dir, QtCore.QUrl.TolerantMode)) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.loader % self.tagger.user_plugin_dir, QtCore.QUrl.TolerantMode)) def open_plugin_site(self): QtGui.QDesktopServices.openUrl(QtCore.QUrl("http://musicbrainz.org/doc/Picard_Plugins", QtCore.QUrl.TolerantMode)) + def mimeTypes(self): + return ["text/uri-list"] + + def dropEvent(self, event): + for path in [os.path.normpath(unicode(u.toLocalFile())) for u in event.mimeData().urls()]: + self.install_plugin(path) + register_options_page(PluginsOptionsPage) diff --git a/picard/ui/ui_options_plugins.py b/picard/ui/ui_options_plugins.py index bdf1e6b2e..cc5e56c64 100644 --- a/picard/ui/ui_options_plugins.py +++ b/picard/ui/ui_options_plugins.py @@ -1,126 +1,117 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'ui\options_plugins.ui' -# -# Created: Thu Oct 22 00:04:48 2009 -# by: PyQt4 UI code generator 4.6 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -class Ui_PluginsOptionsPage(object): - def setupUi(self, PluginsOptionsPage): - PluginsOptionsPage.setObjectName("PluginsOptionsPage") - PluginsOptionsPage.resize(406, 297) - self.vboxlayout = QtGui.QVBoxLayout(PluginsOptionsPage) - self.vboxlayout.setSpacing(6) - self.vboxlayout.setMargin(9) - self.vboxlayout.setObjectName("vboxlayout") - self.splitter = QtGui.QSplitter(PluginsOptionsPage) - self.splitter.setOrientation(QtCore.Qt.Vertical) - self.splitter.setHandleWidth(2) - self.splitter.setObjectName("splitter") - self.groupBox_2 = QtGui.QGroupBox(self.splitter) - self.groupBox_2.setObjectName("groupBox_2") - self.vboxlayout1 = QtGui.QVBoxLayout(self.groupBox_2) - self.vboxlayout1.setSpacing(2) - self.vboxlayout1.setMargin(9) - self.vboxlayout1.setObjectName("vboxlayout1") - self.plugins = QtGui.QTreeWidget(self.groupBox_2) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.plugins.sizePolicy().hasHeightForWidth()) - self.plugins.setSizePolicy(sizePolicy) - self.plugins.setRootIsDecorated(False) - self.plugins.setObjectName("plugins") - self.vboxlayout1.addWidget(self.plugins) - self.horizontalLayout = QtGui.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.folder_open = QtGui.QPushButton(self.groupBox_2) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.folder_open.sizePolicy().hasHeightForWidth()) - self.folder_open.setSizePolicy(sizePolicy) - self.folder_open.setObjectName("folder_open") - self.horizontalLayout.addWidget(self.folder_open) - self.plugin_download = QtGui.QPushButton(self.groupBox_2) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.plugin_download.sizePolicy().hasHeightForWidth()) - self.plugin_download.setSizePolicy(sizePolicy) - self.plugin_download.setObjectName("plugin_download") - self.horizontalLayout.addWidget(self.plugin_download) - self.vboxlayout1.addLayout(self.horizontalLayout) - self.groupBox = QtGui.QGroupBox(self.splitter) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) - self.groupBox.setSizePolicy(sizePolicy) - self.groupBox.setObjectName("groupBox") - self.vboxlayout2 = QtGui.QVBoxLayout(self.groupBox) - self.vboxlayout2.setSpacing(0) - self.vboxlayout2.setMargin(9) - self.vboxlayout2.setObjectName("vboxlayout2") - self.scrollArea = QtGui.QScrollArea(self.groupBox) - self.scrollArea.setEnabled(True) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) - self.scrollArea.setSizePolicy(sizePolicy) - self.scrollArea.setFrameShape(QtGui.QFrame.HLine) - self.scrollArea.setFrameShadow(QtGui.QFrame.Plain) - self.scrollArea.setLineWidth(0) - self.scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.scrollArea.setWidgetResizable(True) - self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.scrollArea.setObjectName("scrollArea") - self.scrollAreaWidgetContents = QtGui.QWidget(self.scrollArea) - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 368, 69)) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.scrollAreaWidgetContents.sizePolicy().hasHeightForWidth()) - self.scrollAreaWidgetContents.setSizePolicy(sizePolicy) - self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") - self.verticalLayout = QtGui.QVBoxLayout(self.scrollAreaWidgetContents) - self.verticalLayout.setSizeConstraint(QtGui.QLayout.SetNoConstraint) - self.verticalLayout.setContentsMargins(0, 0, 6, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.details = QtGui.QLabel(self.scrollAreaWidgetContents) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.details.sizePolicy().hasHeightForWidth()) - self.details.setSizePolicy(sizePolicy) - self.details.setMinimumSize(QtCore.QSize(0, 0)) - self.details.setFrameShape(QtGui.QFrame.Box) - self.details.setLineWidth(0) - self.details.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) - self.details.setWordWrap(True) - self.details.setIndent(0) - self.details.setOpenExternalLinks(True) - self.details.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) - self.details.setObjectName("details") - self.verticalLayout.addWidget(self.details) - self.scrollArea.setWidget(self.scrollAreaWidgetContents) - self.vboxlayout2.addWidget(self.scrollArea) - self.vboxlayout.addWidget(self.splitter) - - self.retranslateUi(PluginsOptionsPage) - QtCore.QMetaObject.connectSlotsByName(PluginsOptionsPage) - - def retranslateUi(self, PluginsOptionsPage): - self.groupBox_2.setTitle(_("Plugins")) - self.plugins.headerItem().setText(0, _("Name")) - self.plugins.headerItem().setText(1, _("Version")) - self.plugins.headerItem().setText(2, _("Author")) - self.folder_open.setText(_("Open plugin folder")) - self.plugin_download.setText(_("Download plugins")) - self.groupBox.setTitle(_("Details")) - +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui/options_plugins.ui' +# +# Created: Mon Jun 20 19:52:58 2011 +# by: PyQt4 UI code generator 4.8.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_PluginsOptionsPage(object): + def setupUi(self, PluginsOptionsPage): + PluginsOptionsPage.setObjectName("PluginsOptionsPage") + PluginsOptionsPage.resize(513, 312) + self.vboxlayout = QtGui.QVBoxLayout(PluginsOptionsPage) + self.vboxlayout.setObjectName("vboxlayout") + self.splitter = QtGui.QSplitter(PluginsOptionsPage) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setHandleWidth(2) + self.splitter.setObjectName("splitter") + self.groupBox_2 = QtGui.QGroupBox(self.splitter) + self.groupBox_2.setObjectName("groupBox_2") + self.vboxlayout1 = QtGui.QVBoxLayout(self.groupBox_2) + self.vboxlayout1.setSpacing(2) + self.vboxlayout1.setObjectName("vboxlayout1") + self.plugins = QtGui.QTreeWidget(self.groupBox_2) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHeightForWidth(self.plugins.sizePolicy().hasHeightForWidth()) + self.plugins.setSizePolicy(sizePolicy) + self.plugins.setAcceptDrops(True) + self.plugins.setDragDropMode(QtGui.QAbstractItemView.DropOnly) + self.plugins.setRootIsDecorated(False) + self.plugins.setObjectName("plugins") + self.vboxlayout1.addWidget(self.plugins) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.install_plugin = QtGui.QPushButton(self.groupBox_2) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) + sizePolicy.setHeightForWidth(self.install_plugin.sizePolicy().hasHeightForWidth()) + self.install_plugin.setSizePolicy(sizePolicy) + self.install_plugin.setObjectName("install_plugin") + self.horizontalLayout.addWidget(self.install_plugin) + self.folder_open = QtGui.QPushButton(self.groupBox_2) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) + sizePolicy.setHeightForWidth(self.folder_open.sizePolicy().hasHeightForWidth()) + self.folder_open.setSizePolicy(sizePolicy) + self.folder_open.setObjectName("folder_open") + self.horizontalLayout.addWidget(self.folder_open) + self.plugin_download = QtGui.QPushButton(self.groupBox_2) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) + sizePolicy.setHeightForWidth(self.plugin_download.sizePolicy().hasHeightForWidth()) + self.plugin_download.setSizePolicy(sizePolicy) + self.plugin_download.setObjectName("plugin_download") + self.horizontalLayout.addWidget(self.plugin_download) + self.vboxlayout1.addLayout(self.horizontalLayout) + self.groupBox = QtGui.QGroupBox(self.splitter) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) + self.groupBox.setSizePolicy(sizePolicy) + self.groupBox.setObjectName("groupBox") + self.vboxlayout2 = QtGui.QVBoxLayout(self.groupBox) + self.vboxlayout2.setSpacing(0) + self.vboxlayout2.setObjectName("vboxlayout2") + self.scrollArea = QtGui.QScrollArea(self.groupBox) + self.scrollArea.setEnabled(True) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) + self.scrollArea.setSizePolicy(sizePolicy) + self.scrollArea.setFrameShape(QtGui.QFrame.HLine) + self.scrollArea.setFrameShadow(QtGui.QFrame.Plain) + self.scrollArea.setLineWidth(0) + self.scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.scrollArea.setWidgetResizable(True) + self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.scrollArea.setObjectName("scrollArea") + self.scrollAreaWidgetContents = QtGui.QWidget() + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 459, 76)) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHeightForWidth(self.scrollAreaWidgetContents.sizePolicy().hasHeightForWidth()) + self.scrollAreaWidgetContents.setSizePolicy(sizePolicy) + self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") + self.verticalLayout = QtGui.QVBoxLayout(self.scrollAreaWidgetContents) + self.verticalLayout.setSizeConstraint(QtGui.QLayout.SetNoConstraint) + self.verticalLayout.setContentsMargins(0, 0, 6, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.details = QtGui.QLabel(self.scrollAreaWidgetContents) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHeightForWidth(self.details.sizePolicy().hasHeightForWidth()) + self.details.setSizePolicy(sizePolicy) + self.details.setFrameShape(QtGui.QFrame.Box) + self.details.setLineWidth(0) + self.details.setText("") + self.details.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.details.setWordWrap(True) + self.details.setIndent(0) + self.details.setOpenExternalLinks(True) + self.details.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.details.setObjectName("details") + self.verticalLayout.addWidget(self.details) + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.vboxlayout2.addWidget(self.scrollArea) + self.vboxlayout.addWidget(self.splitter) + + self.retranslateUi(PluginsOptionsPage) + QtCore.QMetaObject.connectSlotsByName(PluginsOptionsPage) + + def retranslateUi(self, PluginsOptionsPage): + self.groupBox_2.setTitle(_("Plugins")) + self.plugins.headerItem().setText(0, _("Name")) + self.plugins.headerItem().setText(1, _("Version")) + self.plugins.headerItem().setText(2, _("Author")) + self.install_plugin.setText(_(u"Install plugin…")) + self.folder_open.setText(_("Open plugin folder")) + self.plugin_download.setText(_("Download plugins")) + self.groupBox.setTitle(_("Details")) + diff --git a/ui/options_plugins.ui b/ui/options_plugins.ui index 845d2cd38..03300535b 100644 --- a/ui/options_plugins.ui +++ b/ui/options_plugins.ui @@ -6,17 +6,11 @@ 0 0 - 406 - 297 + 513 + 312
- - 6 - - - 9 - @@ -33,9 +27,6 @@ 2 - - 9 - @@ -44,6 +35,12 @@ 0 + + true + + + QAbstractItemView::DropOnly + false @@ -66,6 +63,19 @@ + + + + + 0 + 0 + + + + Install plugin… + + + @@ -110,9 +120,6 @@ 0 - - 9 - @@ -147,8 +154,8 @@ 0 0 - 368 - 69 + 459 + 76 From dfe1003d39b4c76bef811a0a9467890ec7e21ed0 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sat, 25 Jun 2011 21:27:29 -0500 Subject: [PATCH 09/79] Fix a small encoding issue with the plugins patch. --- picard/ui/options/plugins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/picard/ui/options/plugins.py b/picard/ui/options/plugins.py index ee0a02f0f..35a813eda 100644 --- a/picard/ui/options/plugins.py +++ b/picard/ui/options/plugins.py @@ -22,6 +22,7 @@ import os.path import sys from PyQt4 import QtCore, QtGui from picard.config import TextOption +from picard.util import encode_filename from picard.ui.options import OptionsPage, register_options_page from picard.ui.ui_options_plugins import Ui_PluginsOptionsPage @@ -131,6 +132,7 @@ class PluginsOptionsPage(OptionsPage): self.install_plugin(path) def install_plugin(self, path): + path = encode_filename(path) file = os.path.basename(path) dest = os.path.join(self.tagger.user_plugin_dir, file) if os.path.exists(dest): From 10c739b7f4a671d5b07096ce6cdc9046750cf9b5 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 27 Jun 2011 15:11:52 -0500 Subject: [PATCH 10/79] Forgot to commit this with the rest of the CD Lookup changes. --- picard/webservice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/webservice.py b/picard/webservice.py index 71c146c3e..05d7298c7 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -290,7 +290,7 @@ class XmlWebService(QtCore.QObject): self._get_by_id('puid', puid, handler, inc) def lookup_discid(self, discid, handler): - self._get_by_id('discid', discid, handler, ['artist-credits']) + self._get_by_id('discid', discid, handler, ['artist-credits', 'labels']) def _find(self, entitytype, handler, kwargs): host = self.config.setting["server_host"] From 92e97878ddbf67cc65aca4402fd81c413c0ccd9c Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 27 Jun 2011 16:32:11 -0500 Subject: [PATCH 11/79] Add %_recordingcomment% and %_releasecomment% support. --- picard/mbxml.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/picard/mbxml.py b/picard/mbxml.py index e0873ba00..e6c0541d5 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -140,6 +140,7 @@ def label_info_from_node(node): catalog_numbers.append(label_info.catalog_number[0].text) return (labels, catalog_numbers) + def track_to_metadata(node, track, config=None): m = track.metadata recording_to_metadata(node.recording[0], track, config) @@ -170,6 +171,8 @@ def recording_to_metadata(node, track, config=None): m['title'] = nodes[0].text elif name == 'length' and nodes[0].text: m.length = int(nodes[0].text) + elif name == 'disambiguation': + m['~recordingcomment'] = nodes[0].text elif name == 'artist_credit': artist_credit_to_metadata(nodes[0], m, config=config) if name == 'relation_list': @@ -203,6 +206,8 @@ def release_to_metadata(node, m, config=None, album=None): m['releasestatus'] = nodes[0].text.lower() elif name == 'title' and not standardize_title: m['album'] = nodes[0].text + elif name == 'disambiguation': + m['~releasecomment'] = nodes[0].text elif name == 'asin': m['asin'] = nodes[0].text elif name == 'artist_credit': From d611464e0161d5e336b60560bb0888774c71e53e Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 28 Jun 2011 16:31:30 -0500 Subject: [PATCH 12/79] Fix an issue related to registering multiple functions on an ExtensionPoint --- picard/plugin.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/picard/plugin.py b/picard/plugin.py index a71cfb15d..c87dbebe8 100644 --- a/picard/plugin.py +++ b/picard/plugin.py @@ -26,9 +26,10 @@ import traceback _suffixes = [s[0] for s in imp.get_suffixes()] _package_entries = ["__init__.py", "__init__.pyc", "__init__.pyo"] +_extension_points = [] -def plugin_name_from_path(path): +def _plugin_name_from_path(path): path = os.path.normpath(path) file = os.path.basename(path) if os.path.isdir(path): @@ -44,23 +45,28 @@ def plugin_name_from_path(path): return None +def _unregister_module_extensions(module): + for ep in _extension_points: + ep.unregister_module(module) + + class ExtensionPoint(QtCore.QObject): def __init__(self): QtCore.QObject.__init__(self) self.__items = [] + _extension_points.append(self) def register(self, module, item): if module.startswith("picard.plugins"): module = module[15:] - for i, (module_, item_) in enumerate(self.__items): - if module == module_: - self.__items[i] = (module, item) - return else: module = None self.__items.append((module, item)) + def unregister_module(self, name): + self.__items = filter(lambda i: i[0] != name, self.__items) + def __iter__(self): enabled_plugins = self.config.setting["enabled_plugins"].split() for module, item in self.__items: @@ -134,7 +140,7 @@ class PluginManager(QtCore.QObject): return names = set() for path in [os.path.join(plugindir, file) for file in os.listdir(plugindir)]: - name = plugin_name_from_path(path) + name = _plugin_name_from_path(path) if name: names.add(name) for name in names: @@ -145,6 +151,12 @@ class PluginManager(QtCore.QObject): info = imp.find_module(name, [plugindir]) plugin = None try: + index = None + for i, p in enumerate(self.plugins): + if name == p.module_name: + _unregister_module_extensions(name) + index = i + break plugin_module = imp.load_module("picard.plugins." + name, *info) plugin = PluginWrapper(plugin_module, plugindir) for version in list(plugin.api_versions): @@ -152,10 +164,8 @@ class PluginManager(QtCore.QObject): if api_version.startswith(version): plugin.compatible = True setattr(picard.plugins, name, plugin_module) - for i, p in enumerate(self.plugins): - if name == p.module_name: - self.plugins[i] = plugin - break + if index: + self.plugins[index] = plugin else: self.plugins.append(plugin) break @@ -172,7 +182,7 @@ class PluginManager(QtCore.QObject): return plugin def install_plugin(self, path, dest): - plugin_name = plugin_name_from_path(path) + plugin_name = _plugin_name_from_path(path) plugin_dir = self.tagger.user_plugin_dir if plugin_name: try: From 5f9154fdaee0fe918e358e94c822c76ceecc6604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Wei=C3=9Fl?= Date: Wed, 29 Jun 2011 00:29:04 +0200 Subject: [PATCH 13/79] Fix avcodec compile error --- picard/musicdns/avcodec.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/picard/musicdns/avcodec.c b/picard/musicdns/avcodec.c index 932fc2eea..8e0269830 100644 --- a/picard/musicdns/avcodec.c +++ b/picard/musicdns/avcodec.c @@ -35,6 +35,10 @@ #endif #include +#if (LIBAVCODEC_VERSION_INT < ((52<<16)+(64<<8)+0)) +#define AVMEDIA_TYPE_AUDIO CODEC_TYPE_AUDIO +#endif + #ifdef _WIN32 #include @@ -251,7 +255,7 @@ decode(PyObject *self, PyObject *args) codec_context = NULL; for (i = 0; i < format_context->nb_streams; i++) { codec_context = (AVCodecContext *)format_context->streams[i]->codec; - if (codec_context && codec_context->codec_type == CODEC_TYPE_AUDIO) + if (codec_context && codec_context->codec_type == AVMEDIA_TYPE_AUDIO) break; } if (codec_context == NULL) { @@ -290,7 +294,18 @@ decode(PyObject *self, PyObject *args) while (size > 0) { output_size = buffer_size + AVCODEC_MAX_AUDIO_FRAME_SIZE; +#if (LIBAVCODEC_VERSION_INT <= ((52<<16) + (25<<8) + 0)) len = avcodec_decode_audio2(codec_context, (int16_t *)buffer_ptr, &output_size, data, size); +#else + { + AVPacket avpkt; + av_init_packet(&avpkt); + avpkt.data = data; + avpkt.size = size; + len = avcodec_decode_audio3(codec_context, (int16_t *)buffer_ptr, &output_size, &avpkt); + av_free_packet(&avpkt); + } +#endif if (len < 0) break; From ad85ad5665ac3500a0d930cc8f46ed35c53ea710 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 30 Jun 2011 00:28:30 -0500 Subject: [PATCH 14/79] Move the AVPacket initialization outside of the loop. --- picard/musicdns/avcodec.c | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/picard/musicdns/avcodec.c b/picard/musicdns/avcodec.c index 8e0269830..bfc79375f 100644 --- a/picard/musicdns/avcodec.c +++ b/picard/musicdns/avcodec.c @@ -69,7 +69,7 @@ ufile_open(URLContext *h, const char *filename, int flags) *w_ptr = a | (b << 4) | (c << 8) | (d << 12); if (*w_ptr == 0) break; - w_ptr++; + w_ptr++; } *w_ptr = 0; @@ -89,7 +89,7 @@ ufile_open(URLContext *h, const char *filename, int flags) fd = -1; size = wcslen(w_filename) + 2; ansi_filename = malloc(size); - if (ansi_filename) { + if (ansi_filename) { if (WideCharToMultiByte(CP_ACP, 0, w_filename, -1, ansi_filename, size, NULL, NULL) > 0) { fd = _open(ansi_filename, access, 0666); } @@ -174,8 +174,8 @@ decode(PyObject *self, PyObject *args) PyObject *filename; AVPacket packet; unsigned int i; - int buffer_size, channels, sample_rate, size, len, output_size; - uint8_t *buffer, *buffer_ptr, *data; + int buffer_size, channels, sample_rate, len, output_size; + uint8_t *buffer, *buffer_ptr; PyThreadState *_save; #ifdef _WIN32 @@ -212,13 +212,13 @@ decode(PyObject *self, PyObject *args) *e_ptr++ = 0x20; *e_ptr++ = 0x20; *e_ptr++ = 0x20; - /* copy ASCII filename to the end for extension-based format detection */ + /* copy ASCII filename to the end for extension-based format detection */ w_ptr = w_filename; while (*w_ptr) { *e_ptr++ = (*w_ptr++) & 0xFF; } *e_ptr = 0; - + Py_UNBLOCK_THREADS if (av_open_input_file(&format_context, e_filename, NULL, 0, NULL) != 0) { Py_BLOCK_THREADS @@ -285,33 +285,29 @@ decode(PyObject *self, PyObject *args) buffer_ptr = buffer; memset(buffer, 0, buffer_size); + AVPacket avpkt; + av_init_packet(&avpkt); + while (buffer_size > 0) { if (av_read_frame(format_context, &packet) < 0) break; - size = packet.size; - data = packet.data; + avpkt.size = packet.size; + avpkt.data = packet.data; - while (size > 0) { + while (avpkt.size > 0) { output_size = buffer_size + AVCODEC_MAX_AUDIO_FRAME_SIZE; #if (LIBAVCODEC_VERSION_INT <= ((52<<16) + (25<<8) + 0)) - len = avcodec_decode_audio2(codec_context, (int16_t *)buffer_ptr, &output_size, data, size); + len = avcodec_decode_audio2(codec_context, (int16_t *)buffer_ptr, &output_size, avpkt.data, avpkt.size); #else - { - AVPacket avpkt; - av_init_packet(&avpkt); - avpkt.data = data; - avpkt.size = size; - len = avcodec_decode_audio3(codec_context, (int16_t *)buffer_ptr, &output_size, &avpkt); - av_free_packet(&avpkt); - } + len = avcodec_decode_audio3(codec_context, (int16_t *)buffer_ptr, &output_size, &avpkt); #endif if (len < 0) break; - size -= len; - data += len; + avpkt.size -= len; + avpkt.data += len; if (output_size <= 0) continue; @@ -326,6 +322,9 @@ decode(PyObject *self, PyObject *args) av_free_packet(&packet); } + if (avpkt.data) + av_free_packet(&avpkt); + if (codec_context) avcodec_close(codec_context); From 92a541465aed1fab0553729dbce3859284b95b37 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 30 Jun 2011 10:24:02 -0500 Subject: [PATCH 15/79] Fix another CD Lookup issue when there's no labels on a release. --- picard/mbxml.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index e6c0541d5..6b51c9400 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -133,11 +133,12 @@ def artist_credit_to_metadata(node, m=None, release=None, config=None): def label_info_from_node(node): labels = [] catalog_numbers = [] - for label_info in node.label_info: - if 'label' in label_info.children: - labels.append(label_info.label[0].name[0].text) - if 'catalog_number' in label_info.children: - catalog_numbers.append(label_info.catalog_number[0].text) + if node.count != "0": + for label_info in node.label_info: + if 'label' in label_info.children: + labels.append(label_info.label[0].name[0].text) + if 'catalog_number' in label_info.children: + catalog_numbers.append(label_info.catalog_number[0].text) return (labels, catalog_numbers) From c57913b2d095c6e0e897e08350eee4037174a045 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 30 Jun 2011 14:53:25 -0500 Subject: [PATCH 16/79] Include recording-rels in album requests. --- picard/album.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/album.py b/picard/album.py index 0e6559b91..4dd3fba3f 100644 --- a/picard/album.py +++ b/picard/album.py @@ -252,7 +252,7 @@ class Album(DataObject, Item): require_authentication = False inc = ['release-groups', 'media', 'recordings', 'puids', 'artist-credits', 'labels', 'isrcs'] if self.config.setting['release_ars'] or self.config.setting['track_ars']: - inc += ['artist-rels', 'release-rels', 'url-rels', 'work-rels'] + inc += ['artist-rels', 'release-rels', 'url-rels', 'recording-rels', 'work-rels'] if self.config.setting['track_ars']: inc += ['recording-level-rels', 'work-level-rels'] if self.config.setting['folksonomy_tags']: From e26a5e7d4341406db1b2b0e863b94486dfec818e Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 30 Jun 2011 19:34:32 -0500 Subject: [PATCH 17/79] - Add preferred format support. - Use different widgets for preferred release country/format to allow multiple selections. - Move these options (and preferred release types) onto a new "Preferred Releases" options page, under "Metadata." - Share more code between file and cluster lookups matching. (compare_to_release was added to the Metadata class to allow this.) --- picard/cluster.py | 39 +- picard/file.py | 83 +-- picard/metadata.py | 67 +- picard/ui/options/dialog.py | 1 + picard/ui/options/matching.py | 25 - picard/ui/options/metadata.py | 12 - picard/ui/ui_options_matching.py | 155 +--- picard/ui/ui_options_metadata.py | 208 +++--- ui/options_matching.ui | 272 +------ ui/options_metadata.ui | 1179 +++++++++++++++--------------- 10 files changed, 774 insertions(+), 1267 deletions(-) diff --git a/picard/cluster.py b/picard/cluster.py index ba70f0724..1f0320b5b 100644 --- a/picard/cluster.py +++ b/picard/cluster.py @@ -43,7 +43,7 @@ class Cluster(QtCore.QObject, Item): self.lookup_queued = False # Weights for different elements when comparing a cluster to a release - self.comparison_weights = { 'title' : 17, 'artist' : 6, 'totaltracks' : 5, 'country': 4 } + self.comparison_weights = { 'album' : 17, 'artist' : 6, 'totaltracks' : 5, 'releasecountry': 2, 'format': 2 } def __repr__(self): return '' % self.metadata['album'] @@ -132,41 +132,24 @@ class Cluster(QtCore.QObject, Item): * title = 17 * artist name = 6 * number of tracks = 5 - * release country = 4 + * release country = 2 + * format = 2 - TODO: - * prioritize official albums over compilations (optional?) """ total = 0.0 - - a = self.metadata['album'] - b = release.title[0].text - total += similarity2(a, b) * self.comparison_weights['title'] + parts = [] + w = self.comparison_weights a = self.metadata['artist'] b = artist_credit_from_node(release.artist_credit[0], self.config)[0] - total += similarity2(a, b) * self.comparison_weights['artist'] + parts.append((similarity2(a, b), w["artist"])) + total += w["artist"] - a = len(self.files) - b = int(release.medium_list[0].track_count[0].text) - if a > b: - score = 0.0 - elif a < b: - score = 0.3 - else: - score = 1.0 - total += score * self.comparison_weights['totaltracks'] + t, p = self.metadata.compare_to_release(release, w, self.config) + total += t + parts.extend(p) - preferred_country = self.config.setting["preferred_release_country"] - - if preferred_country: - if "country" in release.children and preferred_country == release.country[0].text: - score = 1.0 - else: - score = 0.0 - total += score * self.comparison_weights["country"] - - return total / sum(self.comparison_weights.values()) + return reduce(lambda x, y: x + y[0] * y[1] / total, parts, 0.0) def _lookup_finished(self, document, http, error): self._signal_lookup_finished() diff --git a/picard/file.py b/picard/file.py index 5b44283b2..e7bda162a 100644 --- a/picard/file.py +++ b/picard/file.py @@ -43,8 +43,7 @@ from picard.util import ( format_time, LockableObject, pathcmp, - mimetype, - load_release_type_scores, + mimetype ) @@ -87,6 +86,10 @@ class File(LockableObject, Item): self.parent = None self.lookup_queued = False + self.comparison_weights = {"title": 13, "artist": 4, "album": 5, + "length": 10, "totaltracks": 4, "releasetype": 20, + "releasecountry": 2, "format": 2} + def __repr__(self): return '' % (self.id, self.base_filename) @@ -440,89 +443,45 @@ class File(LockableObject, Item): * length = 10 * number of tracks = 4 * album type = 20 - * release country = 4 + * release country = 2 + * format = 2 """ total = 0.0 parts = [] - scores = [] + w = self.comparison_weights if 'title' in self.metadata: a = self.metadata['title'] b = track.title[0].text - parts.append((similarity2(a, b), 13)) - total += 13 + parts.append((similarity2(a, b), w["title"])) + total += w["title"] if 'artist' in self.metadata: a = self.metadata['artist'] b = artist_credit_from_node(track.artist_credit[0], self.config)[0] - parts.append((similarity2(a, b), 4)) - total += 4 + parts.append((similarity2(a, b), w["artist"])) + total += w["artist"] a = self.metadata.length - if a > 0 and 'duration' in track.children: - b = int(track.duration[0].text) + if a > 0 and 'length' in track.children: + b = int(track.length[0].text) score = 1.0 - min(abs(a - b), 30000) / 30000.0 - parts.append((score, 10)) - total += 10 - - album = totaltracks = None - if 'album' in self.metadata: - album = self.metadata['album'] - if 'totaltracks' in self.metadata: - totaltracks = int(self.metadata['totaltracks']) + parts.append((score, w["length"])) + total += w["length"] releases = [] if "release_list" in track.children and "release" in track.release_list[0].children: releases = track.release_list[0].release if not releases: - return (reduce(lambda x, y: x + y[0] * y[1] / total, parts, 0.0), None) - - preferred_country = self.config.setting["preferred_release_country"] + return (total, None) + scores = [] for release in releases: - total_ = total - parts_ = list(parts) - - if album: - b = release.title[0].text - parts_.append((similarity2(album, b), 5)) - total_ += 5 - - if preferred_country: - total_ += 4 - if "country" in release.children and preferred_country == release.country[0].text: - score = 1.0 - else: - score = 0.0 - parts_.append((score, 4)) - - track_list = release.medium_list[0].medium[0].track_list[0] - if totaltracks and 'count' in track_list.attribs: - try: - a = totaltracks - b = int(track_list.count) - if a > b: - score = 0.0 - elif a < b: - score = 0.3 - else: - score = 1.0 - parts_.append((score, 4)) - total_ += 4 - except ValueError: - pass - - type_scores = load_release_type_scores(self.config.setting["release_type_scores"]) - if 'release_group' in release.children and 'type' in release.release_group[0].attribs: - release_type = release.release_group[0].type - score = type_scores.get(release_type, type_scores.get('Other', 0.5)) - else: - score = 0.0 - parts_.append((score, 20)) - total_ += 20 - + t, p = self.metadata.compare_to_release(release, w, self.config) + total_ = total + t + parts_ = list(parts) + p scores.append((reduce(lambda x, y: x + y[0] * y[1] / total_, parts_, 0.0), release.id)) return max(scores, key=lambda x: x[0]) diff --git a/picard/metadata.py b/picard/metadata.py index 88e9e12df..4c64970ea 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -21,7 +21,7 @@ import re import unicodedata from picard.plugin import ExtensionPoint from picard.similarity import similarity, similarity2 -from picard.util import format_time +from picard.util import format_time, load_release_type_scores class Metadata(object): @@ -78,6 +78,71 @@ class Metadata(object): #print "******", reduce(lambda x, y: x + y[0] * y[1] / total, parts, 0.0) return reduce(lambda x, y: x + y[0] * y[1] / total, parts, 0.0) + def compare_to_release(self, release, weights, config): + total = 0.0 + parts = [] + + if "album" in self: + b = release.title[0].text + parts.append((similarity2(self["album"], b), weights["album"])) + total += weights["album"] + + if "totaltracks" in self: + a = int(self["totaltracks"]) + if "title" in weights: + b = int(release.medium_list[0].medium[0].track_list[0].count) + else: + b = int(release.medium_list[0].track_count[0].text) + if a > b: + score = 0.0 + elif a < b: + score = 0.3 + else: + score = 1.0 + parts.append((score, weights["totaltracks"])) + total += weights["totaltracks"] + + preferred_countries = config.setting["preferred_release_countries"].split(" ") + preferred_formats = config.setting["preferred_release_formats"].split(" ") + + total_countries = len(preferred_countries) + if total_countries: + score = 0.0 + total += weights["releasecountry"] + if "country" in release.children: + try: + i = preferred_countries.index(release.country[0].text) + score = float(total_countries - i) / float(total_countries) + except ValueError: + pass + parts.append((score, weights["releasecountry"])) + + total_formats = len(preferred_formats) + if total_formats: + score = 0.0 + subtotal = 0 + for medium in release.medium_list[0].medium: + if "format" in medium.children: + try: + i = preferred_formats.index(medium.format[0].text) + score += float(total_formats - i) / float(total_formats) + except ValueError: + pass + subtotal += 1 + if subtotal > 0: score /= subtotal + parts.append((score, weights["format"])) + + if "releasetype" in weights: + type_scores = load_release_type_scores(config.setting["release_type_scores"]) + if 'release_group' in release.children and 'type' in release.release_group[0].attribs: + release_type = release.release_group[0].type + score = type_scores.get(release_type, type_scores.get('Other', 0.5)) + else: + score = 0.0 + parts.append((score, weights["releasetype"])) + + return (total, parts) + def copy(self, other): self._items = {} for key, values in other.rawitems(): diff --git a/picard/ui/options/dialog.py b/picard/ui/options/dialog.py index 3dcdde2c1..4dcbeae5d 100644 --- a/picard/ui/options/dialog.py +++ b/picard/ui/options/dialog.py @@ -33,6 +33,7 @@ from picard.ui.options import ( ratings, matching, metadata, + releases, moving, renaming, plugins, diff --git a/picard/ui/options/matching.py b/picard/ui/options/matching.py index a93d93731..c7b7811a2 100644 --- a/picard/ui/options/matching.py +++ b/picard/ui/options/matching.py @@ -19,7 +19,6 @@ from PyQt4 import QtCore, QtGui from picard.config import FloatOption, TextOption -from picard.util import load_release_type_scores, save_release_type_scores from picard.ui.options import OptionsPage, OptionsCheckError, register_options_page from picard.ui.ui_options_matching import Ui_MatchingOptionsPage @@ -36,7 +35,6 @@ class MatchingOptionsPage(OptionsPage): FloatOption("setting", "file_lookup_threshold", 0.7), FloatOption("setting", "cluster_lookup_threshold", 0.8), FloatOption("setting", "track_matching_threshold", 0.4), - TextOption("setting", "release_type_scores", "Album 0.5 Single 0.5 EP 0.5 Compilation 0.5 Soundtrack 0.5 Spokenword 0.5 Interview 0.5 Audiobook 0.5 Live 0.5 Remix 0.5 Other 0.5"), ] _release_type_sliders = {} @@ -45,39 +43,16 @@ class MatchingOptionsPage(OptionsPage): super(MatchingOptionsPage, self).__init__(parent) self.ui = Ui_MatchingOptionsPage() self.ui.setupUi(self) - self.connect(self.ui.reset_preferred_types_btn, QtCore.SIGNAL("clicked()"), self.reset_preferred_types) - self._release_type_sliders["Album"] = self.ui.prefer_album_score - self._release_type_sliders["Single"] = self.ui.prefer_single_score - self._release_type_sliders["EP"] = self.ui.prefer_ep_score - self._release_type_sliders["Compilation"] = self.ui.prefer_compilation_score - self._release_type_sliders["Soundtrack"] = self.ui.prefer_soundtrack_score - self._release_type_sliders["Spokenword"] = self.ui.prefer_spokenword_score - self._release_type_sliders["Interview"] = self.ui.prefer_interview_score - self._release_type_sliders["Audiobook"] = self.ui.prefer_audiobook_score - self._release_type_sliders["Live"] = self.ui.prefer_live_score - self._release_type_sliders["Remix"] = self.ui.prefer_remix_score - self._release_type_sliders["Other"] = self.ui.prefer_other_score def load(self): self.ui.file_lookup_threshold.setValue(int(self.config.setting["file_lookup_threshold"] * 100)) self.ui.cluster_lookup_threshold.setValue(int(self.config.setting["cluster_lookup_threshold"] * 100)) self.ui.track_matching_threshold.setValue(int(self.config.setting["track_matching_threshold"] * 100)) - scores = load_release_type_scores(self.config.setting["release_type_scores"]) - for (release_type, release_type_slider) in self._release_type_sliders.iteritems(): - release_type_slider.setValue(int(scores.get(release_type, 0.5) * 100)) def save(self): self.config.setting["file_lookup_threshold"] = float(self.ui.file_lookup_threshold.value()) / 100.0 self.config.setting["cluster_lookup_threshold"] = float(self.ui.cluster_lookup_threshold.value()) / 100.0 self.config.setting["track_matching_threshold"] = float(self.ui.track_matching_threshold.value()) / 100.0 - scores = {} - for (release_type, release_type_slider) in self._release_type_sliders.iteritems(): - scores[release_type] = float(release_type_slider.value()) / 100.0 - self.config.setting["release_type_scores"] = save_release_type_scores(scores) - - def reset_preferred_types(self): - for release_type_slider in self._release_type_sliders.values(): - release_type_slider.setValue(50) register_options_page(MatchingOptionsPage) diff --git a/picard/ui/options/metadata.py b/picard/ui/options/metadata.py index 7218dc71f..599c4456a 100644 --- a/picard/ui/options/metadata.py +++ b/picard/ui/options/metadata.py @@ -21,9 +21,6 @@ from PyQt4 import QtCore, QtGui from picard.config import BoolOption, TextOption from picard.ui.options import OptionsPage, OptionsCheckError, register_options_page from picard.ui.ui_options_metadata import Ui_MetadataOptionsPage -from picard.const import RELEASE_COUNTRIES -import operator -import locale class MetadataOptionsPage(OptionsPage): @@ -41,7 +38,6 @@ class MetadataOptionsPage(OptionsPage): BoolOption("setting", "release_ars", True), BoolOption("setting", "track_ars", False), BoolOption("setting", "folksonomy_tags", False), - TextOption("setting", "preferred_release_country", u""), BoolOption("setting", "convert_punctuation", False), BoolOption("setting", "standardize_tracks", False), BoolOption("setting", "standardize_releases", False), @@ -55,19 +51,12 @@ class MetadataOptionsPage(OptionsPage): self.connect(self.ui.va_name_default, QtCore.SIGNAL("clicked()"), self.set_va_name_default) self.connect(self.ui.nat_name_default, QtCore.SIGNAL("clicked()"), self.set_nat_name_default) - self.ui.preferred_release_country.addItem(_("None"), QtCore.QVariant("")) - country_list = [(c[0], _(c[1])) for c in RELEASE_COUNTRIES.items()] - for country, name in sorted(country_list, key=operator.itemgetter(1), cmp=locale.strcoll): - self.ui.preferred_release_country.addItem(name, QtCore.QVariant(country)) - def load(self): self.ui.translate_artist_names.setChecked(self.config.setting["translate_artist_names"]) self.ui.convert_punctuation.setChecked(self.config.setting["convert_punctuation"]) self.ui.release_ars.setChecked(self.config.setting["release_ars"]) self.ui.track_ars.setChecked(self.config.setting["track_ars"]) self.ui.folksonomy_tags.setChecked(self.config.setting["folksonomy_tags"]) - current_release_country = QtCore.QVariant(self.config.setting["preferred_release_country"]) - self.ui.preferred_release_country.setCurrentIndex(self.ui.preferred_release_country.findData(current_release_country)) self.ui.va_name.setText(self.config.setting["va_name"]) self.ui.nat_name.setText(self.config.setting["nat_name"]) self.ui.standardize_tracks.setChecked(self.config.setting["standardize_tracks"]) @@ -80,7 +69,6 @@ class MetadataOptionsPage(OptionsPage): self.config.setting["release_ars"] = self.ui.release_ars.isChecked() self.config.setting["track_ars"] = self.ui.track_ars.isChecked() self.config.setting["folksonomy_tags"] = self.ui.folksonomy_tags.isChecked() - self.config.setting["preferred_release_country"] = self.ui.preferred_release_country.itemData(self.ui.preferred_release_country.currentIndex()).toString() self.config.setting["va_name"] = self.ui.va_name.text() self.config.setting["nat_name"] = self.ui.nat_name.text() self.config.setting["standardize_tracks"] = self.ui.standardize_tracks.isChecked() diff --git a/picard/ui/ui_options_matching.py b/picard/ui/ui_options_matching.py index 7601f999f..2c14ef0cd 100644 --- a/picard/ui/ui_options_matching.py +++ b/picard/ui/ui_options_matching.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file 'ui/options_matching.ui' # -# Created: Thu May 12 23:52:13 2011 -# by: PyQt4 UI code generator 4.7.2 +# Created: Tue Jun 21 15:31:51 2011 +# by: PyQt4 UI code generator 4.8.4 # # WARNING! All changes made in this file will be lost! @@ -12,21 +12,16 @@ from PyQt4 import QtCore, QtGui class Ui_MatchingOptionsPage(object): def setupUi(self, MatchingOptionsPage): MatchingOptionsPage.setObjectName("MatchingOptionsPage") - MatchingOptionsPage.resize(382, 498) + MatchingOptionsPage.resize(413, 612) self.vboxlayout = QtGui.QVBoxLayout(MatchingOptionsPage) - self.vboxlayout.setSpacing(6) - self.vboxlayout.setMargin(9) self.vboxlayout.setObjectName("vboxlayout") self.rename_files = QtGui.QGroupBox(MatchingOptionsPage) self.rename_files.setObjectName("rename_files") self.gridlayout = QtGui.QGridLayout(self.rename_files) - self.gridlayout.setMargin(9) self.gridlayout.setSpacing(2) self.gridlayout.setObjectName("gridlayout") self.label_6 = QtGui.QLabel(self.rename_files) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.label_6.sizePolicy().hasHeightForWidth()) self.label_6.setSizePolicy(sizePolicy) self.label_6.setObjectName("label_6") @@ -45,140 +40,19 @@ class Ui_MatchingOptionsPage(object): self.gridlayout.addWidget(self.file_lookup_threshold, 0, 1, 1, 1) self.label_4 = QtGui.QLabel(self.rename_files) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth()) self.label_4.setSizePolicy(sizePolicy) self.label_4.setObjectName("label_4") self.gridlayout.addWidget(self.label_4, 0, 0, 1, 1) self.label_5 = QtGui.QLabel(self.rename_files) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.label_5.sizePolicy().hasHeightForWidth()) self.label_5.setSizePolicy(sizePolicy) self.label_5.setObjectName("label_5") self.gridlayout.addWidget(self.label_5, 1, 0, 1, 1) self.vboxlayout.addWidget(self.rename_files) - self.groupBox = QtGui.QGroupBox(MatchingOptionsPage) - self.groupBox.setObjectName("groupBox") - self.gridLayout = QtGui.QGridLayout(self.groupBox) - self.gridLayout.setObjectName("gridLayout") - self.prefer_album_score = QtGui.QSlider(self.groupBox) - self.prefer_album_score.setMaximum(100) - self.prefer_album_score.setProperty("value", 50) - self.prefer_album_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_album_score.setObjectName("prefer_album_score") - self.gridLayout.addWidget(self.prefer_album_score, 0, 2, 1, 1) - self.prefer_single_score = QtGui.QSlider(self.groupBox) - self.prefer_single_score.setMaximum(100) - self.prefer_single_score.setProperty("value", 50) - self.prefer_single_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_single_score.setObjectName("prefer_single_score") - self.gridLayout.addWidget(self.prefer_single_score, 1, 2, 1, 1) - self.label = QtGui.QLabel(self.groupBox) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 0, 0, 1, 1) - self.label_2 = QtGui.QLabel(self.groupBox) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) - self.prefer_ep_score = QtGui.QSlider(self.groupBox) - self.prefer_ep_score.setMaximum(100) - self.prefer_ep_score.setProperty("value", 50) - self.prefer_ep_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_ep_score.setObjectName("prefer_ep_score") - self.gridLayout.addWidget(self.prefer_ep_score, 2, 2, 1, 1) - self.prefer_compilation_score = QtGui.QSlider(self.groupBox) - self.prefer_compilation_score.setMaximum(100) - self.prefer_compilation_score.setProperty("value", 50) - self.prefer_compilation_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_compilation_score.setObjectName("prefer_compilation_score") - self.gridLayout.addWidget(self.prefer_compilation_score, 3, 2, 1, 1) - self.prefer_soundtrack_score = QtGui.QSlider(self.groupBox) - self.prefer_soundtrack_score.setMaximum(100) - self.prefer_soundtrack_score.setProperty("value", 50) - self.prefer_soundtrack_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_soundtrack_score.setObjectName("prefer_soundtrack_score") - self.gridLayout.addWidget(self.prefer_soundtrack_score, 4, 2, 1, 1) - self.prefer_spokenword_score = QtGui.QSlider(self.groupBox) - self.prefer_spokenword_score.setMaximum(100) - self.prefer_spokenword_score.setProperty("value", 50) - self.prefer_spokenword_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_spokenword_score.setObjectName("prefer_spokenword_score") - self.gridLayout.addWidget(self.prefer_spokenword_score, 5, 2, 1, 1) - self.label_3 = QtGui.QLabel(self.groupBox) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1) - self.label_7 = QtGui.QLabel(self.groupBox) - self.label_7.setObjectName("label_7") - self.gridLayout.addWidget(self.label_7, 3, 0, 1, 1) - self.label_8 = QtGui.QLabel(self.groupBox) - self.label_8.setObjectName("label_8") - self.gridLayout.addWidget(self.label_8, 4, 0, 1, 1) - self.label_9 = QtGui.QLabel(self.groupBox) - self.label_9.setObjectName("label_9") - self.gridLayout.addWidget(self.label_9, 5, 0, 1, 1) - self.prefer_interview_score = QtGui.QSlider(self.groupBox) - self.prefer_interview_score.setMaximum(100) - self.prefer_interview_score.setProperty("value", 50) - self.prefer_interview_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_interview_score.setObjectName("prefer_interview_score") - self.gridLayout.addWidget(self.prefer_interview_score, 6, 2, 1, 1) - self.prefer_audiobook_score = QtGui.QSlider(self.groupBox) - self.prefer_audiobook_score.setMaximum(100) - self.prefer_audiobook_score.setProperty("value", 50) - self.prefer_audiobook_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_audiobook_score.setObjectName("prefer_audiobook_score") - self.gridLayout.addWidget(self.prefer_audiobook_score, 7, 2, 1, 1) - self.prefer_live_score = QtGui.QSlider(self.groupBox) - self.prefer_live_score.setMaximum(100) - self.prefer_live_score.setProperty("value", 50) - self.prefer_live_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_live_score.setObjectName("prefer_live_score") - self.gridLayout.addWidget(self.prefer_live_score, 8, 2, 1, 1) - self.label_10 = QtGui.QLabel(self.groupBox) - self.label_10.setObjectName("label_10") - self.gridLayout.addWidget(self.label_10, 6, 0, 1, 1) - self.label_11 = QtGui.QLabel(self.groupBox) - self.label_11.setObjectName("label_11") - self.gridLayout.addWidget(self.label_11, 7, 0, 1, 1) - self.label_12 = QtGui.QLabel(self.groupBox) - self.label_12.setObjectName("label_12") - self.gridLayout.addWidget(self.label_12, 8, 0, 1, 1) - self.prefer_remix_score = QtGui.QSlider(self.groupBox) - self.prefer_remix_score.setMaximum(100) - self.prefer_remix_score.setProperty("value", 50) - self.prefer_remix_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_remix_score.setObjectName("prefer_remix_score") - self.gridLayout.addWidget(self.prefer_remix_score, 9, 2, 1, 1) - self.label_13 = QtGui.QLabel(self.groupBox) - self.label_13.setObjectName("label_13") - self.gridLayout.addWidget(self.label_13, 9, 0, 1, 1) - self.prefer_other_score = QtGui.QSlider(self.groupBox) - self.prefer_other_score.setMaximum(100) - self.prefer_other_score.setSliderPosition(50) - self.prefer_other_score.setOrientation(QtCore.Qt.Horizontal) - self.prefer_other_score.setObjectName("prefer_other_score") - self.gridLayout.addWidget(self.prefer_other_score, 10, 2, 1, 1) - self.label_14 = QtGui.QLabel(self.groupBox) - self.label_14.setObjectName("label_14") - self.gridLayout.addWidget(self.label_14, 10, 0, 1, 1) - self.horizontalLayout = QtGui.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem) - self.reset_preferred_types_btn = QtGui.QPushButton(self.groupBox) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.reset_preferred_types_btn.sizePolicy().hasHeightForWidth()) - self.reset_preferred_types_btn.setSizePolicy(sizePolicy) - self.reset_preferred_types_btn.setObjectName("reset_preferred_types_btn") - self.horizontalLayout.addWidget(self.reset_preferred_types_btn) - self.gridLayout.addLayout(self.horizontalLayout, 12, 2, 1, 1) - self.vboxlayout.addWidget(self.groupBox) - spacerItem1 = QtGui.QSpacerItem(20, 41, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.vboxlayout.addItem(spacerItem1) + spacerItem = QtGui.QSpacerItem(20, 41, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.vboxlayout.addItem(spacerItem) self.label_6.setBuddy(self.file_lookup_threshold) self.label_4.setBuddy(self.file_lookup_threshold) self.label_5.setBuddy(self.file_lookup_threshold) @@ -191,22 +65,9 @@ class Ui_MatchingOptionsPage(object): def retranslateUi(self, MatchingOptionsPage): self.rename_files.setTitle(_("Thresholds")) self.label_6.setText(_("Minimal similarity for matching files to tracks:")) - self.track_matching_threshold.setSuffix(_(" %")) - self.cluster_lookup_threshold.setSuffix(_(" %")) - self.file_lookup_threshold.setSuffix(_(" %")) + self.track_matching_threshold.setSuffix(" %") + self.cluster_lookup_threshold.setSuffix(" %") + self.file_lookup_threshold.setSuffix(" %") self.label_4.setText(_("Minimal similarity for file lookups:")) self.label_5.setText(_("Minimal similarity for cluster lookups:")) - self.groupBox.setTitle(_("Preferred release types")) - self.label.setText(_("Album")) - self.label_2.setText(_("Single")) - self.label_3.setText(_("EP")) - self.label_7.setText(_("Compilation")) - self.label_8.setText(_("Soundtrack")) - self.label_9.setText(_("Spokenword")) - self.label_10.setText(_("Interview")) - self.label_11.setText(_("Audiobook")) - self.label_12.setText(_("Live")) - self.label_13.setText(_("Remix")) - self.label_14.setText(_("Other")) - self.reset_preferred_types_btn.setText(_("Reset all")) diff --git a/picard/ui/ui_options_metadata.py b/picard/ui/ui_options_metadata.py index c9ef2c9a0..d5458ee65 100644 --- a/picard/ui/ui_options_metadata.py +++ b/picard/ui/ui_options_metadata.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'ui/options_metadata.ui' # -# Created: Fri Jun 17 15:22:22 2011 +# Created: Tue Jun 21 17:09:36 2011 # by: PyQt4 UI code generator 4.8.4 # # WARNING! All changes made in this file will be lost! @@ -14,17 +14,9 @@ class Ui_MetadataOptionsPage(object): MetadataOptionsPage.setObjectName("MetadataOptionsPage") MetadataOptionsPage.resize(423, 553) self.verticalLayout = QtGui.QVBoxLayout(MetadataOptionsPage) - self.verticalLayout.setContentsMargins(-1, 0, -1, -1) self.verticalLayout.setObjectName("verticalLayout") - self.verticalLayout_2 = QtGui.QVBoxLayout() - self.verticalLayout_2.setSpacing(0) - self.verticalLayout_2.setSizeConstraint(QtGui.QLayout.SetDefaultConstraint) - self.verticalLayout_2.setContentsMargins(-1, 0, -1, -1) - self.verticalLayout_2.setObjectName("verticalLayout_2") self.rename_files = QtGui.QGroupBox(MetadataOptionsPage) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.rename_files.sizePolicy().hasHeightForWidth()) self.rename_files.setSizePolicy(sizePolicy) self.rename_files.setMinimumSize(QtCore.QSize(397, 135)) @@ -49,38 +41,18 @@ class Ui_MetadataOptionsPage(object): self.folksonomy_tags = QtGui.QCheckBox(self.rename_files) self.folksonomy_tags.setObjectName("folksonomy_tags") self.verticalLayout_3.addWidget(self.folksonomy_tags) - self.label_8 = QtGui.QLabel(self.rename_files) - self.label_8.setObjectName("label_8") - self.verticalLayout_3.addWidget(self.label_8) - self.preferred_release_country = QtGui.QComboBox(self.rename_files) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.preferred_release_country.sizePolicy().hasHeightForWidth()) - self.preferred_release_country.setSizePolicy(sizePolicy) - self.preferred_release_country.setMaximumSize(QtCore.QSize(373, 16777215)) - self.preferred_release_country.setObjectName("preferred_release_country") - self.verticalLayout_3.addWidget(self.preferred_release_country) - self.verticalLayout_2.addWidget(self.rename_files) + self.verticalLayout.addWidget(self.rename_files) self.rename_files_2 = QtGui.QGroupBox(MetadataOptionsPage) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.rename_files_2.sizePolicy().hasHeightForWidth()) self.rename_files_2.setSizePolicy(sizePolicy) self.rename_files_2.setMinimumSize(QtCore.QSize(397, 144)) self.rename_files_2.setFlat(False) self.rename_files_2.setObjectName("rename_files_2") - self.layoutWidget = QtGui.QWidget(self.rename_files_2) - self.layoutWidget.setGeometry(QtCore.QRect(16, 31, 301, 101)) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout.setMargin(0) + self.gridLayout = QtGui.QGridLayout(self.rename_files_2) self.gridLayout.setObjectName("gridLayout") - self.label = QtGui.QLabel(self.layoutWidget) + self.label = QtGui.QLabel(self.rename_files_2) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) self.label.setSizePolicy(sizePolicy) font = QtGui.QFont() @@ -90,7 +62,7 @@ class Ui_MetadataOptionsPage(object): self.label.setAlignment(QtCore.Qt.AlignCenter) self.label.setObjectName("label") self.gridLayout.addWidget(self.label, 0, 1, 1, 3) - self.label_2 = QtGui.QLabel(self.layoutWidget) + self.label_2 = QtGui.QLabel(self.rename_files_2) font = QtGui.QFont() font.setWeight(75) font.setBold(True) @@ -98,63 +70,15 @@ class Ui_MetadataOptionsPage(object): self.label_2.setAlignment(QtCore.Qt.AlignCenter) self.label_2.setObjectName("label_2") self.gridLayout.addWidget(self.label_2, 0, 4, 1, 3) - self.label_3 = QtGui.QLabel(self.layoutWidget) + self.label_3 = QtGui.QLabel(self.rename_files_2) self.label_3.setLayoutDirection(QtCore.Qt.LeftToRight) self.label_3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.label_3.setObjectName("label_3") self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1) - self.label_4 = QtGui.QLabel(self.layoutWidget) - self.label_4.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_4.setObjectName("label_4") - self.gridLayout.addWidget(self.label_4, 2, 0, 1, 1) - self.label_5 = QtGui.QLabel(self.layoutWidget) - self.label_5.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_5.setObjectName("label_5") - self.gridLayout.addWidget(self.label_5, 3, 0, 1, 1) - self.standardize_tracks = QtGui.QRadioButton(self.layoutWidget) - self.standardize_tracks.setEnabled(True) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.standardize_tracks.sizePolicy().hasHeightForWidth()) - self.standardize_tracks.setSizePolicy(sizePolicy) - self.standardize_tracks.setMinimumSize(QtCore.QSize(18, 18)) - self.standardize_tracks.setText("") - self.standardize_tracks.setObjectName("standardize_tracks") - self.buttonGroup = QtGui.QButtonGroup(MetadataOptionsPage) - self.buttonGroup.setObjectName("buttonGroup") - self.buttonGroup.addButton(self.standardize_tracks) - self.gridLayout.addWidget(self.standardize_tracks, 1, 5, 1, 1) - self.standardize_releases = QtGui.QRadioButton(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.standardize_releases.sizePolicy().hasHeightForWidth()) - self.standardize_releases.setSizePolicy(sizePolicy) - self.standardize_releases.setMinimumSize(QtCore.QSize(18, 18)) - self.standardize_releases.setText("") - self.standardize_releases.setObjectName("standardize_releases") - self.buttonGroup_2 = QtGui.QButtonGroup(MetadataOptionsPage) - self.buttonGroup_2.setObjectName("buttonGroup_2") - self.buttonGroup_2.addButton(self.standardize_releases) - self.gridLayout.addWidget(self.standardize_releases, 2, 5, 1, 1) - self.standardize_artists = QtGui.QRadioButton(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.standardize_artists.sizePolicy().hasHeightForWidth()) - self.standardize_artists.setSizePolicy(sizePolicy) - self.standardize_artists.setMinimumSize(QtCore.QSize(18, 18)) - self.standardize_artists.setText("") - self.standardize_artists.setObjectName("standardize_artists") - self.buttonGroup_3 = QtGui.QButtonGroup(MetadataOptionsPage) - self.buttonGroup_3.setObjectName("buttonGroup_3") - self.buttonGroup_3.addButton(self.standardize_artists) - self.gridLayout.addWidget(self.standardize_artists, 3, 5, 1, 1) - self.tracks_on_cover = QtGui.QRadioButton(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) + spacerItem = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem, 1, 1, 1, 1) + self.tracks_on_cover = QtGui.QRadioButton(self.rename_files_2) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) sizePolicy.setHeightForWidth(self.tracks_on_cover.sizePolicy().hasHeightForWidth()) self.tracks_on_cover.setSizePolicy(sizePolicy) self.tracks_on_cover.setMinimumSize(QtCore.QSize(18, 18)) @@ -163,24 +87,67 @@ class Ui_MetadataOptionsPage(object): self.tracks_on_cover.setIconSize(QtCore.QSize(16, 16)) self.tracks_on_cover.setChecked(True) self.tracks_on_cover.setObjectName("tracks_on_cover") + self.buttonGroup = QtGui.QButtonGroup(MetadataOptionsPage) + self.buttonGroup.setObjectName("buttonGroup") self.buttonGroup.addButton(self.tracks_on_cover) self.gridLayout.addWidget(self.tracks_on_cover, 1, 2, 1, 1) - self.releases_on_cover = QtGui.QRadioButton(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) + spacerItem1 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem1, 1, 3, 1, 1) + spacerItem2 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem2, 1, 4, 1, 1) + self.standardize_tracks = QtGui.QRadioButton(self.rename_files_2) + self.standardize_tracks.setEnabled(True) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHeightForWidth(self.standardize_tracks.sizePolicy().hasHeightForWidth()) + self.standardize_tracks.setSizePolicy(sizePolicy) + self.standardize_tracks.setMinimumSize(QtCore.QSize(18, 18)) + self.standardize_tracks.setText("") + self.standardize_tracks.setObjectName("standardize_tracks") + self.buttonGroup.addButton(self.standardize_tracks) + self.gridLayout.addWidget(self.standardize_tracks, 1, 5, 1, 1) + spacerItem3 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem3, 1, 6, 1, 1) + self.label_4 = QtGui.QLabel(self.rename_files_2) + self.label_4.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_4.setObjectName("label_4") + self.gridLayout.addWidget(self.label_4, 2, 0, 1, 1) + spacerItem4 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem4, 2, 1, 1, 1) + self.releases_on_cover = QtGui.QRadioButton(self.rename_files_2) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) sizePolicy.setHeightForWidth(self.releases_on_cover.sizePolicy().hasHeightForWidth()) self.releases_on_cover.setSizePolicy(sizePolicy) self.releases_on_cover.setMinimumSize(QtCore.QSize(18, 18)) self.releases_on_cover.setText("") self.releases_on_cover.setChecked(True) self.releases_on_cover.setObjectName("releases_on_cover") + self.buttonGroup_2 = QtGui.QButtonGroup(MetadataOptionsPage) + self.buttonGroup_2.setObjectName("buttonGroup_2") self.buttonGroup_2.addButton(self.releases_on_cover) self.gridLayout.addWidget(self.releases_on_cover, 2, 2, 1, 1) - self.artists_on_cover = QtGui.QRadioButton(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) + spacerItem5 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem5, 2, 3, 1, 1) + spacerItem6 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem6, 2, 4, 1, 1) + self.standardize_releases = QtGui.QRadioButton(self.rename_files_2) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHeightForWidth(self.standardize_releases.sizePolicy().hasHeightForWidth()) + self.standardize_releases.setSizePolicy(sizePolicy) + self.standardize_releases.setMinimumSize(QtCore.QSize(18, 18)) + self.standardize_releases.setText("") + self.standardize_releases.setObjectName("standardize_releases") + self.buttonGroup_2.addButton(self.standardize_releases) + self.gridLayout.addWidget(self.standardize_releases, 2, 5, 1, 1) + spacerItem7 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem7, 2, 6, 1, 1) + self.label_5 = QtGui.QLabel(self.rename_files_2) + self.label_5.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_5.setObjectName("label_5") + self.gridLayout.addWidget(self.label_5, 3, 0, 1, 1) + spacerItem8 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem8, 3, 1, 1, 1) + self.artists_on_cover = QtGui.QRadioButton(self.rename_files_2) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) sizePolicy.setHeightForWidth(self.artists_on_cover.sizePolicy().hasHeightForWidth()) self.artists_on_cover.setSizePolicy(sizePolicy) self.artists_on_cover.setMinimumSize(QtCore.QSize(18, 18)) @@ -188,37 +155,30 @@ class Ui_MetadataOptionsPage(object): self.artists_on_cover.setIconSize(QtCore.QSize(16, 16)) self.artists_on_cover.setChecked(True) self.artists_on_cover.setObjectName("artists_on_cover") + self.buttonGroup_3 = QtGui.QButtonGroup(MetadataOptionsPage) + self.buttonGroup_3.setObjectName("buttonGroup_3") self.buttonGroup_3.addButton(self.artists_on_cover) self.gridLayout.addWidget(self.artists_on_cover, 3, 2, 1, 1) - spacerItem = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem, 1, 1, 1, 1) - spacerItem1 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem1, 2, 1, 1, 1) - spacerItem2 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem2, 3, 1, 1, 1) - spacerItem3 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem3, 1, 4, 1, 1) - spacerItem4 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem4, 2, 4, 1, 1) - spacerItem5 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem5, 3, 4, 1, 1) - spacerItem6 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem6, 1, 3, 1, 1) - spacerItem7 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem7, 2, 3, 1, 1) - spacerItem8 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem8, 3, 3, 1, 1) - spacerItem9 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem9, 1, 6, 1, 1) - spacerItem10 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem10, 2, 6, 1, 1) - spacerItem11 = QtGui.QSpacerItem(40, 18, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + spacerItem9 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem9, 3, 3, 1, 1) + spacerItem10 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem10, 3, 4, 1, 1) + self.standardize_artists = QtGui.QRadioButton(self.rename_files_2) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHeightForWidth(self.standardize_artists.sizePolicy().hasHeightForWidth()) + self.standardize_artists.setSizePolicy(sizePolicy) + self.standardize_artists.setMinimumSize(QtCore.QSize(18, 18)) + self.standardize_artists.setText("") + self.standardize_artists.setObjectName("standardize_artists") + self.buttonGroup_3.addButton(self.standardize_artists) + self.gridLayout.addWidget(self.standardize_artists, 3, 5, 1, 1) + spacerItem11 = QtGui.QSpacerItem(33, 18, QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Minimum) self.gridLayout.addItem(spacerItem11, 3, 6, 1, 1) - self.verticalLayout_2.addWidget(self.rename_files_2) + spacerItem12 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem12, 2, 7, 1, 1) + self.verticalLayout.addWidget(self.rename_files_2) self.rename_files_3 = QtGui.QGroupBox(MetadataOptionsPage) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.rename_files_3.sizePolicy().hasHeightForWidth()) self.rename_files_3.setSizePolicy(sizePolicy) self.rename_files_3.setMinimumSize(QtCore.QSize(397, 0)) @@ -244,10 +204,9 @@ class Ui_MetadataOptionsPage(object): self.va_name = QtGui.QLineEdit(self.rename_files_3) self.va_name.setObjectName("va_name") self.gridlayout.addWidget(self.va_name, 1, 0, 1, 1) - self.verticalLayout_2.addWidget(self.rename_files_3) - self.verticalLayout.addLayout(self.verticalLayout_2) - spacerItem12 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.MinimumExpanding) - self.verticalLayout.addItem(spacerItem12) + self.verticalLayout.addWidget(self.rename_files_3) + spacerItem13 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.MinimumExpanding) + self.verticalLayout.addItem(spacerItem13) self.label_6.setBuddy(self.va_name_default) self.label_7.setBuddy(self.nat_name_default) @@ -265,7 +224,6 @@ class Ui_MetadataOptionsPage(object): self.release_ars.setText(_("Use release relationships")) self.track_ars.setText(_("Use track relationships")) self.folksonomy_tags.setText(_("Use folksonomy tags as genre")) - self.label_8.setText(_("Preferred release country:")) self.rename_files_2.setTitle(_("Standardization")) self.label.setText(_("As on cover")) self.label_2.setText(_("Standardized")) diff --git a/ui/options_matching.ui b/ui/options_matching.ui index 92cbf35b2..414493ea7 100644 --- a/ui/options_matching.ui +++ b/ui/options_matching.ui @@ -6,26 +6,17 @@ 0 0 - 382 - 498 + 413 + 612 - - 6 - - - 9 - Thresholds - - 9 - 2 @@ -110,265 +101,6 @@ - - - - Preferred release types - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - Album - - - - - - - Single - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - EP - - - - - - - Compilation - - - - - - - Soundtrack - - - - - - - Spokenword - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - Interview - - - - - - - Audiobook - - - - - - - Live - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - Remix - - - - - - - 100 - - - 50 - - - Qt::Horizontal - - - - - - - Other - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - Reset all - - - - - - - - diff --git a/ui/options_metadata.ui b/ui/options_metadata.ui index f994f1ff2..558feba97 100644 --- a/ui/options_metadata.ui +++ b/ui/options_metadata.ui @@ -11,610 +11,595 @@ - - 0 - - - - 0 + + + + 0 + 0 + - - QLayout::SetDefaultConstraint + + + 397 + 135 + - - 0 + + + false + - - - - - 0 - 0 - - - - - 397 - 135 - - - - - false - - - - Metadata - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - Translate foreign artist names to English where possible - - - - - - - Convert Unicode punctuation characters to ASCII - - - - - - - Use release relationships - - - - - - - Use track relationships - - - - - - - Use folksonomy tags as genre - - - - - - - Preferred release country: - - - - - - - - 0 - 0 - - - - - 373 - 16777215 - - - - - - - - - - - - 0 - 0 - - - - - 397 - 144 - - - - Standardization - - - false - - - - - 16 - 31 - 301 - 101 - + + Metadata + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + Translate foreign artist names to English where possible - - - - - - 0 - 0 - - - - - 75 - true - - - - As on cover - - - Qt::AlignCenter - - - - - - - - 75 - true - - - - Standardized - - - Qt::AlignCenter - - - - - - - Qt::LeftToRight - - - Track titles - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Release titles - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Artist names - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - true - - - - 0 - 0 - - - - - 18 - 18 - - - - - - - buttonGroup - - - - - - - - 0 - 0 - - - - - 18 - 18 - - - - - - - buttonGroup_2 - - - - - - - - 0 - 0 - - - - - 18 - 18 - - - - - - - buttonGroup_3 - - - - - - - - 0 - 0 - - - - - 18 - 18 - - - - Qt::LeftToRight - - - - - - - 16 - 16 - - - - true - - - buttonGroup - - - - - - - - 0 - 0 - - - - - 18 - 18 - - - - - - - true - - - buttonGroup_2 - - - - - - - - 0 - 0 - - - - - 18 - 18 - - - - - - - - 16 - 16 - - - - true - - - buttonGroup_3 - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - - 40 - 18 - - - - - - - - Qt::Horizontal - - - - 40 - 18 - - - - - - - - - - - - 0 - 0 - - - - - 397 - 0 - - - - Custom Fields - - - - 2 + + + + + Convert Unicode punctuation characters to ASCII - - - - Various artists: - - - va_name_default - - - - - - - Non-album tracks: - - - nat_name_default - - - - - - - - - - Default - - - - - - - Default - - - - - - - - - - + + + + + + Use release relationships + + + + + + + Use track relationships + + + + + + + Use folksonomy tags as genre + + + + +
+ + + + + + 0 + 0 + + + + + 397 + 144 + + + + Standardization + + + false + + + + + + + 0 + 0 + + + + + 75 + true + + + + As on cover + + + Qt::AlignCenter + + + + + + + + 75 + true + + + + Standardized + + + Qt::AlignCenter + + + + + + + Qt::LeftToRight + + + Track titles + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + + 0 + 0 + + + + + 18 + 18 + + + + Qt::LeftToRight + + + + + + + 16 + 16 + + + + true + + + buttonGroup + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + true + + + + 0 + 0 + + + + + 18 + 18 + + + + + + + buttonGroup + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + Release titles + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + + 0 + 0 + + + + + 18 + 18 + + + + + + + true + + + buttonGroup_2 + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + + 0 + 0 + + + + + 18 + 18 + + + + + + + buttonGroup_2 + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + Artist names + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + + 0 + 0 + + + + + 18 + 18 + + + + + + + + 16 + 16 + + + + true + + + buttonGroup_3 + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + + 0 + 0 + + + + + 18 + 18 + + + + + + + buttonGroup_3 + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 33 + 18 + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + + + + + 0 + 0 + + + + + 397 + 0 + + + + Custom Fields + + + + 2 + + + + + Various artists: + + + va_name_default + + + + + + + Non-album tracks: + + + nat_name_default + + + + + + + + + + Default + + + + + + + Default + + + + + + + + From 96661dbe28b37f88ff163b6703982658fbf0c3c5 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 30 Jun 2011 20:14:19 -0500 Subject: [PATCH 18/79] Oversight from the last commit --- picard/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/metadata.py b/picard/metadata.py index 4c64970ea..13ae609ae 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -108,7 +108,6 @@ class Metadata(object): total_countries = len(preferred_countries) if total_countries: score = 0.0 - total += weights["releasecountry"] if "country" in release.children: try: i = preferred_countries.index(release.country[0].text) @@ -140,6 +139,7 @@ class Metadata(object): else: score = 0.0 parts.append((score, weights["releasetype"])) + total += weights["releasetype"] return (total, parts) From 384f5d8aeb2e6d51586abf4e01f5c33b5c025f0b Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 4 Jul 2011 13:35:34 -0500 Subject: [PATCH 19/79] Missing file I forgot to commit... --- picard/ui/options/releases.py | 140 ++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 picard/ui/options/releases.py diff --git a/picard/ui/options/releases.py b/picard/ui/options/releases.py new file mode 100644 index 000000000..ada14b29c --- /dev/null +++ b/picard/ui/options/releases.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2006 Lukáš Lalinský +# +# 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 operator import itemgetter +from locale import strcoll +from PyQt4 import QtCore, QtGui +from picard.config import TextOption +from picard.util import load_release_type_scores, save_release_type_scores +from picard.ui.options import OptionsPage, OptionsCheckError, register_options_page +from picard.ui.ui_options_releases import Ui_ReleasesOptionsPage +from picard.const import RELEASE_COUNTRIES, RELEASE_FORMATS + + +class ReleasesOptionsPage(OptionsPage): + + NAME = "releases" + TITLE = N_("Preferred Releases") + PARENT = "metadata" + SORT_ORDER = 10 + ACTIVE = True + + options = [ + TextOption("setting", "release_type_scores", "Album 0.5 Single 0.5 EP 0.5 Compilation 0.5 Soundtrack 0.5 Spokenword 0.5 Interview 0.5 Audiobook 0.5 Live 0.5 Remix 0.5 Other 0.5"), + TextOption("setting", "preferred_release_countries", u""), + TextOption("setting", "preferred_release_formats", u""), + ] + + _release_type_sliders = {} + + def __init__(self, parent=None): + super(ReleasesOptionsPage, self).__init__(parent) + self.ui = Ui_ReleasesOptionsPage() + self.ui.setupUi(self) + self.connect(self.ui.reset_preferred_types_btn, QtCore.SIGNAL("clicked()"), self.reset_preferred_types) + self._release_type_sliders["Album"] = self.ui.prefer_album_score + self._release_type_sliders["Single"] = self.ui.prefer_single_score + self._release_type_sliders["EP"] = self.ui.prefer_ep_score + self._release_type_sliders["Compilation"] = self.ui.prefer_compilation_score + self._release_type_sliders["Soundtrack"] = self.ui.prefer_soundtrack_score + self._release_type_sliders["Spokenword"] = self.ui.prefer_spokenword_score + self._release_type_sliders["Interview"] = self.ui.prefer_interview_score + self._release_type_sliders["Audiobook"] = self.ui.prefer_audiobook_score + self._release_type_sliders["Live"] = self.ui.prefer_live_score + self._release_type_sliders["Remix"] = self.ui.prefer_remix_score + self._release_type_sliders["Other"] = self.ui.prefer_other_score + + self.connect(self.ui.add_countries, QtCore.SIGNAL("clicked()"), self.add_preferred_countries) + self.connect(self.ui.remove_countries, QtCore.SIGNAL("clicked()"), self.remove_preferred_countries) + self.connect(self.ui.add_formats, QtCore.SIGNAL("clicked()"), self.add_preferred_formats) + self.connect(self.ui.remove_formats, QtCore.SIGNAL("clicked()"), self.remove_preferred_formats) + self.ui.country_list.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) + self.ui.preferred_country_list.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) + self.ui.format_list.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) + self.ui.preferred_format_list.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) + + def load(self): + scores = load_release_type_scores(self.config.setting["release_type_scores"]) + for (release_type, release_type_slider) in self._release_type_sliders.iteritems(): + release_type_slider.setValue(int(scores.get(release_type, 0.5) * 100)) + + self._load_list_items("preferred_release_countries", RELEASE_COUNTRIES, + self.ui.country_list, self.ui.preferred_country_list) + self._load_list_items("preferred_release_formats", RELEASE_FORMATS, + self.ui.format_list, self.ui.preferred_format_list) + + def save(self): + scores = {} + for (release_type, release_type_slider) in self._release_type_sliders.iteritems(): + scores[release_type] = float(release_type_slider.value()) / 100.0 + self.config.setting["release_type_scores"] = save_release_type_scores(scores) + + self._save_list_items("preferred_release_countries", self.ui.preferred_country_list) + self._save_list_items("preferred_release_formats", self.ui.preferred_format_list) + + def reset_preferred_types(self): + for release_type_slider in self._release_type_sliders.values(): + release_type_slider.setValue(50) + + def add_preferred_countries(self): + self._move_selected_items(self.ui.country_list, self.ui.preferred_country_list) + + def remove_preferred_countries(self): + self._move_selected_items(self.ui.preferred_country_list, self.ui.country_list) + self.ui.country_list.sortItems() + + def add_preferred_formats(self): + self._move_selected_items(self.ui.format_list, self.ui.preferred_format_list) + + def remove_preferred_formats(self): + self._move_selected_items(self.ui.preferred_format_list, self.ui.format_list) + self.ui.format_list.sortItems() + + def _move_selected_items(self, list1, list2): + for item in list1.selectedItems(): + clone = item.clone() + list2.addItem(clone) + list1.takeItem(list1.row(item)) + + def _load_list_items(self, setting, source, list1, list2): + source_list = [(c[0], c[1]) for c in source.items()] + source_list.sort(key=itemgetter(1), cmp=strcoll) + saved_data = self.config.setting[setting].split(" ") + move = [] + for data, name in source_list: + item = QtGui.QListWidgetItem(name) + item.setData(QtCore.Qt.UserRole, QtCore.QVariant(data)) + try: + i = saved_data.index(data) + move.append((i, item)) + except: + list1.addItem(item) + move.sort(key=itemgetter(0)) + for i, item in move: + list2.addItem(item) + + def _save_list_items(self, setting, list1): + data = [] + for i in range(list1.count()): + item = list1.item(i) + data.append(unicode(item.data(QtCore.Qt.UserRole).toString())) + self.config.setting[setting] = " ".join(data) + + +register_options_page(ReleasesOptionsPage) From c1d6b2e0b3f6737938533a0069907a56b8e0bf7e Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 4 Jul 2011 13:38:00 -0500 Subject: [PATCH 20/79] More files I forgot to add. --- picard/ui/ui_options_releases.py | 220 ++++++++++++++++ ui/options_releases.ui | 430 +++++++++++++++++++++++++++++++ 2 files changed, 650 insertions(+) create mode 100644 picard/ui/ui_options_releases.py create mode 100644 ui/options_releases.ui diff --git a/picard/ui/ui_options_releases.py b/picard/ui/ui_options_releases.py new file mode 100644 index 000000000..3e4f04c56 --- /dev/null +++ b/picard/ui/ui_options_releases.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui/options_releases.ui' +# +# Created: Tue Jun 21 18:35:44 2011 +# by: PyQt4 UI code generator 4.8.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_ReleasesOptionsPage(object): + def setupUi(self, ReleasesOptionsPage): + ReleasesOptionsPage.setObjectName("ReleasesOptionsPage") + ReleasesOptionsPage.resize(551, 497) + self.verticalLayout_3 = QtGui.QVBoxLayout(ReleasesOptionsPage) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.type_group = QtGui.QGroupBox(ReleasesOptionsPage) + self.type_group.setObjectName("type_group") + self.gridLayout = QtGui.QGridLayout(self.type_group) + self.gridLayout.setVerticalSpacing(6) + self.gridLayout.setObjectName("gridLayout") + self.label = QtGui.QLabel(self.type_group) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 1) + self.prefer_album_score = QtGui.QSlider(self.type_group) + self.prefer_album_score.setMaximum(100) + self.prefer_album_score.setProperty("value", 50) + self.prefer_album_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_album_score.setObjectName("prefer_album_score") + self.gridLayout.addWidget(self.prefer_album_score, 0, 1, 1, 1) + self.label_2 = QtGui.QLabel(self.type_group) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 0, 2, 1, 1) + self.prefer_single_score = QtGui.QSlider(self.type_group) + self.prefer_single_score.setMaximum(100) + self.prefer_single_score.setProperty("value", 50) + self.prefer_single_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_single_score.setObjectName("prefer_single_score") + self.gridLayout.addWidget(self.prefer_single_score, 0, 3, 1, 1) + self.label_3 = QtGui.QLabel(self.type_group) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 0, 4, 1, 1) + self.prefer_ep_score = QtGui.QSlider(self.type_group) + self.prefer_ep_score.setMaximum(100) + self.prefer_ep_score.setProperty("value", 50) + self.prefer_ep_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_ep_score.setObjectName("prefer_ep_score") + self.gridLayout.addWidget(self.prefer_ep_score, 0, 5, 1, 1) + self.label_7 = QtGui.QLabel(self.type_group) + self.label_7.setObjectName("label_7") + self.gridLayout.addWidget(self.label_7, 1, 0, 1, 1) + self.prefer_compilation_score = QtGui.QSlider(self.type_group) + self.prefer_compilation_score.setMaximum(100) + self.prefer_compilation_score.setProperty("value", 50) + self.prefer_compilation_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_compilation_score.setObjectName("prefer_compilation_score") + self.gridLayout.addWidget(self.prefer_compilation_score, 1, 1, 1, 1) + self.label_8 = QtGui.QLabel(self.type_group) + self.label_8.setObjectName("label_8") + self.gridLayout.addWidget(self.label_8, 1, 2, 1, 1) + self.prefer_soundtrack_score = QtGui.QSlider(self.type_group) + self.prefer_soundtrack_score.setMaximum(100) + self.prefer_soundtrack_score.setProperty("value", 50) + self.prefer_soundtrack_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_soundtrack_score.setObjectName("prefer_soundtrack_score") + self.gridLayout.addWidget(self.prefer_soundtrack_score, 1, 3, 1, 1) + self.label_9 = QtGui.QLabel(self.type_group) + self.label_9.setObjectName("label_9") + self.gridLayout.addWidget(self.label_9, 1, 4, 1, 1) + self.prefer_spokenword_score = QtGui.QSlider(self.type_group) + self.prefer_spokenword_score.setMaximum(100) + self.prefer_spokenword_score.setProperty("value", 50) + self.prefer_spokenword_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_spokenword_score.setObjectName("prefer_spokenword_score") + self.gridLayout.addWidget(self.prefer_spokenword_score, 1, 5, 1, 1) + self.label_10 = QtGui.QLabel(self.type_group) + self.label_10.setObjectName("label_10") + self.gridLayout.addWidget(self.label_10, 2, 0, 1, 1) + self.prefer_interview_score = QtGui.QSlider(self.type_group) + self.prefer_interview_score.setMaximum(100) + self.prefer_interview_score.setProperty("value", 50) + self.prefer_interview_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_interview_score.setObjectName("prefer_interview_score") + self.gridLayout.addWidget(self.prefer_interview_score, 2, 1, 1, 1) + self.label_11 = QtGui.QLabel(self.type_group) + self.label_11.setObjectName("label_11") + self.gridLayout.addWidget(self.label_11, 2, 2, 1, 1) + self.prefer_audiobook_score = QtGui.QSlider(self.type_group) + self.prefer_audiobook_score.setMaximum(100) + self.prefer_audiobook_score.setProperty("value", 50) + self.prefer_audiobook_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_audiobook_score.setObjectName("prefer_audiobook_score") + self.gridLayout.addWidget(self.prefer_audiobook_score, 2, 3, 1, 1) + self.label_12 = QtGui.QLabel(self.type_group) + self.label_12.setObjectName("label_12") + self.gridLayout.addWidget(self.label_12, 2, 4, 1, 1) + self.prefer_live_score = QtGui.QSlider(self.type_group) + self.prefer_live_score.setMaximum(100) + self.prefer_live_score.setProperty("value", 50) + self.prefer_live_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_live_score.setObjectName("prefer_live_score") + self.gridLayout.addWidget(self.prefer_live_score, 2, 5, 1, 1) + self.label_13 = QtGui.QLabel(self.type_group) + self.label_13.setObjectName("label_13") + self.gridLayout.addWidget(self.label_13, 3, 0, 1, 1) + self.prefer_remix_score = QtGui.QSlider(self.type_group) + self.prefer_remix_score.setMaximum(100) + self.prefer_remix_score.setProperty("value", 50) + self.prefer_remix_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_remix_score.setObjectName("prefer_remix_score") + self.gridLayout.addWidget(self.prefer_remix_score, 3, 1, 1, 1) + self.label_14 = QtGui.QLabel(self.type_group) + self.label_14.setObjectName("label_14") + self.gridLayout.addWidget(self.label_14, 3, 2, 1, 1) + self.prefer_other_score = QtGui.QSlider(self.type_group) + self.prefer_other_score.setMaximum(100) + self.prefer_other_score.setSliderPosition(50) + self.prefer_other_score.setOrientation(QtCore.Qt.Horizontal) + self.prefer_other_score.setObjectName("prefer_other_score") + self.gridLayout.addWidget(self.prefer_other_score, 3, 3, 1, 1) + self.reset_preferred_types_btn = QtGui.QPushButton(self.type_group) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHeightForWidth(self.reset_preferred_types_btn.sizePolicy().hasHeightForWidth()) + self.reset_preferred_types_btn.setSizePolicy(sizePolicy) + self.reset_preferred_types_btn.setObjectName("reset_preferred_types_btn") + self.gridLayout.addWidget(self.reset_preferred_types_btn, 4, 5, 1, 1) + self.verticalLayout_3.addWidget(self.type_group) + self.country_group = QtGui.QGroupBox(ReleasesOptionsPage) + self.country_group.setObjectName("country_group") + self.horizontalLayout = QtGui.QHBoxLayout(self.country_group) + self.horizontalLayout.setSpacing(4) + self.horizontalLayout.setObjectName("horizontalLayout") + self.country_list = QtGui.QListWidget(self.country_group) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHeightForWidth(self.country_list.sizePolicy().hasHeightForWidth()) + self.country_list.setSizePolicy(sizePolicy) + self.country_list.setObjectName("country_list") + self.horizontalLayout.addWidget(self.country_list) + self.verticalLayout = QtGui.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.add_countries = QtGui.QPushButton(self.country_group) + self.add_countries.setObjectName("add_countries") + self.verticalLayout.addWidget(self.add_countries) + self.remove_countries = QtGui.QPushButton(self.country_group) + self.remove_countries.setObjectName("remove_countries") + self.verticalLayout.addWidget(self.remove_countries) + spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem1) + self.horizontalLayout.addLayout(self.verticalLayout) + self.preferred_country_list = QtGui.QListWidget(self.country_group) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHeightForWidth(self.preferred_country_list.sizePolicy().hasHeightForWidth()) + self.preferred_country_list.setSizePolicy(sizePolicy) + self.preferred_country_list.setDragEnabled(True) + self.preferred_country_list.setDragDropMode(QtGui.QAbstractItemView.InternalMove) + self.preferred_country_list.setObjectName("preferred_country_list") + self.horizontalLayout.addWidget(self.preferred_country_list) + self.verticalLayout_3.addWidget(self.country_group) + self.format_group = QtGui.QGroupBox(ReleasesOptionsPage) + self.format_group.setObjectName("format_group") + self.horizontalLayout_2 = QtGui.QHBoxLayout(self.format_group) + self.horizontalLayout_2.setSpacing(4) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.format_list = QtGui.QListWidget(self.format_group) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHeightForWidth(self.format_list.sizePolicy().hasHeightForWidth()) + self.format_list.setSizePolicy(sizePolicy) + self.format_list.setObjectName("format_list") + self.horizontalLayout_2.addWidget(self.format_list) + self.verticalLayout_2 = QtGui.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + spacerItem2 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem2) + self.add_formats = QtGui.QPushButton(self.format_group) + self.add_formats.setObjectName("add_formats") + self.verticalLayout_2.addWidget(self.add_formats) + self.remove_formats = QtGui.QPushButton(self.format_group) + self.remove_formats.setObjectName("remove_formats") + self.verticalLayout_2.addWidget(self.remove_formats) + spacerItem3 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem3) + self.horizontalLayout_2.addLayout(self.verticalLayout_2) + self.preferred_format_list = QtGui.QListWidget(self.format_group) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHeightForWidth(self.preferred_format_list.sizePolicy().hasHeightForWidth()) + self.preferred_format_list.setSizePolicy(sizePolicy) + self.preferred_format_list.setDragEnabled(True) + self.preferred_format_list.setDragDropMode(QtGui.QAbstractItemView.InternalMove) + self.preferred_format_list.setObjectName("preferred_format_list") + self.horizontalLayout_2.addWidget(self.preferred_format_list) + self.verticalLayout_3.addWidget(self.format_group) + + self.retranslateUi(ReleasesOptionsPage) + QtCore.QMetaObject.connectSlotsByName(ReleasesOptionsPage) + + def retranslateUi(self, ReleasesOptionsPage): + ReleasesOptionsPage.setWindowTitle(_("Form")) + self.type_group.setTitle(_("Preferred release types")) + self.label.setText(_("Album")) + self.label_2.setText(_("Single")) + self.label_3.setText(_("EP")) + self.label_7.setText(_("Compilation")) + self.label_8.setText(_("Soundtrack")) + self.label_9.setText(_("Spokenword")) + self.label_10.setText(_("Interview")) + self.label_11.setText(_("Audiobook")) + self.label_12.setText(_("Live")) + self.label_13.setText(_("Remix")) + self.label_14.setText(_("Other")) + self.reset_preferred_types_btn.setText(_("Reset all")) + self.country_group.setTitle(_("Preferred release countries")) + self.add_countries.setText(">") + self.remove_countries.setText("<") + self.format_group.setTitle(_("Preferred release formats")) + self.add_formats.setText(">") + self.remove_formats.setText("<") + diff --git a/ui/options_releases.ui b/ui/options_releases.ui new file mode 100644 index 000000000..ba63bee15 --- /dev/null +++ b/ui/options_releases.ui @@ -0,0 +1,430 @@ + + + ReleasesOptionsPage + + + + 0 + 0 + 551 + 497 + + + + Form + + + + + + Preferred release types + + + + 6 + + + + + Album + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + Single + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + EP + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + Compilation + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + Soundtrack + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + Spokenword + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + Interview + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + Audiobook + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + Live + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + Remix + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + Other + + + + + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + Reset all + + + + + + + + + + Preferred release countries + + + + 4 + + + + + + 0 + 0 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + > + + + + + + + < + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + 0 + + + + true + + + QAbstractItemView::InternalMove + + + + + + + + + + Preferred release formats + + + + 4 + + + + + + 0 + 0 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + > + + + + + + + < + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + 0 + + + + true + + + QAbstractItemView::InternalMove + + + + + + + + + + + From 7b23daee4758e69efa098df17e21155abe79eb22 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 5 Jul 2011 19:40:57 -0500 Subject: [PATCH 21/79] Changes to lookup/request queuing based on recent mb-devel discussion. --- picard/cluster.py | 22 ++-- picard/file.py | 28 +++-- picard/tagger.py | 23 +--- picard/webservice.py | 268 ++++++++++++++++++++----------------------- 4 files changed, 153 insertions(+), 188 deletions(-) diff --git a/picard/cluster.py b/picard/cluster.py index 1f0320b5b..aa06d9f60 100644 --- a/picard/cluster.py +++ b/picard/cluster.py @@ -40,7 +40,8 @@ class Cluster(QtCore.QObject, Item): self.hide_if_empty = hide_if_empty self.related_album = related_album self.files = [] - self.lookup_queued = False + + self.lookup_task = None # Weights for different elements when comparing a cluster to a release self.comparison_weights = { 'album' : 17, 'artist' : 6, 'totaltracks' : 5, 'releasecountry': 2, 'format': 2 } @@ -152,7 +153,7 @@ class Cluster(QtCore.QObject, Item): return reduce(lambda x, y: x + y[0] * y[1] / total, parts, 0.0) def _lookup_finished(self, document, http, error): - self._signal_lookup_finished() + self.lookup_task = None try: releases = document.metadata[0].release_list[0].release @@ -169,7 +170,7 @@ class Cluster(QtCore.QObject, Item): for release in releases: matches.append((self._compare_to_release(release), release)) matches.sort(reverse=True) - self.log.debug("Matches: %r", matches) + #self.log.debug("Matches: %r", matches) if matches[0][0] < self.config.setting['cluster_lookup_threshold']: self.tagger.window.set_statusbar_message(N_("No matching releases for cluster %s"), self.metadata['album'], timeout=3000) @@ -177,21 +178,20 @@ class Cluster(QtCore.QObject, Item): self.tagger.window.set_statusbar_message(N_("Cluster %s identified!"), self.metadata['album'], timeout=3000) self.tagger.move_files_to_album(self.files, matches[0][1].id) - def _signal_lookup_finished(self): - if self.lookup_queued: - self.lookup_queued = False - self.emit(QtCore.SIGNAL("lookup_finished")) - def lookup_metadata(self): """ Try to identify the cluster using the existing metadata. """ self.tagger.window.set_statusbar_message(N_("Looking up the metadata for cluster %s..."), self.metadata['album']) - QtCore.QTimer.singleShot(10000, self._signal_lookup_finished) - self.tagger.xmlws.find_releases(self._lookup_finished, + self.lookup_task = self.tagger.xmlws.find_releases(self._lookup_finished, artist=self.metadata.get('artist', ''), release=self.metadata.get('album', ''), tracks=str(len(self.files)), limit=25) + def clear_lookup_task(self): + if self.lookup_task: + self.tagger.xmlws.remove_task(self.lookup_task) + self.lookup_task = None + @staticmethod def cluster(files, threshold): artistDict = ClusterDict() @@ -260,8 +260,6 @@ class UnmatchedFiles(Cluster): self.tagger.window.enable_cluster(self.get_num_files() > 0) def lookup_metadata(self): - self.lookup_queued = False - self.emit(QtCore.SIGNAL("lookup_finished")) self.tagger.autotag(self.files) diff --git a/picard/file.py b/picard/file.py index e7bda162a..b08cf6949 100644 --- a/picard/file.py +++ b/picard/file.py @@ -84,7 +84,8 @@ class File(LockableObject, Item): self.similarity = 1.0 self.parent = None - self.lookup_queued = False + + self.lookup_task = None self.comparison_weights = {"title": 13, "artist": 4, "album": 5, "length": 10, "totaltracks": 4, "releasetype": 20, @@ -327,6 +328,7 @@ class File(LockableObject, Item): def move(self, parent): if parent != self.parent: self.log.debug("Moving %r from %r to %r", self, self.parent, parent) + self.clear_lookup_task() if self.parent: self.clear_pending() self.parent.remove_file(self) @@ -487,7 +489,7 @@ class File(LockableObject, Item): return max(scores, key=lambda x: x[0]) def _lookup_finished(self, lookuptype, document, http, error): - self._signal_lookup_finished() + self.lookup_task = None if self.state == File.REMOVED: return @@ -515,7 +517,7 @@ class File(LockableObject, Item): score, release = self._compare_to_track(track) matches.append((score, track, release)) matches.sort(reverse=True) - self.log.debug("Track matches: %r", matches) + #self.log.debug("Track matches: %r", matches) if lookuptype != 'puid': threshold = self.config.setting['file_lookup_threshold'] @@ -535,25 +537,22 @@ class File(LockableObject, Item): else: self.tagger.move_file_to_nat(self, track.id, node=track) - def _signal_lookup_finished(self): - if self.lookup_queued: - self.lookup_queued = False - self.emit(QtCore.SIGNAL("lookup_finished")) - def lookup_trackid(self, trackid): """ Try to identify the file using the trackid. """ - self.tagger.xmlws.get_track_by_id(trackid, partial(self._lookup_finished, 'trackid')) + self.clear_lookup_task() + self.lookup_task = self.tagger.xmlws.get_track_by_id(trackid, partial(self._lookup_finished, 'trackid')) def lookup_puid(self, puid): """ Try to identify the file using the PUID. """ self.tagger.window.set_statusbar_message(N_("Looking up the PUID for file %s..."), self.filename) - self.tagger.xmlws.lookup_puid(puid, partial(self._lookup_finished, 'puid')) + self.clear_lookup_task() + self.lookup_task = self.tagger.xmlws.lookup_puid(puid, partial(self._lookup_finished, 'puid')) def lookup_metadata(self): """ Try to identify the file using the existing metadata. """ self.tagger.window.set_statusbar_message(N_("Looking up the metadata for file %s..."), self.filename) - QtCore.QTimer.singleShot(10000, self._signal_lookup_finished) - self.tagger.xmlws.find_tracks(partial(self._lookup_finished, 'metadata'), + self.clear_lookup_task() + self.lookup_task = self.tagger.xmlws.find_tracks(partial(self._lookup_finished, 'metadata'), track=self.metadata.get('title', ''), artist=self.metadata.get('artist', ''), release=self.metadata.get('album', ''), @@ -562,6 +561,11 @@ class File(LockableObject, Item): qdur=str(self.metadata.length / 2000), limit=25) + def clear_lookup_task(self): + if self.lookup_task: + self.tagger.xmlws.remove_task(self.lookup_task) + self.lookup_task = None + def set_pending(self): if self.state == File.REMOVED: return diff --git a/picard/tagger.py b/picard/tagger.py index 1eaa667c7..5d5d49665 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -209,9 +209,6 @@ class Tagger(QtGui.QApplication): self.nats = None - self.lookup_queue = queue.Queue() - self.lookup_running = False - def setup_gettext(self, localedir): """Setup locales, load translations, install gettext functions.""" if self.config.setting["ui_language"]: @@ -498,7 +495,7 @@ class Tagger(QtGui.QApplication): """Remove files from the tagger.""" for file in files: if self.files.has_key(file.filename): - self.lookup_queue.remove(file) + file.clear_lookup_task() self.analyze_queue.remove(file.filename) del self.files[file.filename] file.remove(from_parent) @@ -516,7 +513,7 @@ class Tagger(QtGui.QApplication): self.log.debug("Removing %r", cluster) files = list(cluster.files) cluster.files = [] - self.lookup_queue.remove(cluster) + cluster.clear_lookup_task() self.remove_files(files, from_parent=False) self.clusters.remove(cluster) self.emit(QtCore.SIGNAL("cluster_removed"), cluster) @@ -582,20 +579,8 @@ class Tagger(QtGui.QApplication): def autotag(self, objects): for obj in objects: - if isinstance(obj, (File, Cluster)): - if not obj.lookup_queued: - obj.lookup_queued = True - self.lookup_queue.put(obj) - self.connect(obj, QtCore.SIGNAL("lookup_finished"), self._run_next_lookup) - if not self.lookup_running: - self.lookup_running = True - self._run_next_lookup() - - def _run_next_lookup(self): - if self.lookup_queue.qsize() > 0: - self.lookup_queue.get().lookup_metadata() - else: - self.lookup_running = False + if isinstance(obj, (File, Cluster)) and not obj.lookup_task: + obj.lookup_metadata() # ======================================================================= # Clusters diff --git a/picard/webservice.py b/picard/webservice.py index 05d7298c7..a68169ee0 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -27,13 +27,14 @@ import os import sys import re import traceback +from collections import deque, defaultdict from PyQt4 import QtCore, QtNetwork, QtXml from picard import version_string from picard.util import partial from picard.const import PUID_SUBMIT_HOST, PUID_SUBMIT_PORT -REQUEST_DELAY = 1000 +REQUEST_DELAY = defaultdict(lambda: 1000) USER_AGENT_STRING = 'MusicBrainz%%20Picard-%s' % version_string @@ -41,10 +42,6 @@ def _escape_lucene_query(text): return re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])', r'\\\1', text) -def _node_name(name): - return re.sub('[^a-zA-Z0-9]', '_', unicode(name)) - - def _wrap_xml_metadata(data): return ('' + '%s' % data) @@ -75,13 +72,15 @@ class XmlHandler(QtXml.QXmlDefaultHandler): def init(self): self.document = XmlNode() self.node = self.document + _node_name_re = re.compile('[^a-zA-Z0-9]') + self._node_name = lambda n: _node_name_re.sub('_', unicode(n)) self.path = [] def startElement(self, namespace, name, qname, attrs): node = XmlNode() for i in xrange(attrs.count()): - node.attribs[_node_name(attrs.localName(i))] = unicode(attrs.value(i)) - self.node.children.setdefault(_node_name(name), []).append(node) + node.attribs[self._node_name(attrs.localName(i))] = unicode(attrs.value(i)) + self.node.children.setdefault(self._node_name(name), []).append(node) self.path.append(self.node) self.node = node return True @@ -95,19 +94,6 @@ class XmlHandler(QtXml.QXmlDefaultHandler): return True -class XmlWebServiceRequest(object): - - def __init__(self, request, reply, handler, xml=True): - self.request = request - self.reply = reply - self.handler = handler - self.xml = xml - self.finished = False - - def errorString(self): - return str(self.reply.errorString()) - - class XmlWebService(QtCore.QObject): """ Signals: @@ -122,9 +108,10 @@ class XmlWebService(QtCore.QObject): self.manager.connect(self.manager, QtCore.SIGNAL("authenticationRequired(QNetworkReply *, QAuthenticator *)"), self._site_authenticate) self.manager.connect(self.manager, QtCore.SIGNAL("proxyAuthenticationRequired(QNetworkProxy *, QAuthenticator *)"), self._proxy_authenticate) self._last_request_times = {} - self._active_hosts = set() self._active_requests = {} - self._queue = [] + self._high_priority_queues = {} + self._low_priority_queues = {} + self._hosts = [] self._timer = QtCore.QTimer(self) self._timer.setSingleShot(True) self._timer.timeout.connect(self._run_next_task) @@ -139,93 +126,63 @@ class XmlWebService(QtCore.QObject): self.proxy.setPassword(self.config.setting["proxy_password"]) self.manager.setProxy(self.proxy) - def _prepare_request(self, method, host, port, path, username=None, password=None): + def _start_request(self, method, send, host, port, path, data, handler, xml, mblogin=False): self.log.debug("%s http://%s:%d%s", method, host, port, path) - self.url = QtCore.QUrl.fromEncoded("http://%s:%d%s" % (host, port, path)) - if username: - self.url.setUserName(username) - self.url.setPassword(password) - self.genrequest = QtNetwork.QNetworkRequest(self.url) - self.genrequest.setRawHeader("User-Agent", "MusicBrainz-Picard/%s" % version_string) - if method == "POST": - contenttype = "application/x-www-form-urlencoded" if host == "ofa.musicdns.org" else "application/xml; charset=utf-8" - self.genrequest.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, contenttype) - return self.genrequest - - def _start_request(self, host, port, request): - key = host, port + url = QtCore.QUrl.fromEncoded("http://%s:%d%s" % (host, port, path)) + if mblogin: + url.setUserName(self.config.setting["username"]) + url.setPassword(self.config.setting["password"]) + request = QtNetwork.QNetworkRequest(url) + request.setRawHeader("User-Agent", "MusicBrainz-Picard/%s" % version_string) + if method == "POST" and host == self.config.setting["server_host"]: + request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/xml; charset=utf-8") + reply = send(request, data) if data is not None else send(request) + key = (host, port) self._last_request_times[key] = QtCore.QTime.currentTime() - #print "starting request", key, request.reply, self._last_request_times[key] - request.key = key - self._active_requests[request.reply] = request - self._active_hosts.add(key) - - def _finish_request(self): - for reply, request in self._active_requests.items(): - if request.finished: - self._active_hosts.remove(request.key) - del self._active_requests[reply] - self._timer.start(0) + self._active_requests[reply] = (request, handler, xml) + return True def _process_reply(self, reply): try: - #print "finishing request", reply - request = self._active_requests.get(reply) - if request is None: - print "**** request not found", reply.request().url(), reply - return - request.finished = True - error = int(reply.error()) - if request.handler is not None: - if error: - #print "ERROR", reply.error(), reply.errorString() - #for name in reply.rawHeaderList(): - # print name, reply.rawHeader(name) - self.log.debug("HTTP Error: %d", error) - if request.xml: - xml_handler = XmlHandler() - xml_handler.init() - xml_reader = QtXml.QXmlSimpleReader() - xml_reader.setContentHandler(xml_handler) - xml_input = QtXml.QXmlInputSource(reply) - xml_reader.parse(xml_input) - request.handler(xml_handler.document, request, error) - else: - request.handler(str(reply.readAll()), request, error) - reply.close() - finally: - QtCore.QTimer.singleShot(0, self._finish_request) + request, handler, xml = self._active_requests.pop(reply) + except KeyError: + self.log.error("Error: Request not found for %s" % str(reply.request().url().toString())) + return + error = int(reply.error()) + if handler is not None: + if error: + #print "ERROR", reply.error(), reply.errorString() + #for name in reply.rawHeaderList(): + # print name, reply.rawHeader(name) + self.log.debug("HTTP Error: %d", error) + if xml: + xml_handler = XmlHandler() + xml_handler.init() + xml_reader = QtXml.QXmlSimpleReader() + xml_reader.setContentHandler(xml_handler) + xml_input = QtXml.QXmlInputSource(reply) + xml_reader.parse(xml_input) + handler(xml_handler.document, request, error) + else: + handler(str(reply.readAll()), request, error) + reply.close() - def _get(self, host, port, path, handler, xml=True, mblogin=False): - if mblogin: - self.username = self.config.setting["username"] - self.password = self.config.setting["password"] - request = self._prepare_request("GET", host, port, path, self.username, self.password) - else: - request = self._prepare_request("GET", host, port, path) - reply = self.manager.get(request) - self._start_request(host, port, XmlWebServiceRequest(request, reply, handler, xml)) - return True + def get(self, host, port, path, handler, xml=True, priority=False, important=False, mblogin=False): + func = partial(self._start_request, "GET", self.manager.get, host, port, path, None, handler, xml, mblogin) + return self.add_task(func, host, port, priority, important=important) - def _post(self, host, port, path, data, handler, mblogin=True): + def post(self, host, port, path, data, handler, xml=True, priority=False, important=False, mblogin=True): self.log.debug("POST-DATA %r", data) - if mblogin: - self.username = self.config.setting["username"] - self.password = self.config.setting["password"] - request = self._prepare_request("POST", host, port, path, self.username, self.password) - else: - request = self._prepare_request("POST", host, port, path) - reply = self.manager.post(request, data) - self._start_request(host, port, XmlWebServiceRequest(request, reply, handler)) - return True + func = partial(self._start_request, "POST", self.manager.post, host, port, path, data, handler, xml, mblogin) + return self.add_task(func, host, port, priority, important=important) - def get(self, host, port, path, handler, xml=True, position=None, mblogin=False): - func = partial(self._get, host, port, path, handler, xml, mblogin) - self.add_task(func, host, port, position) + def put(self, host, port, path, data, handler, priority=False, important=False, mblogin=True): + func = partial(self._start_request, "PUT", self.manager.put, host, port, path, data, handler, False, mblogin) + return self.add_task(func, host, port, priority, important=important) - def post(self, host, port, path, data, handler, position=None, mblogin=True): - func = partial(self._post, host, port, path, data, handler, mblogin) - self.add_task(func, host, port, position) + def delete(self, host, port, path, handler, priority=False, important=False, mblogin=True): + func = partial(self._start_request, "DELETE", self.manager.deleteResource, host, port, path, None, handler, False, mblogin) + return self.add_task(func, host, port, priority, important=important) def _site_authenticate(self, reply, authenticator): self.emit(QtCore.SIGNAL("authentication_required"), reply, authenticator) @@ -234,63 +191,85 @@ class XmlWebService(QtCore.QObject): self.emit(QtCore.SIGNAL("proxyAuthentication_required"), proxy, authenticator) def stop(self): - for request in self._active_requests.itervalues(): + self._high_priority_queues = {} + self._low_priority_queues = {} + for request in self._active_requests.values(): request.reply.abort() def _run_next_task(self): - delay, index, key = sys.maxint, None, None - now = QtCore.QTime.currentTime() - for i, (k, task) in enumerate(self._queue): - if k == key or k in self._active_hosts: + delay = sys.maxint + for key in self._hosts: + queue = self._high_priority_queues.get(key) or self._low_priority_queues.get(key) + if not queue: continue - last = self._last_request_times.get(k) - last_ms = last.msecsTo(now) if last is not None else REQUEST_DELAY - if last_ms >= REQUEST_DELAY: - self.log.debug("Last request to %s was %d ms ago, starting another one", k, last_ms) - del self._queue[i] - task() - return - d = REQUEST_DELAY - last_ms + now = QtCore.QTime.currentTime() + last = self._last_request_times.get(key) + request_delay = REQUEST_DELAY[key] + last_ms = last.msecsTo(now) if last is not None else request_delay + if last_ms >= request_delay: + self.log.debug("Last request to %s was %d ms ago, starting another one", key, last_ms) + d = request_delay + queue.popleft()() + else: + d = request_delay - last_ms + self.log.debug("Waiting %d ms before starting another request to %s", d, key) if d < delay: - delay, index, key = d, i, k - if index is not None and not self._timer.isActive(): - self.log.debug("Waiting %d ms before starting another request to %s", - delay, key) + delay = d + if delay < sys.maxint: self._timer.start(delay) - def add_task(self, func, host, port, position=None): + def add_task(self, func, host, port, priority, important=False): key = (host, port) - if position is None: - self._queue.append((key, func)) + if key not in self._hosts: + self._hosts.append(key) + if priority: + queues = self._high_priority_queues else: - self._queue.insert(position, (key, func)) - if key not in self._active_hosts: + queues = self._low_priority_queues + queues.setdefault(key, deque()) + if important: + queues[key].appendleft(func) + else: + queues[key].append(func) + if not self._timer.isActive(): self._timer.start(0) + return (key, func, priority) - def _get_by_id(self, entitytype, entityid, handler, inc=[], mblogin=False): + def remove_task(self, task): + key, func, priority = task + if priority: + queue = self._high_priority_queues[key] + else: + queue = self._low_priority_queues[key] + try: + queue.remove(func) + except: + pass + + def _get_by_id(self, entitytype, entityid, handler, inc=[], params=[], priority=False, important=False, mblogin=False): host = self.config.setting["server_host"] port = self.config.setting["server_port"] - path = "/ws/2/%s/%s?inc=%s" % (entitytype, entityid, "+".join(inc)) - if entitytype == "discid": path += "&cdstubs=no" - self.get(host, port, path, handler, mblogin=mblogin) + path = "/ws/2/%s/%s?inc=%s&%s" % (entitytype, entityid, "+".join(inc), "&".join(params)) + return self.get(host, port, path, handler, priority=priority, important=important, mblogin=mblogin) - def get_release_group_by_id(self, releasegroupid, handler): + def get_release_group_by_id(self, releasegroupid, handler, priority=True, important=False): inc = ['releases', 'media'] - self._get_by_id('release-group', releasegroupid, handler, inc) + return self._get_by_id('release-group', releasegroupid, handler, inc, priority=priority, important=important) - def get_release_by_id(self, releaseid, handler, inc=[], mblogin=False): - self._get_by_id('release', releaseid, handler, inc, mblogin=mblogin) + def get_release_by_id(self, releaseid, handler, inc=[], priority=True, important=False, mblogin=False): + return self._get_by_id('release', releaseid, handler, inc, priority=priority, important=important, mblogin=mblogin) - def get_track_by_id(self, trackid, handler): + def get_track_by_id(self, trackid, handler, priority=False, important=False): inc = ['releases', 'release-groups', 'media', 'artist-credits'] - self._get_by_id('recording', trackid, handler, inc) + return self._get_by_id('recording', trackid, handler, inc, priority=priority, important=important) - def lookup_puid(self, puid, handler): + def lookup_puid(self, puid, handler, priority=False, important=False): inc = ['releases', 'release-groups', 'media', 'artist-credits'] - self._get_by_id('puid', puid, handler, inc) + return self._get_by_id('puid', puid, handler, inc, priority=False, important=False) - def lookup_discid(self, discid, handler): - self._get_by_id('discid', discid, handler, ['artist-credits', 'labels']) + def lookup_discid(self, discid, handler, priority=True, important=True): + inc = ['artist-credits', 'labels'] + return self._get_by_id('discid', discid, handler, inc, params=["cdstubs=no"], priority=priority, important=important) def _find(self, entitytype, handler, kwargs): host = self.config.setting["server_host"] @@ -309,19 +288,19 @@ class XmlWebService(QtCore.QObject): value = str(QtCore.QUrl.toPercentEncoding(QtCore.QString(value))) params.append('%s=%s' % (str(name), value)) path = "/ws/2/%s/?%s" % (entitytype, "&".join(params)) - self.get(host, port, path, handler) + return self.get(host, port, path, handler) def find_releases(self, handler, **kwargs): - self._find('release', handler, kwargs) + return self._find('release', handler, kwargs) def find_tracks(self, handler, **kwargs): - self._find('recording', handler, kwargs) + return self._find('recording', handler, kwargs) def submit_puids(self, puids, handler): path = '/ws/2/recording/?client=' + USER_AGENT_STRING recordings = ''.join(['' % i for i in puids.items()]) data = _wrap_xml_metadata('%s' % recordings) - self.post(PUID_SUBMIT_HOST, PUID_SUBMIT_PORT, path, data, handler) + return self.post(PUID_SUBMIT_HOST, PUID_SUBMIT_PORT, path, data, handler, priority=True, important=True) def submit_ratings(self, ratings, handler): host = self.config.setting['server_host'] @@ -330,16 +309,15 @@ class XmlWebService(QtCore.QObject): recordings = (''.join(['%s' % (i[1], j*20) for i, j in ratings.items() if i[0] == 'recording'])) data = _wrap_xml_metadata('%s' % recordings) - self.post(host, port, path, data, handler) + return self.post(host, port, path, data, handler, priority=True, important=True) def query_musicdns(self, handler, **kwargs): - host = 'ofa.musicdns.org' - port = 80 + host, port = 'ofa.musicdns.org', 80 filters = [] for name, value in kwargs.items(): value = str(QtCore.QUrl.toPercentEncoding(value)) filters.append('%s=%s' % (str(name), value)) - self.post(host, port, '/ofa/1/track/', '&'.join(filters), handler, mblogin = False) + return self.post(host, port, '/ofa/1/track/', '&'.join(filters), handler, mblogin=False) - def download(self, host, port, path, handler, position=None): - self.get(host, port, path, handler, xml=False, position=position) + def download(self, host, port, path, handler, priority=False, important=False): + return self.get(host, port, path, handler, xml=False, priority=priority, important=important) From 29556216763cef165af78367d7c64d02b5ff2670 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 6 Jul 2011 11:23:14 -0500 Subject: [PATCH 22/79] Small fixes to webservice.py. --- picard/webservice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/picard/webservice.py b/picard/webservice.py index a68169ee0..ab5e54c39 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -162,9 +162,9 @@ class XmlWebService(QtCore.QObject): xml_reader.setContentHandler(xml_handler) xml_input = QtXml.QXmlInputSource(reply) xml_reader.parse(xml_input) - handler(xml_handler.document, request, error) + handler(xml_handler.document, reply, error) else: - handler(str(reply.readAll()), request, error) + handler(str(reply.readAll()), reply, error) reply.close() def get(self, host, port, path, handler, xml=True, priority=False, important=False, mblogin=False): @@ -193,7 +193,7 @@ class XmlWebService(QtCore.QObject): def stop(self): self._high_priority_queues = {} self._low_priority_queues = {} - for request in self._active_requests.values(): + for request, h, x in self._active_requests.values(): request.reply.abort() def _run_next_task(self): From 10eaf70b4a0597e7f72f6fdb3e71db1ae75c485f Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 6 Jul 2011 13:39:45 -0500 Subject: [PATCH 23/79] Fix the last.fm plugin for the recent webservice.py changes. --- contrib/plugins/lastfm/__init__.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/contrib/plugins/lastfm/__init__.py b/contrib/plugins/lastfm/__init__.py index b13ad98df..1c3d22f99 100644 --- a/contrib/plugins/lastfm/__init__.py +++ b/contrib/plugins/lastfm/__init__.py @@ -3,8 +3,8 @@ PLUGIN_NAME = u'Last.fm' PLUGIN_AUTHOR = u'Lukáš Lalinský' PLUGIN_DESCRIPTION = u'Use tags from Last.fm as genre.' -PLUGIN_VERSION = "0.2" -PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15"] +PLUGIN_VERSION = "0.3" +PLUGIN_API_VERSIONS = ["0.15"] from PyQt4 import QtGui, QtCore from picard.metadata import register_album_metadata_processor, register_track_metadata_processor @@ -12,6 +12,10 @@ from picard.ui.options import register_options_page, OptionsPage from picard.config import BoolOption, IntOption, TextOption from picard.plugins.lastfm.ui_options_lastfm import Ui_LastfmOptionsPage from picard.util import partial +from picard.webservice import REQUEST_DELAY + +REQUEST_DELAY[(None, None)] = 0 +# REQUEST_DELAY[("ws.audioscrobbler.com", 80)] = 500 _cache = {} # TODO: move this to an options page @@ -36,7 +40,7 @@ def _tags_finalize(album, metadata, tags, next): metadata["genre"] = tags -def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, http, error): +def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, reply, error): try: try: intags = data.toptags[0].tag except AttributeError: intags = [] @@ -51,7 +55,7 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, ht except KeyError: pass if name.lower() not in ignore: tags.append(name.title()) - _cache[str(http.currentRequest().path())] = tags + _cache[str(reply.url().path())] = tags _tags_finalize(album, metadata, current + tags, next) finally: album._requests -= 1 @@ -61,13 +65,14 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, ht def get_tags(album, metadata, path, min_usage, ignore, next, current): """Get tags from an URL.""" try: - if path in _cache: - _tags_finalize(album, metadata, current + _cache[path], next) + decoded = str(QtCore.QUrl.fromPercentEncoding(path)) + if decoded in _cache: + _tags_finalize(album, metadata, current + _cache[decoded], next) else: album._requests += 1 album.tagger.xmlws.get("ws.audioscrobbler.com", 80, path, partial(_tags_downloaded, album, metadata, min_usage, ignore, next, current), - position=1) + priority=True, important=True) finally: album._requests -= 1 album._finalize_loading(None) @@ -112,7 +117,7 @@ def process_track(album, metadata, release, track): func = partial(get_artist_tags_func, []) if func: album._requests += 1 - tagger.xmlws.add_task(func, position=1) + tagger.xmlws.add_task(func, None, None, priority=True, important=True) class LastfmOptionsPage(OptionsPage): From 172de5f7b9434f15e24c491bf1ba980a6990f600 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 6 Jul 2011 14:26:18 -0500 Subject: [PATCH 24/79] Slight modifications to the last.fm plugin. --- contrib/plugins/lastfm/__init__.py | 37 +++++++++++------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/contrib/plugins/lastfm/__init__.py b/contrib/plugins/lastfm/__init__.py index 1c3d22f99..e7ce599c4 100644 --- a/contrib/plugins/lastfm/__init__.py +++ b/contrib/plugins/lastfm/__init__.py @@ -12,9 +12,7 @@ from picard.ui.options import register_options_page, OptionsPage from picard.config import BoolOption, IntOption, TextOption from picard.plugins.lastfm.ui_options_lastfm import Ui_LastfmOptionsPage from picard.util import partial -from picard.webservice import REQUEST_DELAY - -REQUEST_DELAY[(None, None)] = 0 +# from picard.webservice import REQUEST_DELAY # REQUEST_DELAY[("ws.audioscrobbler.com", 80)] = 500 _cache = {} @@ -29,7 +27,6 @@ TITLE_CASE = True def _tags_finalize(album, metadata, tags, next): if next: - album._requests += 1 next(tags) else: tags = list(set(tags)) @@ -64,19 +61,14 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, re def get_tags(album, metadata, path, min_usage, ignore, next, current): """Get tags from an URL.""" - try: - decoded = str(QtCore.QUrl.fromPercentEncoding(path)) - if decoded in _cache: - _tags_finalize(album, metadata, current + _cache[decoded], next) - else: - album._requests += 1 - album.tagger.xmlws.get("ws.audioscrobbler.com", 80, path, - partial(_tags_downloaded, album, metadata, min_usage, ignore, next, current), - priority=True, important=True) - finally: - album._requests -= 1 - album._finalize_loading(None) - return False + decoded = str(QtCore.QUrl.fromPercentEncoding(path)) + if decoded in _cache: + _tags_finalize(album, metadata, current + _cache[decoded], next) + else: + album._requests += 1 + album.tagger.xmlws.get("ws.audioscrobbler.com", 80, path, + partial(_tags_downloaded, album, metadata, min_usage, ignore, next, current), + priority=True, important=True) def encode_str(s): @@ -88,13 +80,13 @@ def encode_str(s): def get_track_tags(album, metadata, artist, track, min_usage, ignore, next, current): """Get track top tags.""" path = "/1.0/track/%s/%s/toptags.xml" % (encode_str(artist), encode_str(track)) - return get_tags(album, metadata, path, min_usage, ignore, next, current) + get_tags(album, metadata, path, min_usage, ignore, next, current) def get_artist_tags(album, metadata, artist, min_usage, ignore, next, current): """Get artist top tags.""" path = "/1.0/artist/%s/toptags.xml" % (encode_str(artist),) - return get_tags(album, metadata, path, min_usage, ignore, next, current) + get_tags(album, metadata, path, min_usage, ignore, next, current) def process_track(album, metadata, release, track): @@ -112,12 +104,9 @@ def process_track(album, metadata, release, track): else: get_artist_tags_func = None if title and use_track_tags: - func = partial(get_track_tags, album, metadata, artist, title, min_tag_usage, ignore_tags, get_artist_tags_func, []) + get_track_tags(album, metadata, artist, title, min_tag_usage, ignore_tags, get_artist_tags_func, []) elif get_artist_tags_func: - func = partial(get_artist_tags_func, []) - if func: - album._requests += 1 - tagger.xmlws.add_task(func, None, None, priority=True, important=True) + get_artist_tags_func([]) class LastfmOptionsPage(OptionsPage): From d3bc904304fc4de270809e3b2f2e0266e97a6784 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 6 Jul 2011 14:37:31 -0500 Subject: [PATCH 25/79] Fix the coverart plugin. --- contrib/plugins/coverart.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contrib/plugins/coverart.py b/contrib/plugins/coverart.py index 48d4c4bff..ebbd3fecf 100644 --- a/contrib/plugins/coverart.py +++ b/contrib/plugins/coverart.py @@ -1,4 +1,4 @@ -""" +""" A small plugin to download cover art for any releseas that have a CoverArtLink or ASIN relation. @@ -6,9 +6,9 @@ CoverArtLink or ASIN relation. Changelog: [2008-04-15] Refactored the code to be similar to the server code (hartzell, phw) - + [2008-03-10] Added CDBaby support (phw) - + [2007-09-06] Added Jamendo support (phw) [2007-04-24] Moved parsing code into here @@ -18,7 +18,7 @@ Changelog: [2007-04-23] Moved it to use the bzr picard Took the hack out Added Amazon ASIN support - + [2007-04-23] Initial plugin, uses a hack that relies on Python being installed and musicbrainz2 for the query. @@ -28,8 +28,8 @@ PLUGIN_NAME = 'Cover Art Downloader' PLUGIN_AUTHOR = 'Oliver Charles, Philipp Wolfer' PLUGIN_DESCRIPTION = '''Downloads cover artwork for releases that have a CoverArtLink or ASIN.''' -PLUGIN_VERSION = "0.6.3" -PLUGIN_API_VERSIONS = ["0.12", "0.15"] +PLUGIN_VERSION = "0.6.4" +PLUGIN_API_VERSIONS = ["0.15"] from picard.metadata import register_album_metadata_processor from picard.util import partial, mimetype @@ -125,7 +125,7 @@ def coverart(album, metadata, release, try_list=None): if relation_list.target_type == 'url': for relation in relation_list.relation: _process_url_relation(try_list, relation) - + # Use the URL of a cover art link directly if relation.type == 'cover art link' or relation.type == 'has_cover_art_at': _try_list_append_image_url(try_list, QUrl(relation.target[0].text)) @@ -140,7 +140,7 @@ def coverart(album, metadata, release, try_list=None): album.tagger.xmlws.download( try_list[0]['host'], try_list[0]['port'], try_list[0]['path'], partial(_coverart_downloaded, album, metadata, release, try_list[1:]), - position=1) + priority=True, important=True) def _process_url_relation(try_list, relation): From 1c5efc90f4d6a6602cd16f756ec79d9d6d0a29b2 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 6 Jul 2011 15:11:37 -0500 Subject: [PATCH 26/79] Use a dictionary for request methods, assume all posts/puts/deletes are priority/important by default. --- picard/webservice.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/picard/webservice.py b/picard/webservice.py index ab5e54c39..73e915019 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -115,6 +115,12 @@ class XmlWebService(QtCore.QObject): self._timer = QtCore.QTimer(self) self._timer.setSingleShot(True) self._timer.timeout.connect(self._run_next_task) + self._request_methods = { + "GET": self.manager.get, + "POST": self.manager.post, + "PUT": self.manager.put, + "DELETE": self.manager.deleteResource + } def setup_proxy(self): self.proxy = QtNetwork.QNetworkProxy() @@ -126,7 +132,7 @@ class XmlWebService(QtCore.QObject): self.proxy.setPassword(self.config.setting["proxy_password"]) self.manager.setProxy(self.proxy) - def _start_request(self, method, send, host, port, path, data, handler, xml, mblogin=False): + def _start_request(self, method, host, port, path, data, handler, xml, mblogin=False): self.log.debug("%s http://%s:%d%s", method, host, port, path) url = QtCore.QUrl.fromEncoded("http://%s:%d%s" % (host, port, path)) if mblogin: @@ -136,6 +142,7 @@ class XmlWebService(QtCore.QObject): request.setRawHeader("User-Agent", "MusicBrainz-Picard/%s" % version_string) if method == "POST" and host == self.config.setting["server_host"]: request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/xml; charset=utf-8") + send = self._request_methods[method] reply = send(request, data) if data is not None else send(request) key = (host, port) self._last_request_times[key] = QtCore.QTime.currentTime() @@ -168,20 +175,20 @@ class XmlWebService(QtCore.QObject): reply.close() def get(self, host, port, path, handler, xml=True, priority=False, important=False, mblogin=False): - func = partial(self._start_request, "GET", self.manager.get, host, port, path, None, handler, xml, mblogin) + func = partial(self._start_request, "GET", host, port, path, None, handler, xml, mblogin) return self.add_task(func, host, port, priority, important=important) - def post(self, host, port, path, data, handler, xml=True, priority=False, important=False, mblogin=True): + def post(self, host, port, path, data, handler, xml=True, priority=True, important=True, mblogin=True): self.log.debug("POST-DATA %r", data) - func = partial(self._start_request, "POST", self.manager.post, host, port, path, data, handler, xml, mblogin) + func = partial(self._start_request, "POST", host, port, path, data, handler, xml, mblogin) return self.add_task(func, host, port, priority, important=important) - def put(self, host, port, path, data, handler, priority=False, important=False, mblogin=True): - func = partial(self._start_request, "PUT", self.manager.put, host, port, path, data, handler, False, mblogin) + def put(self, host, port, path, data, handler, priority=True, important=True, mblogin=True): + func = partial(self._start_request, "PUT", host, port, path, data, handler, False, mblogin) return self.add_task(func, host, port, priority, important=important) - def delete(self, host, port, path, handler, priority=False, important=False, mblogin=True): - func = partial(self._start_request, "DELETE", self.manager.deleteResource, host, port, path, None, handler, False, mblogin) + def delete(self, host, port, path, handler, priority=True, important=True, mblogin=True): + func = partial(self._start_request, "DELETE", host, port, path, None, handler, False, mblogin) return self.add_task(func, host, port, priority, important=important) def _site_authenticate(self, reply, authenticator): @@ -300,7 +307,7 @@ class XmlWebService(QtCore.QObject): path = '/ws/2/recording/?client=' + USER_AGENT_STRING recordings = ''.join(['' % i for i in puids.items()]) data = _wrap_xml_metadata('%s' % recordings) - return self.post(PUID_SUBMIT_HOST, PUID_SUBMIT_PORT, path, data, handler, priority=True, important=True) + return self.post(PUID_SUBMIT_HOST, PUID_SUBMIT_PORT, path, data, handler) def submit_ratings(self, ratings, handler): host = self.config.setting['server_host'] @@ -309,7 +316,7 @@ class XmlWebService(QtCore.QObject): recordings = (''.join(['%s' % (i[1], j*20) for i, j in ratings.items() if i[0] == 'recording'])) data = _wrap_xml_metadata('%s' % recordings) - return self.post(host, port, path, data, handler, priority=True, important=True) + return self.post(host, port, path, data, handler) def query_musicdns(self, handler, **kwargs): host, port = 'ofa.musicdns.org', 80 From 5c17a82a2874496ca4a262fff83b9e2dfb33caeb Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 6 Jul 2011 15:15:56 -0500 Subject: [PATCH 27/79] Requests weren't being aborted correctly when Picard exited. --- picard/webservice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/webservice.py b/picard/webservice.py index 73e915019..e55c71071 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -200,8 +200,8 @@ class XmlWebService(QtCore.QObject): def stop(self): self._high_priority_queues = {} self._low_priority_queues = {} - for request, h, x in self._active_requests.values(): - request.reply.abort() + for reply in self._active_requests.keys(): + reply.abort() def _run_next_task(self): delay = sys.maxint From b98ffe689a028410fde5a7708bec8d9eff1c3e3f Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 11 Jul 2011 14:34:29 -0500 Subject: [PATCH 28/79] Add collections-related functions to webservice.py. --- picard/webservice.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/picard/webservice.py b/picard/webservice.py index e55c71071..f2e559773 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -328,3 +328,26 @@ class XmlWebService(QtCore.QObject): def download(self, host, port, path, handler, priority=False, important=False): return self.get(host, port, path, handler, xml=False, priority=priority, important=important) + + def get_collection(self, id, handler): + host = self.config.setting["server_host"] + port = self.config.setting["server_port"] + path = "/ws/2/collection" + if id is not None: + path += "/%s/releases" % id + return self.get(host, port, path, handler, priority=True, important=True, mblogin=True) + + def get_collection_list(self, handler): + return self.get_collection(None, handler) + + def _collection_request(self, id, releases): + path = "/ws/2/collection/%s/releases/%s?client=%s" % (id, ";".join(releases), USER_AGENT_STRING) + return (self.config.setting['server_host'], self.config.setting['server_port'], path) + + def put_to_collection(self, id, releases, handler): + host, port, path = self._collection_request(id, releases) + return self.put(host, port, path, "", handler) + + def delete_from_collection(self, id, releases, handler): + host, port, path = self._collection_request(id, releases) + return self.delete(host, port, path, handler) From cb3789da14f92401dd56c017f45450e09d841153 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 11 Jul 2011 21:00:08 -0500 Subject: [PATCH 29/79] - Add initial support for managing MusicBrainz collections within Picard. It's available as a right-hand pane. Drag & drop and context menus can be used to add or remove releases. - Cache "num_linked_files" for tracks since it's calculated often, but not updated often. --- picard/album.py | 2 +- picard/collection.py | 144 ++++++++++++++++++++++ picard/track.py | 7 +- picard/ui/itemviews.py | 256 ++++++++++++++++++++++++++++++++++++++-- picard/ui/mainwindow.py | 36 ++++-- 5 files changed, 427 insertions(+), 18 deletions(-) create mode 100644 picard/collection.py diff --git a/picard/album.py b/picard/album.py index 4dd3fba3f..89215984c 100644 --- a/picard/album.py +++ b/picard/album.py @@ -361,7 +361,7 @@ class Album(DataObject, Item): if not self.tracks: return False for track in self.tracks: - if len(track.linked_files) != 1: + if track.num_linked_files != 1: return False else: return True diff --git a/picard/collection.py b/picard/collection.py new file mode 100644 index 000000000..617d3113e --- /dev/null +++ b/picard/collection.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2011 Michael Wiencek +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from PyQt4 import QtCore +from picard.util import partial + + +class CollectionList(QtCore.QObject): + + def __init__(self, view): + QtCore.QObject.__init__(self) + self.view = view + self.collections = {} + self.loaded = False + + def _parse_collection_list(self, document): + collection_list = document.metadata[0].collection_list[0] + collections = collection_list.collection + + for collection in collections: + id = collection.id + name = collection.name[0].text + count = int(collection.release_list[0].count) + self.collections[id] = Collection(id, name, count, self) + + def _collection_list_request_finished(self, document, reply, error): + if error: + self.log.error("%r", unicode(reply.errorString())) + self.view.window.show_collections_action.setChecked(False) + self.view.window.collections_panel.hide() + else: + self._parse_collection_list(document) + self.view.add_collections(self.collections) + self.loaded = True + + def load(self): + self.collections = {} + self.loaded = False + self.tagger.xmlws.get_collection_list(self._collection_list_request_finished) + + +class Collection(QtCore.QObject): + + def __init__(self, id, name, count, list): + self.id = id + self.name = name + self.count = count + self.releases = set() + self.pending_adds = set() + self.pending_removes = set() + self.collection_list = list + self.widget = None + self.release_widgets = {} + self.load() + + def _add_releases(self, ids): + self.releases.update(ids) + self.count += len(ids) + + def _remove_releases(self, ids): + self.releases.difference_update(ids) + self.count -= len(ids) + + def add_releases(self, releases): + ids = releases.keys() + self._add_releases(ids) + self.pending_adds.update(ids) + self.collection_list.view.add_releases(releases, self, pending=True) + self.tagger.xmlws.put_to_collection(self.id, ids, partial(self._add_request_finished, ids)) + + def remove_releases(self, ids): + self._remove_releases(ids) + self.pending_removes.update(ids) + self.color_pending_releases(ids, True) + self.tagger.xmlws.delete_from_collection(self.id, ids, partial(self._remove_request_finished, ids)) + + def load(self): + self.tagger.xmlws.get_collection(self.id, self._collection_request_finished) + + def _add_request_finished(self, ids, document, reply, error): + self.pending_adds.difference_update(ids) + if error: + self.log.error("%r", unicode(reply.errorString())) + self._remove_releases(ids) + self.collection_list.view.remove_releases(ids, self) + else: + self.widget.update_text() + self.color_pending_releases(ids, False) + + def _remove_request_finished(self, ids, document, reply, error): + self.pending_removes.difference_update(ids) + if error: + self.log.error("%r", unicode(reply.errorString())) + self._add_releases(ids) + self.color_pending_releases(ids, False) + else: + ids = set(ids) - self.pending_adds + self.collection_list.view.remove_releases(ids, self) + + def color_pending_releases(self, ids, pending): + if not pending: + ids = set(ids) - self.pending_adds - self.pending_removes + for id in ids: + self.release_widgets[id].color_pending(pending) + + def _collection_request_finished(self, document, reply, error): + if error: + self.log.error("%r", unicode(reply.errorString())) + else: + self._parse_collection(document) + self.widget.update_text() + + def _parse_collection(self, document): + collection = document.metadata[0].collection[0] + self.name = collection.name[0].text + release_list = collection.release_list[0] + releases = {} + if release_list.count != "0": + release_nodes = release_list.release + for node in release_nodes: + title = node.title[0].text + date = node.date[0].text if "date" in node.children else "" + country = node.country[0].text if "country" in node.children else "" + barcode = node.barcode[0].text if "barcode" in node.children else "" + release = (title, date, country, barcode) + releases[node.id] = release + self.releases.add(node.id) + self.collection_list.view.add_releases(releases, self) diff --git a/picard/track.py b/picard/track.py index eec22e6e4..70932ebf1 100644 --- a/picard/track.py +++ b/picard/track.py @@ -42,6 +42,7 @@ class Track(DataObject): DataObject.__init__(self, id) self.album = album self.linked_files = [] + self.num_linked_files = 0 self.metadata = Metadata() def __repr__(self): @@ -50,6 +51,7 @@ class Track(DataObject): def add_file(self, file): if file not in self.linked_files: self.linked_files.append(file) + self.num_linked_files += 1 self.album._add_file(self, file) self.update_file_metadata(file) @@ -69,6 +71,7 @@ class Track(DataObject): if file not in self.linked_files: return self.linked_files.remove(file) + self.num_linked_files -= 1 file.metadata.copy(file.saved_metadata) self.album._remove_file(self, file) self.update() @@ -84,7 +87,7 @@ class Track(DataObject): yield file def is_linked(self): - return len(self.linked_files)>0 + return self.num_linked_files > 0 def can_save(self): """Return if this object can be saved.""" @@ -118,7 +121,7 @@ class Track(DataObject): return False def similarity(self): - if len(self.linked_files) == 1: + if self.num_linked_files == 1: return self.linked_files[0].similarity else: return 1 diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 9880eb31d..3693f811a 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -24,7 +24,8 @@ 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.collection import CollectionList, Collection +from picard.util import encode_filename, icontheme, partial, webbrowser2 from picard.config import Option, TextOption from picard.plugin import ExtensionPoint from picard.const import RELEASE_COUNTRIES @@ -332,7 +333,7 @@ class BaseTreeView(QtGui.QTreeWidget): self.connect(self, QtCore.SIGNAL("doubleClicked(QModelIndex)"), self.activate_item) - def switch_release_version(self, album): + def _switch_release_version(self, album): index = self.sender().data().toInt()[0] album.switch_release_version(album.other_versions[index]) @@ -347,7 +348,7 @@ class BaseTreeView(QtGui.QTreeWidget): if isinstance(obj, Track): menu.addAction(self.window.edit_tags_action) plugin_actions = list(_track_actions) - if len(obj.linked_files) == 1: + if obj.num_linked_files == 1: plugin_actions.extend(_file_actions) if isinstance(obj, NonAlbumTrack): menu.addAction(self.window.refresh_action) @@ -371,7 +372,7 @@ class BaseTreeView(QtGui.QTreeWidget): if isinstance(obj, Album) and not isinstance(obj, NatAlbum): releases_menu = QtGui.QMenu(_("&Other versions"), menu) - self._switch_release_version = partial(self.switch_release_version, obj) + switch_release_version = partial(self._switch_release_version, obj) for i, version in enumerate(obj.other_versions): name = [] if "date" in version: @@ -387,7 +388,7 @@ class BaseTreeView(QtGui.QTreeWidget): action.setCheckable(True) if obj.id == version["mbid"]: action.setChecked(True) - self.connect(action, QtCore.SIGNAL("triggered(bool)"), self._switch_release_version) + self.connect(action, QtCore.SIGNAL("triggered(bool)"), switch_release_version) if releases_menu.isEmpty(): text = _('No other versions') if obj.rgloaded else _('Loading...') action = releases_menu.addAction(text) @@ -395,6 +396,55 @@ class BaseTreeView(QtGui.QTreeWidget): menu.addSeparator() menu.addMenu(releases_menu) + collection_list = self.window.collections_panel.collection_list + + if collection_list.loaded: + selected_releases = {} + + for item in self.selectedItems(): + obj = self.panel.object_from_item(item) + if isinstance(obj, Album) and obj.loaded: + m = obj.metadata + selected_releases[obj.id] = (m["album"], m["date"], m["releasecountry"], m["barcode"]) + + if selected_releases: + collections_menu = QtGui.QMenu(_("Collections"), menu) + selected_ids = set(selected_releases.keys()) + + def nextCheckState(checkbox, collection): + pending = collection.pending_adds | collection.pending_removes + if selected_ids & pending: + return + difference = selected_ids - collection.releases + if not difference: + collection.remove_releases(selected_releases) + checkbox.setCheckState(QtCore.Qt.Unchecked) + else: + releases = {id: selected_releases[id] for id in selected_releases if id in difference} + collection.add_releases(releases) + checkbox.setCheckState(QtCore.Qt.Checked) + + for collection in collection_list.collections.values(): + action = QtGui.QWidgetAction(collections_menu) + checkbox = QtGui.QCheckBox(collection.name) + checkbox.setTristate(True) + action.setDefaultWidget(checkbox) + collections_menu.addAction(action) + + difference = selected_ids - collection.releases + + if not difference: + checkbox.setCheckState(QtCore.Qt.Checked) + elif difference == selected_ids: + checkbox.setCheckState(QtCore.Qt.Unchecked) + else: + checkbox.setCheckState(QtCore.Qt.PartiallyChecked) + + checkbox.nextCheckState = partial(nextCheckState, checkbox, collection) + + if not collections_menu.isEmpty(): + menu.addMenu(collections_menu) + if plugin_actions: plugin_menu = QtGui.QMenu(_("&Plugins"), menu) plugin_menu.addActions(plugin_actions) @@ -440,6 +490,13 @@ class BaseTreeView(QtGui.QTreeWidget): """List of MIME types accepted by this view.""" return ["text/uri-list", "application/picard.file-list", "application/picard.album-list"] + def dragEnterEvent(self, event): + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + else: + event.acceptProposedAction() + def startDrag(self, supportedActions): """Start drag, *without* using pixmap.""" items = self.selectedItems() @@ -551,7 +608,7 @@ class BaseTreeView(QtGui.QTreeWidget): # application/picard.album-list albums = data.data("application/picard.album-list") if albums: - albums = [self.tagger.get_album_by_id(albumsId) for albumsId in str(albums).split("\n")] + albums = [self.tagger.load_album(id) for id in str(albums).split("\n")] self.drop_albums(albums, target) handled = True return handled @@ -625,7 +682,7 @@ class AlbumTreeView(BaseTreeView): except KeyError: self.log.debug("Item for %r not found", track) return - if len(track.linked_files) == 1: + if track.num_linked_files == 1: file = track.linked_files[0] color = self.track_colors[file.state] icon = self.panel.decide_file_icon(file) @@ -641,7 +698,7 @@ class AlbumTreeView(BaseTreeView): #Add linked files (there will either be 0 or >1) oldnum = item.childCount() - newnum = len(track.linked_files) + newnum = track.num_linked_files # remove old items if oldnum > newnum: for i in range(oldnum - newnum): @@ -735,3 +792,186 @@ class AlbumTreeView(BaseTreeView): self.panel.unregister_object(album) if album == self.tagger.nats: self.tagger.nats = None + + +class CollectionTreeItem(QtGui.QTreeWidgetItem): + + def __init__(self, parent, collection): + QtGui.QTreeWidgetItem.__init__(self, parent) + self.collection = collection + self.id = collection.id + font = self.font(0) + font.setBold(True) + self.setFont(0, font) + self.update_text(pending=True) + + def update_text(self, pending=False): + name, count = self.collection.name, self.collection.count + end = "releases" if count != 1 else "release" + color = QtGui.QColor("#808080" if pending else "#000") + self.setTextColor(0, color) + self.setText(0, "%s (%d %s)" % (name, count, end)) + + +class CollectionReleaseTreeItem(QtGui.QTreeWidgetItem): + + def __init__(self, parent, collection, release, id): + QtGui.QTreeWidgetItem.__init__(self, parent) + self.collection = collection + self.release = release + for i, text in enumerate(release): + self.setText(i, text) + self.id = id + + def color_pending(self, pending): + color = QtGui.QColor("#808080" if pending else "#000") + for i in xrange(self.columnCount()): + self.setTextColor(i, color) + + +class CollectionTreeView(QtGui.QTreeWidget): + + def __init__(self, window, parent): + QtGui.QTreeWidget.__init__(self, parent) + self.window = window + self.setHeaderLabels(["Title", "Date", "Country", "Barcode"]) + self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setSortingEnabled(True) + self.refresh_action = QtGui.QAction(icontheme.lookup("view-refresh", icontheme.ICON_SIZE_MENU), _("&Refresh"), self) + self.connect(self.refresh_action, QtCore.SIGNAL("triggered()"), self.refresh) + self.collection_list = CollectionList(self) + + def showEvent(self, event): + if not self.collection_list.loaded: + self.collection_list.load() + QtGui.QTreeView.showEvent(self, event) + + def refresh(self): + while True: + item = self.takeTopLevelItem(0) + if item is None: + break + self.collection_list.load() + + def add_collections(self, collections): + for id, collection in collections.iteritems(): + item = CollectionTreeItem(self, collection) + collection.widget = item + self.resizeColumnToContents(0) + + def add_releases(self, releases, collection, pending=False): + item = collection.widget + for id, release in releases.items(): + if id not in collection.pending_removes: + release_item = CollectionReleaseTreeItem(item, collection, release, id) + collection.release_widgets[id] = release_item + release_item.color_pending(pending) + self.resizeColumnToContents(2) + + def remove_releases(self, ids, collection): + item = collection.widget + for id in ids: + release_item = collection.release_widgets.pop(id) + item.removeChild(release_item) + item.update_text() + + def contextMenuEvent(self, event): + menu = QtGui.QMenu(self) + menu.addAction(self.refresh_action) + releases = {} + for item in self.selectedItems(): + if isinstance(item, CollectionReleaseTreeItem): + collection_id = item.collection.id + releases.setdefault(collection_id, []) + releases[collection_id].append(item.id) + if releases: + def _remove_releases(): + for cid, rids in releases.iteritems(): + collection = self.collection_list.collections[cid] + collection.remove_releases(rids) + remove_action = QtGui.QAction(icontheme.lookup("list-remove"), _("&Remove releases"), self) + self.connect(remove_action, QtCore.SIGNAL("triggered()"), _remove_releases) + menu.addAction(remove_action) + current_item = self.currentItem() + if current_item: + menu.addSeparator() + open_action = QtGui.QAction(_("&View on MusicBrainz"), self) + self.connect(open_action, QtCore.SIGNAL("triggered()"), partial(self.open_in_browser, current_item)) + menu.addAction(open_action) + menu.exec_(event.globalPos()) + event.accept() + + def dragEnterEvent(self, event): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + def supportedDropActions(self): + return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction + + def mimeTypes(self): + return ["application/picard.album-list", "application/picard.collection-list"] + + def startDrag(self, supportedActions): + items = self.selectedItems() + if items: + drag = QtGui.QDrag(self) + drag.setMimeData(self.mimeData(items)) + drag.start(supportedActions) + + def mimeData(self, items): + """Return MIME data for specified items.""" + ids = [] + data = [] + for item in items: + if isinstance(item, CollectionReleaseTreeItem): + ids.append(item.id) + release = [item.id] + release.extend(item.release) + data.append("\n".join(release)) + mimeData = QtCore.QMimeData() + mimeData.setData("application/picard.album-list", "\n".join(ids)) + mimeData.setData("application/picard.collection-list", "\n".join(data)) + return mimeData + + def dropEvent(self, event): + return QtGui.QTreeView.dropEvent(self, event) + + def dropMimeData(self, parent, index, data, action): + if parent is None: + return False + collection = parent.collection + releases = {} + if data.hasFormat("application/picard.album-list"): + mbids = set(map(str, data.data("application/picard.album-list").split("\n"))) + mbids.difference_update(collection.releases) + for mbid in mbids: + album = self.tagger.get_album_by_id(mbid) + if album is not None and album.loaded: + m = album.metadata + releases[album.id] = (m["album"], m["date"], m["releasecountry"], m["barcode"]) + if data.hasFormat("application/picard.collection-list"): + items = map(unicode, data.data("application/picard.collection-list").split("\n")) + while items: + id = str(items[0]) + if id not in collection.releases: + releases[id] = tuple(items[1:5]) + items = items[5:] + if releases: + collection.add_releases(releases) + return True + return False + + def open_in_browser(self, item): + if isinstance(item, CollectionReleaseTreeItem): + entity = "release" + elif isinstance(item, CollectionTreeItem): + entity = "collection" + else: + return + setting = self.window.config.setting + host, port = setting["server_host"], setting["server_port"] + url = "http://%s:%s/%s/%s" % (host, port, entity, item.id) + webbrowser2.open(url) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 765b056cd..b57209c3f 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -30,7 +30,7 @@ from picard.cluster import Cluster from picard.config import Option, BoolOption, TextOption from picard.formats import supported_formats from picard.ui.coverartbox import CoverArtBox -from picard.ui.itemviews import MainPanel +from picard.ui.itemviews import MainPanel, CollectionTreeView from picard.ui.metadatabox import MetadataBox from picard.ui.filebrowser import FileBrowser from picard.ui.tagsfromfilenames import TagsFromFileNamesDialog @@ -58,6 +58,7 @@ class MainWindow(QtGui.QMainWindow): BoolOption("persist", "window_maximized", False), BoolOption("persist", "view_cover_art", False), BoolOption("persist", "view_file_browser", False), + BoolOption("persist", "view_collections", False), TextOption("persist", "current_directory", ""), ] @@ -92,6 +93,11 @@ class MainWindow(QtGui.QMainWindow): self.panel.insertWidget(0, self.file_browser) self.panel.restore_state() + self.collections_panel = CollectionTreeView(self, self.panel) + if not self.show_collections_action.isChecked(): + self.collections_panel.hide() + self.panel.insertWidget(3, self.collections_panel) + self.orig_metadata_box = MetadataBox(self, _("Original Metadata"), True) self.orig_metadata_box.disable() self.metadata_box = MetadataBox(self, _("New Metadata"), False) @@ -251,8 +257,7 @@ class MainWindow(QtGui.QMainWindow): self.exit_action = QtGui.QAction(_(u"E&xit"), self) # TR: Keyboard shortcut for "Exit" self.exit_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+Q"))) - self.connect(self.exit_action, QtCore.SIGNAL("triggered()"), - self.close) + self.connect(self.exit_action, QtCore.SIGNAL("triggered()"), self.close) self.remove_action = QtGui.QAction(icontheme.lookup('list-remove'), _(u"&Remove"), self) self.remove_action.setStatusTip(_(u"Remove selected files/albums")) @@ -267,6 +272,13 @@ class MainWindow(QtGui.QMainWindow): self.show_file_browser_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+B"))) self.connect(self.show_file_browser_action, QtCore.SIGNAL("triggered()"), self.show_file_browser) + self.show_collections_action = QtGui.QAction(_(u"Collections"), self) + self.show_collections_action.setCheckable(True) + if self.config.persist["view_collections"]: + self.show_collections_action.setChecked(True) + self.show_collections_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+E"))) + self.connect(self.show_collections_action, QtCore.SIGNAL("triggered()"), self.show_collections) + self.show_cover_art_action = QtGui.QAction(_(u"&Cover Art"), self) self.show_cover_art_action.setCheckable(True) if self.config.persist["view_cover_art"]: @@ -373,6 +385,7 @@ class MainWindow(QtGui.QMainWindow): menu.addAction(self.remove_action) menu = self.menuBar().addMenu(_(u"&View")) menu.addAction(self.show_file_browser_action) + menu.addAction(self.show_collections_action) menu.addAction(self.show_cover_art_action) menu.addSeparator() menu.addAction(self.toolbar.toggleViewAction()) @@ -407,10 +420,8 @@ class MainWindow(QtGui.QMainWindow): self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) else: self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) - self.cd_lookup_action.setEnabled(len(get_cdrom_drives()) > 0) - def create_toolbar(self): self.toolbar = toolbar = self.addToolBar(_(u"&Toolbar")) self.update_toolbar_style() @@ -663,14 +674,14 @@ class MainWindow(QtGui.QMainWindow): statusBar += _(" (Error: %s)") % obj.error file = obj elif isinstance(obj, Track): - if len(obj.linked_files) == 1: + if obj.num_linked_files == 1: file = obj.linked_files[0] orig_metadata = file.orig_metadata metadata = file.metadata statusBar = "%s (%d%%)" % (file.filename, file.similarity * 100) if file.state == file.ERROR: statusBar += _(" (Error: %s)") % file.error - elif len(obj.linked_files) == 0: + elif obj.num_linked_files == 0: metadata = obj.metadata else: metadata = obj.metadata @@ -705,6 +716,17 @@ class MainWindow(QtGui.QMainWindow): else: self.file_browser.hide() + def show_collections(self): + """Show/hide the Collections.""" + if self.show_collections_action.isChecked(): + sizes = self.panel.sizes() + if sizes[3] == 0: + sizes[3] = sum(sizes) / 3 + self.panel.setSizes(sizes) + self.collections_panel.show() + else: + self.collections_panel.hide() + def show_password_dialog(self, reply, authenticator): dialog = PasswordDialog(authenticator, reply, parent=self) dialog.exec_() From e92c9df781afa5421a33c8494d036ef31ee5038e Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 11 Jul 2011 21:15:41 -0500 Subject: [PATCH 30/79] Don't set mime data for drags without any releases. --- picard/ui/itemviews.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 3693f811a..a9c227890 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -932,8 +932,10 @@ class CollectionTreeView(QtGui.QTreeWidget): release.extend(item.release) data.append("\n".join(release)) mimeData = QtCore.QMimeData() - mimeData.setData("application/picard.album-list", "\n".join(ids)) - mimeData.setData("application/picard.collection-list", "\n".join(data)) + if ids: + mimeData.setData("application/picard.album-list", "\n".join(ids)) + if data: + mimeData.setData("application/picard.collection-list", "\n".join(data)) return mimeData def dropEvent(self, event): From 24e466ffbd5d1d27a45864186e85a311ee07952a Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 11 Jul 2011 22:16:17 -0500 Subject: [PATCH 31/79] Restore the collections panel state on startup. --- picard/ui/itemviews.py | 1 - picard/ui/mainwindow.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index a9c227890..54e9aafc4 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -934,7 +934,6 @@ class CollectionTreeView(QtGui.QTreeWidget): mimeData = QtCore.QMimeData() if ids: mimeData.setData("application/picard.album-list", "\n".join(ids)) - if data: mimeData.setData("application/picard.collection-list", "\n".join(data)) return mimeData diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index b57209c3f..939fa292c 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -91,12 +91,11 @@ class MainWindow(QtGui.QMainWindow): if not self.show_file_browser_action.isChecked(): self.file_browser.hide() self.panel.insertWidget(0, self.file_browser) - self.panel.restore_state() - self.collections_panel = CollectionTreeView(self, self.panel) if not self.show_collections_action.isChecked(): self.collections_panel.hide() self.panel.insertWidget(3, self.collections_panel) + self.panel.restore_state() self.orig_metadata_box = MetadataBox(self, _("Original Metadata"), True) self.orig_metadata_box.disable() @@ -146,6 +145,7 @@ class MainWindow(QtGui.QMainWindow): self.config.persist["window_maximized"] = isMaximized self.config.persist["view_cover_art"] = self.show_cover_art_action.isChecked() self.config.persist["view_file_browser"] = self.show_file_browser_action.isChecked() + self.config.persist["view_collections"] = self.show_collections_action.isChecked() self.file_browser.save_state() self.panel.save_state() From 45df45d13a92cdb9167d6b6895cadba5be5cef79 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 12 Jul 2011 13:44:47 -0500 Subject: [PATCH 32/79] Add more columns to the collections panel. --- picard/album.py | 22 ++++++------ picard/collection.py | 79 +++++++++++++++++++++++++++++++++--------- picard/mbxml.py | 15 ++++++++ picard/ui/itemviews.py | 76 ++++++++++++++++++++-------------------- picard/webservice.py | 3 +- 5 files changed, 129 insertions(+), 66 deletions(-) diff --git a/picard/album.py b/picard/album.py index 89215984c..397ba16bd 100644 --- a/picard/album.py +++ b/picard/album.py @@ -28,8 +28,8 @@ from picard.script import ScriptParser from picard.ui.item import Item from picard.util import format_time, partial, translate_artist, queue, mbid_validate from picard.cluster import Cluster -from picard.mbxml import release_to_metadata, track_to_metadata -from picard.const import RELEASE_FORMATS, VARIOUS_ARTISTS_ID +from picard.mbxml import release_to_metadata, track_to_metadata, media_formats_from_node +from picard.const import VARIOUS_ARTISTS_ID class Album(DataObject, Item): @@ -38,6 +38,8 @@ class Album(DataObject, Item): DataObject.__init__(self, id) self.metadata = Metadata() self.tracks = [] + self.format_str = "" + self.tracks_str = "" self.loaded = False self.rgloaded = False self._files = 0 @@ -77,6 +79,8 @@ class Album(DataObject, Item): m = self._new_metadata m.length = 0 release_to_metadata(release_node, m, config=self.config, album=self) + + self.format_str = media_formats_from_node(release_node.medium_list[0]) if self._discid: m['musicbrainz_discid'] = self._discid @@ -111,6 +115,7 @@ class Album(DataObject, Item): ignore_tags = [s.strip() for s in self.config.setting['ignore_tags'].split(',')] artists = set() + track_counts = [] m['totaldiscs'] = release_node.medium_list[0].count @@ -118,6 +123,7 @@ class Album(DataObject, Item): discnumber = medium.position[0].text track_list = medium.track_list[0] totaltracks = track_list.count + track_counts.append(totaltracks) discsubtitle = medium.title[0].text if "title" in medium.children else "" format = medium.format[0].text if "format" in medium.children else "" @@ -139,6 +145,8 @@ class Album(DataObject, Item): artists.add(tm['musicbrainz_artistid']) m.length += tm.length + self.tracks_str = " + ".join(track_counts) + if len(artists) > 1: for t in self._new_tracks: t.metadata['compilation'] = '1' @@ -162,15 +170,7 @@ class Album(DataObject, Item): if "country" in release.children: version["country"] = release.country[0].text version["totaltracks"] = [int(m.track_list[0].count) for m in release.medium_list[0].medium] - formats = {} - for medium in release.medium_list[0].medium: - if "format" in medium.children: - f = medium.format[0].text - if f in formats: formats[f] += 1 - else: formats[f] = 1 - if formats: - version["format"] = " + ".join(["%s%s" % (str(j)+u"×" if j>1 else "", RELEASE_FORMATS[i]) - for i, j in formats.items()]) + version["format"] = media_formats_from_node(release.medium_list[0]) self.other_versions.append(version) self.other_versions.sort(key=lambda x: x["date"]) diff --git a/picard/collection.py b/picard/collection.py index 617d3113e..15db48745 100644 --- a/picard/collection.py +++ b/picard/collection.py @@ -18,6 +18,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from PyQt4 import QtCore +from picard.mbxml import artist_credit_from_node, media_formats_from_node +from picard.album import Album +from picard.webservice import XmlNode from picard.util import partial @@ -27,8 +30,14 @@ class CollectionList(QtCore.QObject): QtCore.QObject.__init__(self) self.view = view self.collections = {} + self.releases = {} + self.loading = False self.loaded = False + def release_from_obj(self, obj): + self.releases.setdefault(obj.id, CollectionRelease(obj)) + return self.releases[obj.id] + def _parse_collection_list(self, document): collection_list = document.metadata[0].collection_list[0] collections = collection_list.collection @@ -47,10 +56,13 @@ class CollectionList(QtCore.QObject): else: self._parse_collection_list(document) self.view.add_collections(self.collections) + self.loading = False self.loaded = True def load(self): self.collections = {} + self.releases = {} + self.loading = True self.loaded = False self.tagger.xmlws.get_collection_list(self._collection_list_request_finished) @@ -61,7 +73,7 @@ class Collection(QtCore.QObject): self.id = id self.name = name self.count = count - self.releases = set() + self.release_ids = set() self.pending_adds = set() self.pending_removes = set() self.collection_list = list @@ -69,23 +81,28 @@ class Collection(QtCore.QObject): self.release_widgets = {} self.load() - def _add_releases(self, ids): - self.releases.update(ids) + def _add_release_ids(self, ids): + self.release_ids.update(ids) self.count += len(ids) - def _remove_releases(self, ids): - self.releases.difference_update(ids) + def _remove_release_ids(self, ids): + self.release_ids.difference_update(ids) self.count -= len(ids) def add_releases(self, releases): ids = releases.keys() - self._add_releases(ids) + self._add_release_ids(ids) self.pending_adds.update(ids) - self.collection_list.view.add_releases(releases, self, pending=True) + not_pending = {} + for id, release in releases.iteritems(): + if id not in self.pending_removes: + not_pending[id] = release + self.collection_list.releases.setdefault(id, release) + self.collection_list.view.add_releases(not_pending, self, pending=True) self.tagger.xmlws.put_to_collection(self.id, ids, partial(self._add_request_finished, ids)) def remove_releases(self, ids): - self._remove_releases(ids) + self._remove_release_ids(ids) self.pending_removes.update(ids) self.color_pending_releases(ids, True) self.tagger.xmlws.delete_from_collection(self.id, ids, partial(self._remove_request_finished, ids)) @@ -97,7 +114,7 @@ class Collection(QtCore.QObject): self.pending_adds.difference_update(ids) if error: self.log.error("%r", unicode(reply.errorString())) - self._remove_releases(ids) + self._remove_release_ids(ids) self.collection_list.view.remove_releases(ids, self) else: self.widget.update_text() @@ -107,7 +124,7 @@ class Collection(QtCore.QObject): self.pending_removes.difference_update(ids) if error: self.log.error("%r", unicode(reply.errorString())) - self._add_releases(ids) + self._add_release_ids(ids) self.color_pending_releases(ids, False) else: ids = set(ids) - self.pending_adds @@ -134,11 +151,39 @@ class Collection(QtCore.QObject): if release_list.count != "0": release_nodes = release_list.release for node in release_nodes: - title = node.title[0].text - date = node.date[0].text if "date" in node.children else "" - country = node.country[0].text if "country" in node.children else "" - barcode = node.barcode[0].text if "barcode" in node.children else "" - release = (title, date, country, barcode) - releases[node.id] = release - self.releases.add(node.id) + releases[node.id] = self.collection_list.release_from_obj(node) + self.release_ids.add(node.id) + self.collection_list.releases.update(releases) self.collection_list.view.add_releases(releases, self) + + +class CollectionRelease(QtCore.QObject): + + def __init__(self, obj): + self.id = obj.id + self.reference_count = 0 + if isinstance(obj, XmlNode): + self._metadata_from_node(obj) + elif isinstance(obj, Album): + self._metadata_from_album(obj) + + def _metadata_from_node(self, node): + title = node.title[0].text + artist = artist_credit_from_node(node.artist_credit[0])[0] + format = media_formats_from_node(node.medium_list[0]) + tracks = " + ".join([m.track_list[0].count for m in node.medium_list[0].medium]) + date = node.date[0].text if "date" in node.children else "" + country = node.country[0].text if "country" in node.children else "" + barcode = node.barcode[0].text if "barcode" in node.children else "" + self.columns = (title, artist, format, tracks, date, country, barcode) + + def _metadata_from_album(self, album): + m = album.metadata + title = m["album"] + artist = m["albumartist"] + format = album.format_str + tracks = album.tracks_str + date = m["date"] + country = m["releasecountry"] + barcode = m["barcode"] + self.columns = (title, artist, format, tracks, date, country, barcode) diff --git a/picard/mbxml.py b/picard/mbxml.py index 6b51c9400..195b8d255 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -20,6 +20,7 @@ import re import unicodedata from picard.util import format_time, translate_artist +from picard.const import RELEASE_FORMATS _artist_rel_types = { @@ -142,6 +143,20 @@ def label_info_from_node(node): return (labels, catalog_numbers) +def media_formats_from_node(node): + formats = {} + for medium in node.medium: + if "format" in medium.children: + text = medium.format[0].text + formats.setdefault(text, 0) + formats[text] += 1 + if formats: + return " + ".join([(str(j) + u"×" if j > 1 else "") + RELEASE_FORMATS[i] + for i, j in formats.items()]) + else: + return "" + + def track_to_metadata(node, track, config=None): m = track.metadata recording_to_metadata(node.recording[0], track, config) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 54e9aafc4..bf4a3d07d 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -24,7 +24,7 @@ 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.collection import CollectionList, Collection +from picard.collection import CollectionList, Collection, CollectionRelease from picard.util import encode_filename, icontheme, partial, webbrowser2 from picard.config import Option, TextOption from picard.plugin import ExtensionPoint @@ -404,8 +404,7 @@ class BaseTreeView(QtGui.QTreeWidget): for item in self.selectedItems(): obj = self.panel.object_from_item(item) if isinstance(obj, Album) and obj.loaded: - m = obj.metadata - selected_releases[obj.id] = (m["album"], m["date"], m["releasecountry"], m["barcode"]) + selected_releases[obj.id] = collection_list.releases.get(obj.id, CollectionRelease(obj)) if selected_releases: collections_menu = QtGui.QMenu(_("Collections"), menu) @@ -415,7 +414,7 @@ class BaseTreeView(QtGui.QTreeWidget): pending = collection.pending_adds | collection.pending_removes if selected_ids & pending: return - difference = selected_ids - collection.releases + difference = selected_ids - collection.release_ids if not difference: collection.remove_releases(selected_releases) checkbox.setCheckState(QtCore.Qt.Unchecked) @@ -431,7 +430,7 @@ class BaseTreeView(QtGui.QTreeWidget): action.setDefaultWidget(checkbox) collections_menu.addAction(action) - difference = selected_ids - collection.releases + difference = selected_ids - collection.release_ids if not difference: checkbox.setCheckState(QtCore.Qt.Checked) @@ -488,7 +487,10 @@ class BaseTreeView(QtGui.QTreeWidget): def mimeTypes(self): """List of MIME types accepted by this view.""" - return ["text/uri-list", "application/picard.file-list", "application/picard.album-list"] + return ["text/uri-list", + "application/picard.file-list", + "application/picard.album-list", + "application/picard.collection-list"] def dragEnterEvent(self, event): if event.mimeData().hasUrls(): @@ -611,6 +613,11 @@ class BaseTreeView(QtGui.QTreeWidget): albums = [self.tagger.load_album(id) for id in str(albums).split("\n")] self.drop_albums(albums, target) handled = True + albums = data.data("application/picard.collection-list") + if albums: + for id in str(albums).split("\n"): + self.tagger.load_album(id) + handled = True return handled def activate_item(self, index): @@ -819,7 +826,7 @@ class CollectionReleaseTreeItem(QtGui.QTreeWidgetItem): QtGui.QTreeWidgetItem.__init__(self, parent) self.collection = collection self.release = release - for i, text in enumerate(release): + for i, text in enumerate(release.columns): self.setText(i, text) self.id = id @@ -834,7 +841,7 @@ class CollectionTreeView(QtGui.QTreeWidget): def __init__(self, window, parent): QtGui.QTreeWidget.__init__(self, parent) self.window = window - self.setHeaderLabels(["Title", "Date", "Country", "Barcode"]) + self.setHeaderLabels(["Title", "Artist", "Format", "Tracks", "Date", "Country", "Barcode"]) self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) self.setDragEnabled(True) self.setAcceptDrops(True) @@ -845,8 +852,9 @@ class CollectionTreeView(QtGui.QTreeWidget): self.collection_list = CollectionList(self) def showEvent(self, event): - if not self.collection_list.loaded: - self.collection_list.load() + cl = self.collection_list + if not (cl.loaded or cl.loading): + cl.load() QtGui.QTreeView.showEvent(self, event) def refresh(self): @@ -865,17 +873,23 @@ class CollectionTreeView(QtGui.QTreeWidget): def add_releases(self, releases, collection, pending=False): item = collection.widget for id, release in releases.items(): - if id not in collection.pending_removes: - release_item = CollectionReleaseTreeItem(item, collection, release, id) - collection.release_widgets[id] = release_item - release_item.color_pending(pending) - self.resizeColumnToContents(2) + release_item = CollectionReleaseTreeItem(item, collection, release, id) + collection.release_widgets[id] = release_item + release_item.color_pending(pending) + release = self.collection_list.releases[id] + release.reference_count += 1 + for i in xrange(2, 7): + self.resizeColumnToContents(i) def remove_releases(self, ids, collection): item = collection.widget for id in ids: release_item = collection.release_widgets.pop(id) item.removeChild(release_item) + release = self.collection_list.releases[id] + release.reference_count -= 1 + if release.reference_count < 1: + del self.collection_list.releases[id] item.update_text() def contextMenuEvent(self, event): @@ -923,18 +937,10 @@ class CollectionTreeView(QtGui.QTreeWidget): def mimeData(self, items): """Return MIME data for specified items.""" - ids = [] - data = [] - for item in items: - if isinstance(item, CollectionReleaseTreeItem): - ids.append(item.id) - release = [item.id] - release.extend(item.release) - data.append("\n".join(release)) + ids = [i.id for i in items if isinstance(i, CollectionReleaseTreeItem)] mimeData = QtCore.QMimeData() if ids: - mimeData.setData("application/picard.album-list", "\n".join(ids)) - mimeData.setData("application/picard.collection-list", "\n".join(data)) + mimeData.setData("application/picard.collection-list", "\n".join(ids)) return mimeData def dropEvent(self, event): @@ -946,20 +952,16 @@ class CollectionTreeView(QtGui.QTreeWidget): collection = parent.collection releases = {} if data.hasFormat("application/picard.album-list"): - mbids = set(map(str, data.data("application/picard.album-list").split("\n"))) - mbids.difference_update(collection.releases) - for mbid in mbids: - album = self.tagger.get_album_by_id(mbid) + ids = set(map(str, data.data("application/picard.album-list").split("\n"))) + ids.difference_update(collection.release_ids) + for id in ids: + album = self.tagger.get_album_by_id(id) if album is not None and album.loaded: - m = album.metadata - releases[album.id] = (m["album"], m["date"], m["releasecountry"], m["barcode"]) + releases[album.id] = self.collection_list.release_from_obj(album) if data.hasFormat("application/picard.collection-list"): - items = map(unicode, data.data("application/picard.collection-list").split("\n")) - while items: - id = str(items[0]) - if id not in collection.releases: - releases[id] = tuple(items[1:5]) - items = items[5:] + ids = map(unicode, data.data("application/picard.collection-list").split("\n")) + crs = self.collection_list.releases + releases = {id: crs[id] for id in ids if id not in collection.release_ids} if releases: collection.add_releases(releases) return True diff --git a/picard/webservice.py b/picard/webservice.py index f2e559773..3caaaf7ef 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -334,7 +334,8 @@ class XmlWebService(QtCore.QObject): port = self.config.setting["server_port"] path = "/ws/2/collection" if id is not None: - path += "/%s/releases" % id + inc = ["releases", "artist-credits", "media"] + path += "/%s/releases?inc=%s" % (id, "+".join(inc)) return self.get(host, port, path, handler, priority=True, important=True, mblogin=True) def get_collection_list(self, handler): From 080ed16c843c9c220441fee8c90f5eb2cb206022 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 12 Jul 2011 19:03:25 -0500 Subject: [PATCH 33/79] Nitpicky changes --- picard/ui/itemviews.py | 2 +- picard/ui/mainwindow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index bf4a3d07d..bbc54b78b 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -959,7 +959,7 @@ class CollectionTreeView(QtGui.QTreeWidget): if album is not None and album.loaded: releases[album.id] = self.collection_list.release_from_obj(album) if data.hasFormat("application/picard.collection-list"): - ids = map(unicode, data.data("application/picard.collection-list").split("\n")) + ids = map(str, data.data("application/picard.collection-list").split("\n")) crs = self.collection_list.releases releases = {id: crs[id] for id in ids if id not in collection.release_ids} if releases: diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 939fa292c..cb0722d78 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -717,7 +717,7 @@ class MainWindow(QtGui.QMainWindow): self.file_browser.hide() def show_collections(self): - """Show/hide the Collections.""" + """Show/hide the collections panel.""" if self.show_collections_action.isChecked(): sizes = self.panel.sizes() if sizes[3] == 0: From 09a04db3ed7ab7f30d0f46fee20998058e740ab5 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 13 Jul 2011 00:50:47 -0500 Subject: [PATCH 34/79] - Only load release groups when needed (i.e. when a user right-clicks a release). This makes releases load a bit faster. - Include the number of tracks in the "other versions" menus. --- picard/album.py | 12 ++++------ picard/ui/itemviews.py | 53 +++++++++++++++++++++++++----------------- picard/webservice.py | 2 +- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/picard/album.py b/picard/album.py index 397ba16bd..bce329b96 100644 --- a/picard/album.py +++ b/picard/album.py @@ -42,6 +42,7 @@ class Album(DataObject, Item): self.tracks_str = "" self.loaded = False self.rgloaded = False + self.rgid = None self._files = 0 self._requests = 0 self._discid = discid @@ -79,16 +80,12 @@ class Album(DataObject, Item): m = self._new_metadata m.length = 0 release_to_metadata(release_node, m, config=self.config, album=self) - - self.format_str = media_formats_from_node(release_node.medium_list[0]) + self.format_str = media_formats_from_node(release_node.medium_list[0]) + self.rgid = release_node.release_group[0].id if self._discid: m['musicbrainz_discid'] = self._discid - if not self.rgloaded: - releasegroupid = release_node.release_group[0].id - self.tagger.xmlws.get_release_group_by_id(releasegroupid, self._release_group_request_finished) - # 'Translate' artist name if self.config.setting['translate_artist_names']: m['albumartist'] = m['artist'] = translate_artist(m['artist'], m['artistsort']) @@ -169,7 +166,7 @@ class Album(DataObject, Item): version["date"] = release.date[0].text if "country" in release.children: version["country"] = release.country[0].text - version["totaltracks"] = [int(m.track_list[0].count) for m in release.medium_list[0].medium] + version["tracks"] = " + ".join([m.track_list[0].count for m in release.medium_list[0].medium]) version["format"] = media_formats_from_node(release.medium_list[0]) self.other_versions.append(version) self.other_versions.sort(key=lambda x: x["date"]) @@ -212,6 +209,7 @@ class Album(DataObject, Item): self.log.error(traceback.format_exc()) finally: self.rgloaded = True + self.emit(QtCore.SIGNAL("release_group_loaded")) def _finalize_loading(self, error): if error: diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index bbc54b78b..8da54f9b9 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -372,29 +372,40 @@ class BaseTreeView(QtGui.QTreeWidget): if isinstance(obj, Album) and not isinstance(obj, NatAlbum): releases_menu = QtGui.QMenu(_("&Other versions"), menu) - switch_release_version = partial(self._switch_release_version, obj) - for i, version in enumerate(obj.other_versions): - name = [] - if "date" in version: - name.append(version["date"]) - if "country" in version: - try: name.append(RELEASE_COUNTRIES[version["country"]]) - except KeyError: name.append(version["country"]) - if "format" in version: - name.append(version["format"]) - version_name = " / ".join(name).replace('&', '&&') - action = releases_menu.addAction(version_name or _('[no release info]')) - action.setData(QtCore.QVariant(i)) - action.setCheckable(True) - if obj.id == version["mbid"]: - action.setChecked(True) - self.connect(action, QtCore.SIGNAL("triggered(bool)"), switch_release_version) - if releases_menu.isEmpty(): - text = _('No other versions') if obj.rgloaded else _('Loading...') - action = releases_menu.addAction(text) - action.setEnabled(False) menu.addSeparator() menu.addMenu(releases_menu) + loading = releases_menu.addAction(_('Loading...')) + loading.setEnabled(False) + + def _add_other_versions(): + releases_menu.removeAction(loading) + switch_release_version = partial(self._switch_release_version, obj) + actions = [] + for i, version in enumerate(obj.other_versions): + name = [] + if "date" in version and version["date"]: + name.append(version["date"]) + if "country" in version: + name.append(RELEASE_COUNTRIES.get(version["country"], version["country"])) + name.append(version["tracks"]) + if "format" in version and version["format"]: + name.append(version["format"]) + version_name = " / ".join(name).replace('&', '&&') + action = releases_menu.addAction(version_name or _('[no release info]')) + action.setData(QtCore.QVariant(i)) + action.setCheckable(True) + if obj.id == version["mbid"]: + action.setChecked(True) + self.connect(action, QtCore.SIGNAL("triggered(bool)"), switch_release_version) + if releases_menu.isEmpty(): + action = releases_menu.addAction(_('No other versions')) + action.setEnabled(False) + + if not obj.rgloaded: + self.connect(obj, QtCore.SIGNAL("release_group_loaded"), _add_other_versions) + self.tagger.xmlws.get_release_group_by_id(obj.rgid, obj._release_group_request_finished) + else: + _add_other_versions() collection_list = self.window.collections_panel.collection_list diff --git a/picard/webservice.py b/picard/webservice.py index 3caaaf7ef..d8db6e981 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -259,7 +259,7 @@ class XmlWebService(QtCore.QObject): path = "/ws/2/%s/%s?inc=%s&%s" % (entitytype, entityid, "+".join(inc), "&".join(params)) return self.get(host, port, path, handler, priority=priority, important=important, mblogin=mblogin) - def get_release_group_by_id(self, releasegroupid, handler, priority=True, important=False): + def get_release_group_by_id(self, releasegroupid, handler, priority=True, important=True): inc = ['releases', 'media'] return self._get_by_id('release-group', releasegroupid, handler, inc, priority=priority, important=important) From 930da03aa749218636041b8fe09416db4ddd6e1e Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 13 Jul 2011 23:31:14 -0500 Subject: [PATCH 35/79] Referencing a script function that no longer exists (for example, after disabling a plugin) would crash the preferences window. --- picard/script.py | 2 +- picard/ui/options/renaming.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/picard/script.py b/picard/script.py index 6e4ced453..f4ee3ea49 100644 --- a/picard/script.py +++ b/picard/script.py @@ -4,7 +4,7 @@ # Copyright (C) 2006-2007 Lukáš Lalinský # Copyright (C) 2007 Javier Kohen # 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 diff --git a/picard/ui/options/renaming.py b/picard/ui/options/renaming.py index 44da3288c..2134b28f8 100644 --- a/picard/ui/options/renaming.py +++ b/picard/ui/options/renaming.py @@ -23,7 +23,7 @@ import sys from PyQt4 import QtCore, QtGui from picard.config import BoolOption, TextOption from picard.file import File -from picard.script import ScriptParser, SyntaxError +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.util import decode_filename @@ -51,7 +51,7 @@ class RenamingOptionsPage(OptionsPage): self.ui = Ui_RenamingOptionsPage() self.ui.setupUi(self) self.update_examples() - + self.connect(self.ui.ascii_filenames, QtCore.SIGNAL("clicked()"), self.update_examples) self.connect(self.ui.windows_compatible_filenames, QtCore.SIGNAL("clicked()"), self.update_examples) self.connect(self.ui.use_va_format, QtCore.SIGNAL("clicked()"), self.update_examples) @@ -80,7 +80,7 @@ class RenamingOptionsPage(OptionsPage): 'file_naming_format': unicode(self.ui.file_naming_format.toPlainText()), 'va_file_naming_format': unicode(self.ui.va_file_naming_format.toPlainText()), 'move_files_to': os.path.normpath(unicode(self.config.setting["move_files_to"])), - } + } try: if self.config.setting["enable_tagger_script"]: script = self.config.setting["tagger_script"] @@ -90,6 +90,7 @@ class RenamingOptionsPage(OptionsPage): return filename except SyntaxError, e: return "" except TypeError, e: return "" + except UnknownFunction, e: return "" def update_examples(self): # TODO: Here should be more examples etc. @@ -97,7 +98,7 @@ class RenamingOptionsPage(OptionsPage): example1 = self._example_to_filename(self.example_1()) example2 = self._example_to_filename(self.example_2()) self.ui.example_filename.setText(example1 + "
" + example2) - + def load(self): if sys.platform == "win32": self.ui.windows_compatible_filenames.setChecked(True) @@ -216,7 +217,7 @@ class RenamingOptionsPage(OptionsPage): except OptionsCheckError, e: self.ui.renaming_error.setStyleSheet(self.STYLESHEET_ERROR); self.ui.renaming_error.setText(e.info) - return + return def va_test(self): self.ui.renaming_va_error.setStyleSheet(""); @@ -228,5 +229,5 @@ class RenamingOptionsPage(OptionsPage): self.ui.renaming_va_error.setStyleSheet(self.STYLESHEET_ERROR); self.ui.renaming_va_error.setText(e.info) return - + register_options_page(RenamingOptionsPage) From 43642983a4a1878b48c519fc344d18e32d6c9535 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 14 Jul 2011 14:46:48 -0500 Subject: [PATCH 36/79] Make the "config" argument required for functions in mbxml.py --- picard/album.py | 2 +- picard/collection.py | 2 +- picard/mbxml.py | 32 ++++++++++++++------------------ test/test_mbxml.py | 11 +++++++++-- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/picard/album.py b/picard/album.py index bce329b96..330a60d6e 100644 --- a/picard/album.py +++ b/picard/album.py @@ -134,7 +134,7 @@ class Album(DataObject, Item): tm['discnumber'] = discnumber tm['discsubtitle'] = discsubtitle tm['totaltracks'] = totaltracks - if format: tm['format'] = format + if format: tm['media'] = format track_to_metadata(node, config=self.config, track=t) t._customize_metadata(node, release_node, script, parser, ignore_tags) diff --git a/picard/collection.py b/picard/collection.py index 15db48745..eddfaa6c9 100644 --- a/picard/collection.py +++ b/picard/collection.py @@ -169,7 +169,7 @@ class CollectionRelease(QtCore.QObject): def _metadata_from_node(self, node): title = node.title[0].text - artist = artist_credit_from_node(node.artist_credit[0])[0] + artist = artist_credit_from_node(node.artist_credit[0], self.config)[0] format = media_formats_from_node(node.medium_list[0]) tracks = " + ".join([m.track_list[0].count for m in node.medium_list[0].medium]) date = node.date[0].text if "date" in node.children else "" diff --git a/picard/mbxml.py b/picard/mbxml.py index 195b8d255..3f8753465 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -106,14 +106,13 @@ def _set_artist_item(m, release, albumname, name, value): m[name] = value -def artist_credit_from_node(node, config=None): +def artist_credit_from_node(node, config): artist = "" artistsort = "" - standardize_name = config and config.setting["standardize_artists"] for credit in node.name_credit: a = credit.artist[0] artistsort += a.sort_name[0].text - if 'name' in credit.children and not standardize_name: + if 'name' in credit.children and not config.setting["standardize_artists"]: artist += credit.name[0].text else: artist += a.name[0].text @@ -123,7 +122,7 @@ def artist_credit_from_node(node, config=None): return (artist, artistsort) -def artist_credit_to_metadata(node, m=None, release=None, config=None): +def artist_credit_to_metadata(node, m, config, release=False): ids = [n.artist[0].id for n in node.name_credit] _set_artist_item(m, release, 'musicbrainz_albumartistid', 'musicbrainz_artistid', ids) artist, artistsort = artist_credit_from_node(node, config) @@ -157,26 +156,24 @@ def media_formats_from_node(node): return "" -def track_to_metadata(node, track, config=None): +def track_to_metadata(node, track, config): m = track.metadata recording_to_metadata(node.recording[0], track, config) # overwrite with data we have on the track - standardize_title = config and config.setting["standardize_tracks"] - standardize_artist = config and config.setting["standardize_artists"] for name, nodes in node.children.iteritems(): if not nodes: continue - if name == 'title' and not standardize_title: + if name == 'title' and not config.setting["standardize_tracks"]: m['title'] = nodes[0].text if name == 'position': m['tracknumber'] = nodes[0].text elif name == 'length' and nodes[0].text: m.length = int(nodes[0].text) - elif name == 'artist_credit' and not standardize_artist: - artist_credit_to_metadata(nodes[0], m, config=config) + elif name == 'artist_credit' and not config.setting["standardize_artists"]: + artist_credit_to_metadata(nodes[0], m, config) -def recording_to_metadata(node, track, config=None): +def recording_to_metadata(node, track, config): m = track.metadata m.length = 0 m['musicbrainz_trackid'] = node.attribs['id'] @@ -190,11 +187,11 @@ def recording_to_metadata(node, track, config=None): elif name == 'disambiguation': m['~recordingcomment'] = nodes[0].text elif name == 'artist_credit': - artist_credit_to_metadata(nodes[0], m, config=config) + artist_credit_to_metadata(nodes[0], m, config) if name == 'relation_list': _relations_to_metadata(nodes, m, config) elif name == 'release_list' and nodes[0].count != '0': - release_to_metadata(nodes[0].release[0], m) + release_to_metadata(nodes[0].release[0], m, config) elif name == 'tag_list': add_folksonomy_tags(nodes[0], track) elif name == 'user_tag_list': @@ -205,10 +202,9 @@ def recording_to_metadata(node, track, config=None): m['~rating'] = nodes[0].text -def release_to_metadata(node, m, config=None, album=None): +def release_to_metadata(node, m, config, album=None): """Make metadata dict from a XML 'release' node.""" m['musicbrainz_albumid'] = node.attribs['id'] - standardize_title = config and config.setting["standardize_releases"] for name, nodes in node.children.iteritems(): if not nodes: @@ -216,18 +212,18 @@ def release_to_metadata(node, m, config=None, album=None): if name == 'release_group': if 'type' in nodes[0].attribs: m['releasetype'] = nodes[0].type.lower() - if standardize_title: + if config.setting["standardize_releases"]: m['album'] = nodes[0].title[0].text elif name == 'status': m['releasestatus'] = nodes[0].text.lower() - elif name == 'title' and not standardize_title: + elif name == 'title' and not config.setting["standardize_releases"]: m['album'] = nodes[0].text elif name == 'disambiguation': m['~releasecomment'] = nodes[0].text elif name == 'asin': m['asin'] = nodes[0].text elif name == 'artist_credit': - artist_credit_to_metadata(nodes[0], m, True, config=config) + artist_credit_to_metadata(nodes[0], m, config, release=True) elif name == 'date': m['date'] = nodes[0].text elif name == 'country': diff --git a/test/test_mbxml.py b/test/test_mbxml.py index 66bce5f00..13fd84060 100644 --- a/test/test_mbxml.py +++ b/test/test_mbxml.py @@ -3,6 +3,13 @@ from picard.metadata import Metadata from picard.mbxml import track_to_metadata, release_to_metadata from picard.webservice import XmlNode +class config: + setting = { + "standardize_tracks": False, + "standardize_artists": False, + "standardize_releases": False + } + class XmlNode(object): def __init__(self, text=u'', children={}, attribs={}): @@ -50,7 +57,7 @@ class TrackTest(unittest.TestCase): }) track = Track() m = track.metadata = Metadata() - track_to_metadata(node, track) + track_to_metadata(node, track, config) self.failUnlessEqual('123', m['musicbrainz_trackid']) self.failUnlessEqual('456; 789', m['musicbrainz_artistid']) self.failUnlessEqual('Foo', m['title']) @@ -94,7 +101,7 @@ class ReleaseTest(unittest.TestCase): })] }) m = Metadata() - release_to_metadata(release, m) + release_to_metadata(release, m, config) self.failUnlessEqual('123', m['musicbrainz_albumid']) self.failUnlessEqual('456; 789', m['musicbrainz_artistid']) self.failUnlessEqual('456; 789', m['musicbrainz_albumartistid']) From c7c1adfefd88700a494bc7d19d42e4d3891162f7 Mon Sep 17 00:00:00 2001 From: Chad Wilson Date: Thu, 14 Jul 2011 16:14:39 -0500 Subject: [PATCH 37/79] Fixed #5948 - folksonomy tags as genre not supported at NGS RG level --- picard/mbxml.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index 3f8753465..fec5cc8ee 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -70,7 +70,7 @@ def _relations_to_metadata(relation_lists, m, config): if relation_list.target_type == 'artist': for relation in relation_list.relation: value = relation.artist[0].name[0].text - if config and config.setting['translate_artist_names']: + if config.setting['translate_artist_names']: value = translate_artist(value, relation.artist[0].sort_name[0].text) reltype = relation.type attribs = [] @@ -210,10 +210,7 @@ def release_to_metadata(node, m, config, album=None): if not nodes: continue if name == 'release_group': - if 'type' in nodes[0].attribs: - m['releasetype'] = nodes[0].type.lower() - if config.setting["standardize_releases"]: - m['album'] = nodes[0].title[0].text + release_group_to_metadata(nodes[0], m, config, album) elif name == 'status': m['releasestatus'] = nodes[0].text.lower() elif name == 'title' and not config.setting["standardize_releases"]: @@ -245,6 +242,22 @@ def release_to_metadata(node, m, config, album=None): add_user_folksonomy_tags(nodes[0], album) +def release_group_to_metadata(node, m, config, album=None): + """Make metadata dict from a XML 'release-group' node taken from inside a 'release' node.""" + if 'type' in node.attribs: + m['releasetype'] = node.type.lower() + if config.setting["standardize_releases"]: + m['album'] = node.title[0].text + + for name, nodes in node.children.iteritems(): + if not nodes: + continue + if name == 'tag_list': + add_folksonomy_tags(nodes[0], album) + elif name == 'user_tag_list': + add_user_folksonomy_tags(nodes[0], album) + + def add_folksonomy_tags(node, obj): if obj and 'tag' in node.children: for tag in node.tag: From fb3a30dc5eaf618439293f1d88e111938db26c8d Mon Sep 17 00:00:00 2001 From: Chad Wilson Date: Thu, 14 Jul 2011 16:18:08 -0500 Subject: [PATCH 38/79] Tune .bzrignore to not track PYDs, DLLs, PyCharm (.idea) config, compiled locale data or plugins directly in the plugins folder --- .bzrignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.bzrignore b/.bzrignore index 19fabebad..260f869eb 100644 --- a/.bzrignore +++ b/.bzrignore @@ -4,3 +4,8 @@ po/picard.pot build.cfg .pydevproject .project +.idea +*.pyd +*.dll +RE:picard/plugins/(?!__init__.py).* +locale From 6ef1e08e3de4535e9e1daaf82c0120c4069ad11a Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 14 Jul 2011 16:29:52 -0500 Subject: [PATCH 39/79] Add ~originaldate tag. --- picard/mbxml.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index fec5cc8ee..3bcf0419e 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -246,13 +246,15 @@ def release_group_to_metadata(node, m, config, album=None): """Make metadata dict from a XML 'release-group' node taken from inside a 'release' node.""" if 'type' in node.attribs: m['releasetype'] = node.type.lower() - if config.setting["standardize_releases"]: - m['album'] = node.title[0].text for name, nodes in node.children.iteritems(): if not nodes: continue - if name == 'tag_list': + if name == 'title' and config.setting["standardize_releases"]: + m['album'] = node.title[0].text + elif name == 'first_release_date': + m['~originaldate'] = nodes[0].text + elif name == 'tag_list': add_folksonomy_tags(nodes[0], album) elif name == 'user_tag_list': add_user_folksonomy_tags(nodes[0], album) From 2d93ddcc3184be9de07fba6262d92a2ee3da3e01 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 14 Jul 2011 20:15:24 -0500 Subject: [PATCH 40/79] Fix extraneous metadata when tagging a recording as a NAT. --- picard/mbxml.py | 4 +--- picard/tagger.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index 3bcf0419e..7affd0149 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -188,10 +188,8 @@ def recording_to_metadata(node, track, config): m['~recordingcomment'] = nodes[0].text elif name == 'artist_credit': artist_credit_to_metadata(nodes[0], m, config) - if name == 'relation_list': + elif name == 'relation_list': _relations_to_metadata(nodes, m, config) - elif name == 'release_list' and nodes[0].count != '0': - release_to_metadata(nodes[0].release[0], m, config) elif name == 'tag_list': add_folksonomy_tags(nodes[0], track) elif name == 'user_tag_list': diff --git a/picard/tagger.py b/picard/tagger.py index 5d5d49665..c310cb961 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -84,7 +84,6 @@ from picard.util import ( mbid_validate ) from picard.webservice import XmlWebService -from picard.mbxml import recording_to_metadata class Tagger(QtGui.QApplication): From 99f795b0da79d80f07e3b45e97aa8f4386991084 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 14 Jul 2011 20:27:22 -0500 Subject: [PATCH 41/79] Assume that a file with only a trackid was tagged as a NAT. --- picard/file.py | 5 ----- picard/tagger.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/picard/file.py b/picard/file.py index b08cf6949..60fc79d2d 100644 --- a/picard/file.py +++ b/picard/file.py @@ -537,11 +537,6 @@ class File(LockableObject, Item): else: self.tagger.move_file_to_nat(self, track.id, node=track) - def lookup_trackid(self, trackid): - """ Try to identify the file using the trackid. """ - self.clear_lookup_task() - self.lookup_task = self.tagger.xmlws.get_track_by_id(trackid, partial(self._lookup_finished, 'trackid')) - def lookup_puid(self, puid): """ Try to identify the file using the PUID. """ self.tagger.window.set_statusbar_message(N_("Looking up the PUID for file %s..."), self.filename) diff --git a/picard/tagger.py b/picard/tagger.py index c310cb961..54e17c948 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -326,7 +326,7 @@ class Tagger(QtGui.QApplication): else: self.move_file_to_album(file, albumid) elif mbid_validate(trackid): - file.lookup_trackid(trackid) + self.move_file_to_nat(file, trackid) elif self.config.setting['analyze_new_files']: self.analyze([file]) From 91c7266bc8171047f061e17e86835c7b3d26679d Mon Sep 17 00:00:00 2001 From: Chad Wilson Date: Thu, 14 Jul 2011 21:23:05 -0500 Subject: [PATCH 42/79] Fix to use of folksonomy tags from tracks/recordings as genre http://bugs.musicbrainz.org/ticket/5947 --- picard/track.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/track.py b/picard/track.py index 70932ebf1..11f6bee84 100644 --- a/picard/track.py +++ b/picard/track.py @@ -174,7 +174,7 @@ class Track(DataObject): def _convert_folksonomy_tags_to_genre(self, ignore_tags): # Combine release and track tags - tags = dict(self.album.folksonomy_tags) + tags = dict(self.folksonomy_tags) for name, count in self.album.folksonomy_tags.iteritems(): tags.setdefault(name, 0) tags[name] += count From 7681f8434ef97d7493b0851d48523c04866f3c78 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 14 Jul 2011 21:25:51 -0500 Subject: [PATCH 43/79] Load folksonomy tags for NATs. --- picard/track.py | 10 +++++++++- picard/webservice.py | 8 ++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/picard/track.py b/picard/track.py index 11f6bee84..14d6cfd1c 100644 --- a/picard/track.py +++ b/picard/track.py @@ -221,7 +221,15 @@ class NonAlbumTrack(Track): return super(NonAlbumTrack, self).column(column) def load(self): - self.tagger.xmlws.get_track_by_id(self.id, partial(self._recording_request_finished)) + inc = ["artist-credits"] + mblogin = False + if self.config.setting["folksonomy_tags"]: + if self.config.setting["only_my_tags"]: + mblogin = True + inc += ["user-tags"] + else: + inc += ["tags"] + self.tagger.xmlws.get_track_by_id(self.id, partial(self._recording_request_finished), inc, mblogin=mblogin) def _recording_request_finished(self, document, http, error): if error: diff --git a/picard/webservice.py b/picard/webservice.py index d8db6e981..024b531a1 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -256,7 +256,8 @@ class XmlWebService(QtCore.QObject): def _get_by_id(self, entitytype, entityid, handler, inc=[], params=[], priority=False, important=False, mblogin=False): host = self.config.setting["server_host"] port = self.config.setting["server_port"] - path = "/ws/2/%s/%s?inc=%s&%s" % (entitytype, entityid, "+".join(inc), "&".join(params)) + path = "/ws/2/%s/%s?inc=%s" % (entitytype, entityid, "+".join(inc)) + if params: path += "&" + "&".join(params) return self.get(host, port, path, handler, priority=priority, important=important, mblogin=mblogin) def get_release_group_by_id(self, releasegroupid, handler, priority=True, important=True): @@ -266,9 +267,8 @@ class XmlWebService(QtCore.QObject): def get_release_by_id(self, releaseid, handler, inc=[], priority=True, important=False, mblogin=False): return self._get_by_id('release', releaseid, handler, inc, priority=priority, important=important, mblogin=mblogin) - def get_track_by_id(self, trackid, handler, priority=False, important=False): - inc = ['releases', 'release-groups', 'media', 'artist-credits'] - return self._get_by_id('recording', trackid, handler, inc, priority=priority, important=important) + def get_track_by_id(self, trackid, handler, inc=[], priority=True, important=False, mblogin=False): + return self._get_by_id('recording', trackid, handler, inc, priority=priority, important=important, mblogin=mblogin) def lookup_puid(self, puid, handler, priority=False, important=False): inc = ['releases', 'release-groups', 'media', 'artist-credits'] From 1dcbd56fc83a6773d53dad6a4810609d16f16c53 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 14 Jul 2011 21:30:43 -0500 Subject: [PATCH 44/79] Move the standardization checks so that less comparisons are performed. (Nitpicky change.) --- picard/mbxml.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index 7affd0149..f1a4ba7e4 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -163,9 +163,10 @@ def track_to_metadata(node, track, config): for name, nodes in node.children.iteritems(): if not nodes: continue - if name == 'title' and not config.setting["standardize_tracks"]: - m['title'] = nodes[0].text - if name == 'position': + if name == 'title': + if not config.setting["standardize_tracks"]: + m['title'] = nodes[0].text + elif name == 'position': m['tracknumber'] = nodes[0].text elif name == 'length' and nodes[0].text: m.length = int(nodes[0].text) @@ -211,8 +212,9 @@ def release_to_metadata(node, m, config, album=None): release_group_to_metadata(nodes[0], m, config, album) elif name == 'status': m['releasestatus'] = nodes[0].text.lower() - elif name == 'title' and not config.setting["standardize_releases"]: - m['album'] = nodes[0].text + elif name == 'title': + if not config.setting["standardize_releases"]: + m['album'] = nodes[0].text elif name == 'disambiguation': m['~releasecomment'] = nodes[0].text elif name == 'asin': @@ -248,8 +250,9 @@ def release_group_to_metadata(node, m, config, album=None): for name, nodes in node.children.iteritems(): if not nodes: continue - if name == 'title' and config.setting["standardize_releases"]: - m['album'] = node.title[0].text + if name == 'title': + if config.setting["standardize_releases"]: + m['album'] = node.title[0].text elif name == 'first_release_date': m['~originaldate'] = nodes[0].text elif name == 'tag_list': From 42068475e7586eb94becb9a81c62abdf8bb70579 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 14 Jul 2011 21:42:38 -0500 Subject: [PATCH 45/79] When artist standardization is enabled, use the release group artist credit. --- picard/mbxml.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index f1a4ba7e4..9b3be1241 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -220,7 +220,8 @@ def release_to_metadata(node, m, config, album=None): elif name == 'asin': m['asin'] = nodes[0].text elif name == 'artist_credit': - artist_credit_to_metadata(nodes[0], m, config, release=True) + if not config.setting["standardize_artists"]: + artist_credit_to_metadata(nodes[0], m, config, release=True) elif name == 'date': m['date'] = nodes[0].text elif name == 'country': @@ -253,6 +254,9 @@ def release_group_to_metadata(node, m, config, album=None): if name == 'title': if config.setting["standardize_releases"]: m['album'] = node.title[0].text + elif name == 'artist_credit': + if config.setting["standardize_artists"]: + artist_credit_to_metadata(nodes[0], m, config, release=True) elif name == 'first_release_date': m['~originaldate'] = nodes[0].text elif name == 'tag_list': From c5c3c76757341108040d710c01829028dabeedd1 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 14 Jul 2011 22:40:01 -0500 Subject: [PATCH 46/79] Always load collections on startup. --- picard/collection.py | 3 --- picard/ui/itemviews.py | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/picard/collection.py b/picard/collection.py index eddfaa6c9..cb39c324a 100644 --- a/picard/collection.py +++ b/picard/collection.py @@ -31,7 +31,6 @@ class CollectionList(QtCore.QObject): self.view = view self.collections = {} self.releases = {} - self.loading = False self.loaded = False def release_from_obj(self, obj): @@ -56,13 +55,11 @@ class CollectionList(QtCore.QObject): else: self._parse_collection_list(document) self.view.add_collections(self.collections) - self.loading = False self.loaded = True def load(self): self.collections = {} self.releases = {} - self.loading = True self.loaded = False self.tagger.xmlws.get_collection_list(self._collection_list_request_finished) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 8da54f9b9..893505156 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -861,11 +861,9 @@ class CollectionTreeView(QtGui.QTreeWidget): self.refresh_action = QtGui.QAction(icontheme.lookup("view-refresh", icontheme.ICON_SIZE_MENU), _("&Refresh"), self) self.connect(self.refresh_action, QtCore.SIGNAL("triggered()"), self.refresh) self.collection_list = CollectionList(self) + self.collection_list.load() def showEvent(self, event): - cl = self.collection_list - if not (cl.loaded or cl.loading): - cl.load() QtGui.QTreeView.showEvent(self, event) def refresh(self): From 80f98021d6792a3a996a200d700c42ee91f41eea Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Fri, 15 Jul 2011 19:09:10 -0500 Subject: [PATCH 47/79] Disable all "standardization" for pseudo-releases. --- picard/mbxml.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index 9b3be1241..0b76f304c 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -159,19 +159,21 @@ def media_formats_from_node(node): def track_to_metadata(node, track, config): m = track.metadata recording_to_metadata(node.recording[0], track, config) + transl = m['releasestatus'] == "pseudo-release" # overwrite with data we have on the track for name, nodes in node.children.iteritems(): if not nodes: continue if name == 'title': - if not config.setting["standardize_tracks"]: + if not config.setting["standardize_tracks"] or transl: m['title'] = nodes[0].text elif name == 'position': m['tracknumber'] = nodes[0].text elif name == 'length' and nodes[0].text: m.length = int(nodes[0].text) - elif name == 'artist_credit' and not config.setting["standardize_artists"]: - artist_credit_to_metadata(nodes[0], m, config) + elif name == 'artist_credit': + if not config.setting["standardize_artists"] or transl: + artist_credit_to_metadata(nodes[0], m, config) def recording_to_metadata(node, track, config): @@ -205,22 +207,24 @@ def release_to_metadata(node, m, config, album=None): """Make metadata dict from a XML 'release' node.""" m['musicbrainz_albumid'] = node.attribs['id'] + if "status" in node.children: + m['releasestatus'] = node.status[0].text.lower() + transl = m['releasestatus'] == "pseudo-release" + for name, nodes in node.children.iteritems(): if not nodes: continue if name == 'release_group': release_group_to_metadata(nodes[0], m, config, album) - elif name == 'status': - m['releasestatus'] = nodes[0].text.lower() elif name == 'title': - if not config.setting["standardize_releases"]: + if not config.setting["standardize_releases"] or transl: m['album'] = nodes[0].text elif name == 'disambiguation': m['~releasecomment'] = nodes[0].text elif name == 'asin': m['asin'] = nodes[0].text elif name == 'artist_credit': - if not config.setting["standardize_artists"]: + if not config.setting["standardize_artists"] or transl: artist_credit_to_metadata(nodes[0], m, config, release=True) elif name == 'date': m['date'] = nodes[0].text @@ -247,15 +251,16 @@ def release_group_to_metadata(node, m, config, album=None): """Make metadata dict from a XML 'release-group' node taken from inside a 'release' node.""" if 'type' in node.attribs: m['releasetype'] = node.type.lower() + transl = m['releasestatus'] == "pseudo-release" for name, nodes in node.children.iteritems(): if not nodes: continue if name == 'title': - if config.setting["standardize_releases"]: + if config.setting["standardize_releases"] and not transl: m['album'] = node.title[0].text elif name == 'artist_credit': - if config.setting["standardize_artists"]: + if config.setting["standardize_artists"] and not transl: artist_credit_to_metadata(nodes[0], m, config, release=True) elif name == 'first_release_date': m['~originaldate'] = nodes[0].text From 151c41bdde50873bf09af4e4a5cc73ea6ea71a0b Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Fri, 15 Jul 2011 22:43:42 -0500 Subject: [PATCH 48/79] Fixes ticket #5938 (relationships are entered twice (release <-> recording)) --- picard/mbxml.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index 0b76f304c..f670d3dc9 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -87,7 +87,8 @@ def _relations_to_metadata(relation_lists, m, config): name = _artist_rel_types[reltype] except KeyError: continue - m.add(name, value) + if value not in m[name]: + m.add(name, value) elif relation_list.target_type == 'work': for relation in relation_list.relation: if relation.type == 'performance': From 012abbf7de2fe22b5b1d197f90ae3060b0771168 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Fri, 15 Jul 2011 22:51:29 -0500 Subject: [PATCH 49/79] Don't request collections if no username or password is set. --- picard/ui/itemviews.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 893505156..ee6aae2ba 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -861,7 +861,9 @@ class CollectionTreeView(QtGui.QTreeWidget): self.refresh_action = QtGui.QAction(icontheme.lookup("view-refresh", icontheme.ICON_SIZE_MENU), _("&Refresh"), self) self.connect(self.refresh_action, QtCore.SIGNAL("triggered()"), self.refresh) self.collection_list = CollectionList(self) - self.collection_list.load() + + if self.config.setting["username"] and self.config.setting["password"]: + self.collection_list.load() def showEvent(self, event): QtGui.QTreeView.showEvent(self, event) From 26f06b1dbf07b97337a98ab1dd50ecc52c7948ec Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sat, 16 Jul 2011 00:06:01 -0500 Subject: [PATCH 50/79] Drag and drop fix for newer version of Qt. --- picard/ui/options/plugins.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/picard/ui/options/plugins.py b/picard/ui/options/plugins.py index 35a813eda..0f21674c1 100644 --- a/picard/ui/options/plugins.py +++ b/picard/ui/options/plugins.py @@ -51,6 +51,7 @@ class PluginsOptionsPage(OptionsPage): self.connect(self.ui.plugins, QtCore.SIGNAL("itemSelectionChanged()"), self.change_details) self.ui.plugins.__class__.mimeTypes = self.mimeTypes self.ui.plugins.__class__.dropEvent = self.dropEvent + self.ui.plugins.__class__.dragEnterEvent = self.dragEnterEvent if sys.platform == "win32": self.loader="file:///%s" else: @@ -154,6 +155,10 @@ class PluginsOptionsPage(OptionsPage): def mimeTypes(self): return ["text/uri-list"] + def dragEnterEvent(self, event): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + def dropEvent(self, event): for path in [os.path.normpath(unicode(u.toLocalFile())) for u in event.mimeData().urls()]: self.install_plugin(path) From 6ff3b0b178ee6c06810f26016a17a7683f91846f Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sat, 16 Jul 2011 00:10:49 -0500 Subject: [PATCH 51/79] Fix method assignment --- picard/ui/options/plugins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/picard/ui/options/plugins.py b/picard/ui/options/plugins.py index 0f21674c1..8f00b662a 100644 --- a/picard/ui/options/plugins.py +++ b/picard/ui/options/plugins.py @@ -49,9 +49,9 @@ class PluginsOptionsPage(OptionsPage): self.ui.setupUi(self) self.items = {} self.connect(self.ui.plugins, QtCore.SIGNAL("itemSelectionChanged()"), self.change_details) - self.ui.plugins.__class__.mimeTypes = self.mimeTypes - self.ui.plugins.__class__.dropEvent = self.dropEvent - self.ui.plugins.__class__.dragEnterEvent = self.dragEnterEvent + self.ui.plugins.mimeTypes = self.mimeTypes + self.ui.plugins.dropEvent = self.dropEvent + self.ui.plugins.dragEnterEvent = self.dragEnterEvent if sys.platform == "win32": self.loader="file:///%s" else: From 5a64f0bff9069d725a70a56509d35495f21819c4 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sat, 16 Jul 2011 00:50:48 -0500 Subject: [PATCH 52/79] Request user ratings for standalone recordings --- picard/track.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/picard/track.py b/picard/track.py index 14d6cfd1c..3bf803699 100644 --- a/picard/track.py +++ b/picard/track.py @@ -229,6 +229,9 @@ class NonAlbumTrack(Track): inc += ["user-tags"] else: inc += ["tags"] + if self.config.setting["enable_ratings"]: + mblogin = True + inc += ["user-ratings"] self.tagger.xmlws.get_track_by_id(self.id, partial(self._recording_request_finished), inc, mblogin=mblogin) def _recording_request_finished(self, document, http, error): From b1239754a78a799044218c0e8053ac5bf90ad148 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sat, 16 Jul 2011 00:52:34 -0500 Subject: [PATCH 53/79] Fix exception saving a POPM frame with a decimal rating (e.g. 4.5/5) --- picard/formats/id3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index d1ea27dfd..1f11007b9 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -274,7 +274,7 @@ class ID3File(File): count = 0 # Convert rating to range between 0 and 255 - rating = int(values[0]) * 255 / (settings['rating_steps'] - 1) + rating = int(round(float(values[0]) * 255 / (settings['rating_steps'] - 1))) tags.add(id3.POPM(email=settings['rating_user_email'], rating=rating, count=count)) elif name in self.__rtranslate: frameid = self.__rtranslate[name] From 54ff1484100654741c0f5e091d0fb351f85ad99e Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 17 Jul 2011 01:25:19 -0500 Subject: [PATCH 54/79] Allow to work on the compilation tag --- picard/album.py | 36 ++++++++++++++++++++++++++---------- picard/track.py | 11 +---------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/picard/album.py b/picard/album.py index 330a60d6e..2c4c735c5 100644 --- a/picard/album.py +++ b/picard/album.py @@ -103,15 +103,15 @@ class Album(DataObject, Item): # Prepare parser for user's script if self.config.setting["enable_tagger_script"]: script = self.config.setting["tagger_script"] - parser = ScriptParser() else: - script = parser = None + script = None # Strip leading/trailing whitespace m.strip_whitespace() ignore_tags = [s.strip() for s in self.config.setting['ignore_tags'].split(',')] - artists = set() + first_artist = None + compilation = False track_counts = [] m['totaldiscs'] = release_node.medium_list[0].count @@ -137,18 +137,34 @@ class Album(DataObject, Item): if format: tm['media'] = format track_to_metadata(node, config=self.config, track=t) - t._customize_metadata(node, release_node, script, parser, ignore_tags) - - artists.add(tm['musicbrainz_artistid']) m.length += tm.length + artist_id = tm['musicbrainz_artistid'] + if compilation is False: + if first_artist is None: + first_artist = artist_id + if first_artist != artist_id: + compilation = True + for track in self._new_tracks: + track.metadata['compilation'] = '1' + else: + tm['compilation'] = '1' + + t._customize_metadata(node, release_node, ignore_tags) + self.tracks_str = " + ".join(track_counts) - if len(artists) > 1: - for t in self._new_tracks: - t.metadata['compilation'] = '1' - if script: + parser = ScriptParser() + # Run tagger script for each track + for track in self._new_tracks: + tm = track.metadata + try: + parser.eval(script, tm) + except: + self.log.error(traceback.format_exc()) + # Strip leading/trailing whitespace + tm.strip_whitespace() # Run tagger script for the album itself try: parser.eval(script, m) diff --git a/picard/track.py b/picard/track.py index 3bf803699..5d3417b22 100644 --- a/picard/track.py +++ b/picard/track.py @@ -137,7 +137,7 @@ class Track(DataObject): else: return m[column], similarity - def _customize_metadata(self, node, release, script, parser, ignore_tags=None): + def _customize_metadata(self, node, release, ignore_tags=None): tm = self.metadata # 'Translate' artist name @@ -163,15 +163,6 @@ class Track(DataObject): except: self.log.error(traceback.format_exc()) - if script: - # Run TaggerScript - try: - parser.eval(script, tm) - except: - self.log.error(traceback.format_exc()) - # Strip leading/trailing whitespace - tm.strip_whitespace() - def _convert_folksonomy_tags_to_genre(self, ignore_tags): # Combine release and track tags tags = dict(self.folksonomy_tags) From 7be16905bbeaf29f2d9e4c7bbe089377fa8e6ec2 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 17 Jul 2011 10:22:01 -0500 Subject: [PATCH 55/79] Run all metadata processor plugins inside _finalize_loading, before tagger script. (Fixes http://bugs.musicbrainz.org/ticket/5850) --- picard/album.py | 127 +++++++++++++++++++++++++----------------------- picard/track.py | 10 +--- 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/picard/album.py b/picard/album.py index 2c4c735c5..819539cef 100644 --- a/picard/album.py +++ b/picard/album.py @@ -19,8 +19,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import traceback +from collections import deque from PyQt4 import QtCore -from picard.metadata import Metadata, run_album_metadata_processors +from picard.metadata import Metadata, run_album_metadata_processors, run_track_metadata_processors from picard.dataobj import DataObject from picard.file import File from picard.track import Track @@ -47,6 +48,7 @@ class Album(DataObject, Item): self._requests = 0 self._discid = discid self._after_load_callbacks = queue.Queue() + self._metadata_plugins = deque() self.other_versions = [] self.unmatched_files = Cluster(_("Unmatched Files"), special=True, related_album=self, hide_if_empty=True) @@ -94,18 +96,6 @@ class Album(DataObject, Item): if m['musicbrainz_artistid'] == VARIOUS_ARTISTS_ID: m['albumartistsort'] = m['artistsort'] = m['albumartist'] = m['artist'] = self.config.setting['va_name'] - # Album metadata plugins - try: - run_album_metadata_processors(self, m, release_node) - except: - self.log.error(traceback.format_exc()) - - # Prepare parser for user's script - if self.config.setting["enable_tagger_script"]: - script = self.config.setting["tagger_script"] - else: - script = None - # Strip leading/trailing whitespace m.strip_whitespace() @@ -116,6 +106,9 @@ class Album(DataObject, Item): m['totaldiscs'] = release_node.medium_list[0].count + plugins = partial(run_album_metadata_processors, self, m, release_node) + self._metadata_plugins.append(plugins) + for medium in release_node.medium_list[0].medium: discnumber = medium.position[0].text track_list = medium.track_list[0] @@ -150,43 +143,14 @@ class Album(DataObject, Item): else: tm['compilation'] = '1' - t._customize_metadata(node, release_node, ignore_tags) + t._customize_metadata(ignore_tags) + plugins = partial(run_track_metadata_processors, self, tm, release_node, node) + self._metadata_plugins.append(plugins) self.tracks_str = " + ".join(track_counts) - if script: - parser = ScriptParser() - # Run tagger script for each track - for track in self._new_tracks: - tm = track.metadata - try: - parser.eval(script, tm) - except: - self.log.error(traceback.format_exc()) - # Strip leading/trailing whitespace - tm.strip_whitespace() - # Run tagger script for the album itself - try: - parser.eval(script, m) - except: - self.log.error(traceback.format_exc()) - return True - def _parse_release_group(self, document): - releases = document.metadata[0].release_group[0].release_list[0].release - for release in releases: - version = {} - version["mbid"] = release.id - if "date" in release.children: - version["date"] = release.date[0].text - if "country" in release.children: - version["country"] = release.country[0].text - version["tracks"] = " + ".join([m.track_list[0].count for m in release.medium_list[0].medium]) - version["format"] = media_formats_from_node(release.medium_list[0]) - self.other_versions.append(version) - self.other_versions.sort(key=lambda x: x["date"]) - def _release_request_finished(self, document, http, error): parsed = False try: @@ -213,6 +177,20 @@ class Album(DataObject, Item): if parsed or error: self._finalize_loading(error) + def _parse_release_group(self, document): + releases = document.metadata[0].release_group[0].release_list[0].release + for release in releases: + version = {} + version["mbid"] = release.id + if "date" in release.children: + version["date"] = release.date[0].text + if "country" in release.children: + version["country"] = release.country[0].text + version["tracks"] = " + ".join([m.track_list[0].count for m in release.medium_list[0].medium]) + version["format"] = media_formats_from_node(release.medium_list[0]) + self.other_versions.append(version) + self.other_versions.sort(key=lambda x: x["date"]) + def _release_group_request_finished(self, document, http, error): try: if error: @@ -234,22 +212,49 @@ class Album(DataObject, Item): del self._new_metadata del self._new_tracks self.update() - else: - if not self._requests: - for track in self.tracks: - for file in list(track.linked_files): - file.move(self.unmatched_files) - self.metadata = self._new_metadata - self.tracks = self._new_tracks - del self._new_metadata - del self._new_tracks - self.loaded = True - self.match_files(self.unmatched_files.files) - self.update() - self.tagger.window.set_statusbar_message('Album %s loaded', self.id, timeout=3000) - while self._after_load_callbacks.qsize() > 0: - func = self._after_load_callbacks.get() - func() + return + + # Run metadata plugins + while self._metadata_plugins: + try: + self._metadata_plugins.pop()() + except: + self.log.error(traceback.format_exc()) + + if not self._requests: + # Prepare parser for user's script + if self.config.setting["enable_tagger_script"]: + script = self.config.setting["tagger_script"] + if script: + parser = ScriptParser() + # Run tagger script for each track + for track in self._new_tracks: + try: + parser.eval(script, track.metadata) + except: + self.log.error(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: + self.log.error(traceback.format_exc()) + + for track in self.tracks: + for file in list(track.linked_files): + file.move(self.unmatched_files) + self.metadata = self._new_metadata + self.tracks = self._new_tracks + del self._new_metadata + del self._new_tracks + self.loaded = True + self.match_files(self.unmatched_files.files) + self.update() + self.tagger.window.set_statusbar_message('Album %s loaded', self.id, timeout=3000) + while self._after_load_callbacks.qsize() > 0: + func = self._after_load_callbacks.get() + func() def load(self): if self._requests: diff --git a/picard/track.py b/picard/track.py index 5d3417b22..4fc220863 100644 --- a/picard/track.py +++ b/picard/track.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from PyQt4 import QtCore -from picard.metadata import Metadata, run_track_metadata_processors +from picard.metadata import Metadata from picard.dataobj import DataObject from picard.util import format_time, translate_artist, asciipunct, partial from picard.mbxml import recording_to_metadata @@ -137,7 +137,7 @@ class Track(DataObject): else: return m[column], similarity - def _customize_metadata(self, node, release, ignore_tags=None): + def _customize_metadata(self, ignore_tags=None): tm = self.metadata # 'Translate' artist name @@ -157,12 +157,6 @@ class Track(DataObject): if self.config.setting['convert_punctuation']: tm.apply_func(asciipunct) - # Track metadata plugins - try: - run_track_metadata_processors(self.album, tm, release, node) - except: - self.log.error(traceback.format_exc()) - def _convert_folksonomy_tags_to_genre(self, ignore_tags): # Combine release and track tags tags = dict(self.folksonomy_tags) From f3bf2b1270708fd888719370472d900143414c23 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 17 Jul 2011 11:45:34 -0500 Subject: [PATCH 56/79] Small misc. fixes --- picard/album.py | 4 +--- picard/ui/itemviews.py | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/picard/album.py b/picard/album.py index 819539cef..364953aa6 100644 --- a/picard/album.py +++ b/picard/album.py @@ -96,9 +96,6 @@ class Album(DataObject, Item): if m['musicbrainz_artistid'] == VARIOUS_ARTISTS_ID: m['albumartistsort'] = m['artistsort'] = m['albumartist'] = m['artist'] = self.config.setting['va_name'] - # Strip leading/trailing whitespace - m.strip_whitespace() - ignore_tags = [s.strip() for s in self.config.setting['ignore_tags'].split(',')] first_artist = None compilation = False @@ -240,6 +237,7 @@ class Album(DataObject, Item): parser.eval(script, self._new_metadata) except: self.log.error(traceback.format_exc()) + self._new_metadata.strip_whitespace() for track in self.tracks: for file in list(track.linked_files): diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index ee6aae2ba..651319b93 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -402,8 +402,9 @@ class BaseTreeView(QtGui.QTreeWidget): action.setEnabled(False) if not obj.rgloaded: - self.connect(obj, QtCore.SIGNAL("release_group_loaded"), _add_other_versions) - self.tagger.xmlws.get_release_group_by_id(obj.rgid, obj._release_group_request_finished) + if obj.rgid: + self.connect(obj, QtCore.SIGNAL("release_group_loaded"), _add_other_versions) + self.tagger.xmlws.get_release_group_by_id(obj.rgid, obj._release_group_request_finished) else: _add_other_versions() From 22975920cbee0826f4869c123b235bd377bbea59 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 17 Jul 2011 13:22:58 -0500 Subject: [PATCH 57/79] Update NEWS.txt --- NEWS.txt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/NEWS.txt b/NEWS.txt index 92dcf0b01..330f20fbe 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,3 +1,16 @@ +Version 0.15 - 2011-07-17 + * Added options for using standardized track, release, and artist metadata. + * Added preferred release format support. + * Expanded preferred release country support to allow multiple countries. + * Added support for tagging non-album tracks (standalone recordings). + * Plugins can now be installed via drag and drop, or a file browser. + * Added several new tags: %_originaldate%, %_recordingcomment%, and %_releasecomment% + * Changes to request queuing: added separate high and low priority queues for each host. + * Tagger scripts now run after metadata plugins finish (#5850) + * The "compilation" tag can now be $unset or modified via tagger script. + * Added a shortcut (Ctrl+I) for Edit->Details. + * Miscellaneous bug fixes. + Version 0.15beta1 - 2011-05-29 * Support for the NGS web service @@ -118,7 +131,7 @@ Version 0.10 - 2008-07-27 * Fixed crash when reading CD TOC on 64-bit systems * Fixed handling of MP4 files with no metadata * Change the hotkey for help to the right key for OS X - * Replace special characters after tagger script evalutaion to allow + * Replace special characters after tagger script evalutaion to allow special characters being replaced by tagger script * Actually ignore 'ignored (folksonomy) tags' * Remove dependency on Mutagen 1.13, version 1.11 is enough now @@ -218,7 +231,7 @@ Version 0.9.0alpha12 - 2007-07-29 environment variable "PICARD_DEBUG". * For plugins: - metadata["~#length"] is now metadata.length - - metadata["~#artwork"] is now metadata.images + - metadata["~#artwork"] is now metadata.images * New Features: * Save embedded images to MP4 files. * Added option to select release events for albums. From 29e1d38dbed72dda4c5b28f288181938b2c28c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Lalinsk=C3=BD?= Date: Sun, 17 Jul 2011 20:28:00 +0200 Subject: [PATCH 58/79] Increase version number --- picard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/__init__.py b/picard/__init__.py index d71888b7c..99729f377 100644 --- a/picard/__init__.py +++ b/picard/__init__.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. -version_info = (0, 15, 0, 'beta', 2) +version_info = (0, 15, 0, 'final', 0) if version_info[3] == 'final': if version_info[2] == 0: From 551b10f61fb97861c9ee6ab8b38ab5950e1d7663 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 17 Jul 2011 16:59:29 -0500 Subject: [PATCH 59/79] libdiscid fix for OS X --- picard/disc.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/picard/disc.py b/picard/disc.py index 859b36e67..1421d2956 100644 --- a/picard/disc.py +++ b/picard/disc.py @@ -82,7 +82,7 @@ def _openLibrary(): # Check to see if we're running in a Mac OS X bundle. if sys.platform == 'darwin': try: - libDiscId = ctypes.cdll.LoadLibrary('../Frameworks/libdiscid.1.dylib') + libDiscId = ctypes.cdll.LoadLibrary('../Frameworks/libdiscid.0.dylib') _setPrototypes(libDiscId) return libDiscId except OSError, e: @@ -105,7 +105,7 @@ def _openLibrary(): if sys.platform == 'linux2': libName = 'libdiscid.so.0' elif sys.platform == 'darwin': - libName = 'libdiscid.1.dylib' + libName = 'libdiscid.0.dylib' elif sys.platform == 'win32': libName = 'discid.dll' else: diff --git a/setup.py b/setup.py index 4efc1ea34..ffb18e10a 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ try: 'optimize' : 2, 'argv_emulation' : True, 'iconfile' : 'picard.icns', - 'frameworks' : ['libofa.0.dylib', 'libiconv.2.dylib', 'libdiscid.1.dylib'], + 'frameworks' : ['libofa.0.dylib', 'libiconv.2.dylib', 'libdiscid.0.dylib'], 'includes' : ['sip', 'PyQt4.Qt', 'picard.util.astrcmp', 'picard.musicdns.ofa', 'picard.musicdns.avcodec'], 'excludes' : ['pydoc'], 'plist' : { 'CFBundleName' : 'MusicBrainz Picard', From 01cfe146b44a5c9c3bb201884b6b3eaaf8baf166 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 18 Jul 2011 09:15:26 -0500 Subject: [PATCH 60/79] Reverting this to give proper credit. --- picard/mbxml.py | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index f670d3dc9..3e6f3ef91 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -216,7 +216,10 @@ def release_to_metadata(node, m, config, album=None): if not nodes: continue if name == 'release_group': - release_group_to_metadata(nodes[0], m, config, album) + if 'type' in nodes[0].attribs: + m['releasetype'] = nodes[0].type.lower() + if config.setting["standardize_releases"] and not transl: + m['album'] = nodes[0].title[0].text elif name == 'title': if not config.setting["standardize_releases"] or transl: m['album'] = nodes[0].text @@ -248,29 +251,6 @@ def release_to_metadata(node, m, config, album=None): add_user_folksonomy_tags(nodes[0], album) -def release_group_to_metadata(node, m, config, album=None): - """Make metadata dict from a XML 'release-group' node taken from inside a 'release' node.""" - if 'type' in node.attribs: - m['releasetype'] = node.type.lower() - transl = m['releasestatus'] == "pseudo-release" - - for name, nodes in node.children.iteritems(): - if not nodes: - continue - if name == 'title': - if config.setting["standardize_releases"] and not transl: - m['album'] = node.title[0].text - elif name == 'artist_credit': - if config.setting["standardize_artists"] and not transl: - artist_credit_to_metadata(nodes[0], m, config, release=True) - elif name == 'first_release_date': - m['~originaldate'] = nodes[0].text - elif name == 'tag_list': - add_folksonomy_tags(nodes[0], album) - elif name == 'user_tag_list': - add_user_folksonomy_tags(nodes[0], album) - - def add_folksonomy_tags(node, obj): if obj and 'tag' in node.children: for tag in node.tag: From c0520b86e16fcf3813229a2b7a8e3817fad0aabb Mon Sep 17 00:00:00 2001 From: Chad Wilson Date: Mon, 18 Jul 2011 09:18:30 -0500 Subject: [PATCH 61/79] Fixed #5948 - folksonomy tags as genre not supported at NGS RG level --- picard/mbxml.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index 3e6f3ef91..2c70e81d9 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -216,10 +216,7 @@ def release_to_metadata(node, m, config, album=None): if not nodes: continue if name == 'release_group': - if 'type' in nodes[0].attribs: - m['releasetype'] = nodes[0].type.lower() - if config.setting["standardize_releases"] and not transl: - m['album'] = nodes[0].title[0].text + release_group_to_metadata(nodes[0], m, config, album) elif name == 'title': if not config.setting["standardize_releases"] or transl: m['album'] = nodes[0].text @@ -251,6 +248,22 @@ def release_to_metadata(node, m, config, album=None): add_user_folksonomy_tags(nodes[0], album) +def release_group_to_metadata(node, m, config, album=None): + """Make metadata dict from a XML 'release-group' node taken from inside a 'release' node.""" + if 'type' in node.attribs: + m['releasetype'] = node.type.lower() + if config.setting["standardize_releases"]: + m['album'] = node.title[0].text + + for name, nodes in node.children.iteritems(): + if not nodes: + continue + if name == 'tag_list': + add_folksonomy_tags(nodes[0], album) + elif name == 'user_tag_list': + add_user_folksonomy_tags(nodes[0], album) + + def add_folksonomy_tags(node, obj): if obj and 'tag' in node.children: for tag in node.tag: From 20844f37ac42b131510deceb6eb178ccaefe94f3 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 18 Jul 2011 09:19:18 -0500 Subject: [PATCH 62/79] - Add the originaldate tag. - Use the release group artist credit when artist standardization is enabled. --- picard/mbxml.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index 2c70e81d9..f670d3dc9 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -252,13 +252,20 @@ def release_group_to_metadata(node, m, config, album=None): """Make metadata dict from a XML 'release-group' node taken from inside a 'release' node.""" if 'type' in node.attribs: m['releasetype'] = node.type.lower() - if config.setting["standardize_releases"]: - m['album'] = node.title[0].text + transl = m['releasestatus'] == "pseudo-release" for name, nodes in node.children.iteritems(): if not nodes: continue - if name == 'tag_list': + if name == 'title': + if config.setting["standardize_releases"] and not transl: + m['album'] = node.title[0].text + elif name == 'artist_credit': + if config.setting["standardize_artists"] and not transl: + artist_credit_to_metadata(nodes[0], m, config, release=True) + elif name == 'first_release_date': + m['~originaldate'] = nodes[0].text + elif name == 'tag_list': add_folksonomy_tags(nodes[0], album) elif name == 'user_tag_list': add_user_folksonomy_tags(nodes[0], album) From 891c3f82bac5f19ee3f1f8235525f6d4edcdae5e Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 18 Jul 2011 14:36:42 -0500 Subject: [PATCH 63/79] Remove timers slowing down file/directory adds --- picard/tagger.py | 23 ++++++++++------------- picard/util/thread.py | 7 +++---- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/picard/tagger.py b/picard/tagger.py index 54e17c948..42ad3e351 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -29,6 +29,7 @@ import signal import sys import traceback import time +from collections import deque # Install gettext "noop" function. import __builtin__ @@ -347,17 +348,16 @@ class Tagger(QtGui.QApplication): file.load(self._file_loaded) def process_directory_listing(self, root, queue, result=None, error=None): - delay = 10 try: # Read directory listing if result is not None and error is None: files = [] - directories = [] + directories = deque() try: for path in result: path = os.path.join(root, path) if os.path.isdir(path): - directories.append(path) + directories.appendleft(path) else: try: files.append(decode_filename(path)) @@ -367,25 +367,22 @@ class Tagger(QtGui.QApplication): finally: if files: self.add_files(files) - delay = min(25 * len(files), 500) - queue = directories + queue + queue.extendleft(directories) finally: # Scan next directory in the queue try: - path = queue.pop(0) + path = queue.popleft() except IndexError: pass else: - func = partial(self.other_queue.put, - (partial(os.listdir, path), - partial(self.process_directory_listing, - path, queue), - QtCore.Qt.LowEventPriority)) - QtCore.QTimer.singleShot(delay, func) + self.other_queue.put(( + partial(os.listdir, path), + partial(self.process_directory_listing, path, queue), + QtCore.Qt.LowEventPriority)) def add_directory(self, path): path = encode_filename(path) self.other_queue.put((partial(os.listdir, path), - partial(self.process_directory_listing, path, []), + partial(self.process_directory_listing, path, deque()), QtCore.Qt.LowEventPriority)) def get_file_by_id(self, id): diff --git a/picard/util/thread.py b/picard/util/thread.py index ca3768ac0..92245a649 100644 --- a/picard/util/thread.py +++ b/picard/util/thread.py @@ -57,8 +57,7 @@ class Thread(QtCore.QThread): if item is None: continue queue.run_item(self, queue, item) - self.usleep(100) - + def run_item(thread, item): func, next, priority = item try: @@ -91,7 +90,7 @@ class ThreadPool(QtCore.QObject): def __init__(self, parent=None): QtCore.QObject.__init__(self, parent) - self.threads = [] + self.threads = [] ThreadPool.instance = self def start(self): @@ -101,7 +100,7 @@ class ThreadPool(QtCore.QObject): def stop(self): for thread in self.threads: thread.stop() - + # FIXME: if a queue is in more than 1 thread, unlock will be called # more than once. for thread in self.threads: From 78e1cad299427c3a29eb1932ef04e6c1134e0ca4 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 19 Jul 2011 12:36:06 -0500 Subject: [PATCH 64/79] - Fix "album metadata not transfered to tracks anymore" (#5960) - Fix "Picard 0.15 fails to load standalone recordings (non-album tracks)" (#5961) - Move medium metadata parsing out of album.py and into mbxml.py. --- picard/album.py | 62 ++++++++++++++++++++++++------------------------- picard/mbxml.py | 20 +++++++++++++--- picard/track.py | 2 +- 3 files changed, 49 insertions(+), 35 deletions(-) diff --git a/picard/album.py b/picard/album.py index 364953aa6..5a85d54ac 100644 --- a/picard/album.py +++ b/picard/album.py @@ -29,7 +29,7 @@ from picard.script import ScriptParser from picard.ui.item import Item from picard.util import format_time, partial, translate_artist, queue, mbid_validate from picard.cluster import Cluster -from picard.mbxml import release_to_metadata, track_to_metadata, media_formats_from_node +from picard.mbxml import release_to_metadata, medium_to_metadata, track_to_metadata, media_formats_from_node from picard.const import VARIOUS_ARTISTS_ID @@ -106,27 +106,20 @@ class Album(DataObject, Item): plugins = partial(run_album_metadata_processors, self, m, release_node) self._metadata_plugins.append(plugins) - for medium in release_node.medium_list[0].medium: - discnumber = medium.position[0].text - track_list = medium.track_list[0] - totaltracks = track_list.count - track_counts.append(totaltracks) - discsubtitle = medium.title[0].text if "title" in medium.children else "" - format = medium.format[0].text if "format" in medium.children else "" + for medium_node in release_node.medium_list[0].medium: + mm = Metadata() + mm.copy(m) + medium_to_metadata(medium_node, mm) + track_counts.append(mm['totaltracks']) - for node in track_list.track: - t = Track(node.recording[0].id, self) + for track_node in medium_node.track_list[0].track: + t = Track(track_node.recording[0].id, self) self._new_tracks.append(t) # Get track metadata tm = t.metadata - tm.copy(m) - tm['discnumber'] = discnumber - tm['discsubtitle'] = discsubtitle - tm['totaltracks'] = totaltracks - if format: tm['media'] = format - - track_to_metadata(node, config=self.config, track=t) + tm.copy(mm) + track_to_metadata(track_node, t, self.config) m.length += tm.length artist_id = tm['musicbrainz_artistid'] @@ -141,7 +134,7 @@ class Album(DataObject, Item): tm['compilation'] = '1' t._customize_metadata(ignore_tags) - plugins = partial(run_track_metadata_processors, self, tm, release_node, node) + plugins = partial(run_track_metadata_processors, self, tm, release_node, track_node) self._metadata_plugins.append(plugins) self.tracks_str = " + ".join(track_counts) @@ -214,30 +207,37 @@ class Album(DataObject, Item): # Run metadata plugins while self._metadata_plugins: try: - self._metadata_plugins.pop()() + self._metadata_plugins.popleft()() except: self.log.error(traceback.format_exc()) if not self._requests: # Prepare parser for user's script + script = None if self.config.setting["enable_tagger_script"]: script = self.config.setting["tagger_script"] + parser = ScriptParser() + + for track in self._new_tracks: + # Update the track with new album metadata, in case it + # was modified by plugins. + track.metadata.update(self._new_metadata) + # Run tagger script for each track if script: - parser = ScriptParser() - # Run tagger script for each track - for track in self._new_tracks: - try: - parser.eval(script, track.metadata) - except: - self.log.error(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) + parser.eval(script, track.metadata) except: self.log.error(traceback.format_exc()) - self._new_metadata.strip_whitespace() + # Strip leading/trailing whitespace + track.metadata.strip_whitespace() + + # Run tagger script for the album itself + if script: + try: + parser.eval(script, self._new_metadata) + except: + self.log.error(traceback.format_exc()) + self._new_metadata.strip_whitespace() for track in self.tracks: for file in list(track.linked_files): diff --git a/picard/mbxml.py b/picard/mbxml.py index f670d3dc9..7ac43a2b8 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -148,10 +148,10 @@ def media_formats_from_node(node): for medium in node.medium: if "format" in medium.children: text = medium.format[0].text - formats.setdefault(text, 0) - formats[text] += 1 + formats[text] = formats.get(text, 0) + 1 if formats: - return " + ".join([(str(j) + u"×" if j > 1 else "") + RELEASE_FORMATS[i] + return " + ".join([ + (str(j) + u"×" if j > 1 else "") + RELEASE_FORMATS[i] for i, j in formats.items()]) else: return "" @@ -204,6 +204,20 @@ def recording_to_metadata(node, track, config): m['~rating'] = nodes[0].text +def medium_to_metadata(node, m): + for name, nodes in node.children.iteritems(): + if not nodes: + continue + if name == 'position': + m['discnumber'] = nodes[0].text + elif name == 'track_list': + m['totaltracks'] = nodes[0].count + elif name == 'title': + m['discsubtitle'] = nodes[0].text + elif name == 'format': + m['media'] = nodes[0].text + + def release_to_metadata(node, m, config, album=None): """Make metadata dict from a XML 'release' node.""" m['musicbrainz_albumid'] = node.attribs['id'] diff --git a/picard/track.py b/picard/track.py index 4fc220863..1127b59cd 100644 --- a/picard/track.py +++ b/picard/track.py @@ -238,7 +238,7 @@ class NonAlbumTrack(Track): parser = ScriptParser() else: script = parser = None - self._customize_metadata(recording, None, script, parser) + self._customize_metadata(recording) self.loaded = True if self.callback: self.callback() From 75ed23d02a31b589fd4f743aca5574d0948a4102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Lalinsk=C3=BD?= Date: Wed, 20 Jul 2011 11:47:33 +0200 Subject: [PATCH 65/79] Add CD-R to the list of formats, make the code not fail on an unknown format --- picard/const.py | 1 + picard/mbxml.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/picard/const.py b/picard/const.py index 90d841623..3dde4065e 100644 --- a/picard/const.py +++ b/picard/const.py @@ -44,6 +44,7 @@ VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' # Release formats RELEASE_FORMATS = { u'CD': N_('CD'), + u'CD-R': N_('CD-R'), u'HDCD': N_('HDCD'), u'Vinyl': N_('Vinyl'), u'7" Vinyl': N_('7" Vinyl'), diff --git a/picard/mbxml.py b/picard/mbxml.py index 7ac43a2b8..83d66d466 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -151,7 +151,7 @@ def media_formats_from_node(node): formats[text] = formats.get(text, 0) + 1 if formats: return " + ".join([ - (str(j) + u"×" if j > 1 else "") + RELEASE_FORMATS[i] + (str(j) + u"×" if j > 1 else "") + RELEASE_FORMATS.get(i, i) for i, j in formats.items()]) else: return "" From 3e2c86c38a30a1fe91ca35e53aace0b3479254b6 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 20 Jul 2011 14:25:53 -0500 Subject: [PATCH 66/79] Merging with gsoc-ngs branch --- picard/album.py | 3 ++- picard/mbxml.py | 20 +++++++----------- picard/tagger.py | 18 ++++++---------- picard/util/thread.py | 49 +++++++++++++------------------------------ 4 files changed, 31 insertions(+), 59 deletions(-) diff --git a/picard/album.py b/picard/album.py index 5a85d54ac..75d1c3a92 100644 --- a/picard/album.py +++ b/picard/album.py @@ -221,7 +221,8 @@ class Album(DataObject, Item): for track in self._new_tracks: # Update the track with new album metadata, in case it # was modified by plugins. - track.metadata.update(self._new_metadata) + for key, values in self._new_metadata.rawitems(): + track.metadata[key] = values[:] # Run tagger script for each track if script: try: diff --git a/picard/mbxml.py b/picard/mbxml.py index 83d66d466..b61cb1e28 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -98,15 +98,6 @@ def _relations_to_metadata(relation_lists, m, config): # TODO: Release, Track, URL relations -def _set_artist_item(m, release, albumname, name, value): - if release: - m[albumname] = value - if name not in m: - m[name] = value - else: - m[name] = value - - def artist_credit_from_node(node, config): artist = "" artistsort = "" @@ -125,10 +116,15 @@ def artist_credit_from_node(node, config): def artist_credit_to_metadata(node, m, config, release=False): ids = [n.artist[0].id for n in node.name_credit] - _set_artist_item(m, release, 'musicbrainz_albumartistid', 'musicbrainz_artistid', ids) artist, artistsort = artist_credit_from_node(node, config) - _set_artist_item(m, release, 'albumartist', 'artist', artist) - _set_artist_item(m, release, 'albumartistsort', 'artistsort', artistsort) + if release: + m["musicbrainz_albumartistid"] = ids + m["albumartist"] = artist + m["albumartistsort"] = artistsort + else: + m["musicbrainz_artistid"] = ids + m["artist"] = artist + m["artistsort"] = artistsort def label_info_from_node(node): diff --git a/picard/tagger.py b/picard/tagger.py index 42ad3e351..4123862c7 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -127,25 +127,19 @@ class Tagger(QtGui.QApplication): self.thread_pool = thread.ThreadPool(self) self.load_queue = queue.Queue() - self.load_queue.run_item = thread.generic_run_item - self.save_queue = queue.Queue() - self.save_queue.run_item = thread.generic_run_item - self.analyze_queue = queue.Queue() self.analyze_queue.run_item = analyze_thread_run_item self.analyze_queue.next = self._lookup_puid - self.other_queue = queue.Queue() - self.other_queue.run_item = thread.generic_run_item threads = self.thread_pool.threads - threads.append(thread.Thread(self.thread_pool, [self.load_queue, - self.other_queue])) - threads.append(thread.Thread(self.thread_pool, [self.save_queue])) - threads.append(thread.Thread(self.thread_pool, [self.other_queue, - self.load_queue])) - threads.append(thread.Thread(self.thread_pool, [self.analyze_queue])) + threads.append(thread.Thread(self.thread_pool, self.load_queue)) + threads.append(thread.Thread(self.thread_pool, self.load_queue)) + threads.append(thread.Thread(self.thread_pool, self.save_queue)) + threads.append(thread.Thread(self.thread_pool, self.other_queue)) + threads.append(thread.Thread(self.thread_pool, self.other_queue)) + threads.append(thread.Thread(self.thread_pool, self.analyze_queue)) self.thread_pool.start() self.stopping = False diff --git a/picard/util/thread.py b/picard/util/thread.py index 92245a649..7cc7063cd 100644 --- a/picard/util/thread.py +++ b/picard/util/thread.py @@ -18,6 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import sys +import traceback from picard.util.queue import Queue from PyQt4 import QtCore @@ -36,34 +37,29 @@ class ProxyToMainEvent(QtCore.QEvent): class Thread(QtCore.QThread): - def __init__(self, parent, queues): + def __init__(self, parent, queue): QtCore.QThread.__init__(self, parent) - self.queues = queues + self.queue = queue self.stopping = False def stop(self): self.stopping = True - self.queues[0].put(None) - - def get_job(self): - for queue in self.queues: - if queue.qsize() > 0: - return (queue, queue.get()) - return (self.queues[0], self.queues[0].get()) + self.queue.put(None) def run(self): while not self.stopping: - queue, item = self.get_job() + item = None + if self.queue.qsize() > 0: + item = self.queue.get() if item is None: continue - queue.run_item(self, queue, item) + self.run_item(item) - def run_item(thread, item): + def run_item(self, item): func, next, priority = item try: result = func() except: - import traceback self.log.error(traceback.format_exc()) self.to_main(next, priority, error=sys.exc_info()[1]) else: @@ -73,16 +69,6 @@ class Thread(QtCore.QThread): event = ProxyToMainEvent(func, args, kwargs) QtCore.QCoreApplication.postEvent(self.parent(), event, priority) -def generic_run_item(thread, queue, item): - func, next, priority = item - try: - result = func() - except: - import traceback - thread.log.error(traceback.format_exc()) - thread.to_main(next, priority, error=sys.exc_info()[1]) - else: - thread.to_main(next, priority, result=result) class ThreadPool(QtCore.QObject): @@ -98,23 +84,18 @@ class ThreadPool(QtCore.QObject): thread.start(QtCore.QThread.LowPriority) def stop(self): + queues = set() for thread in self.threads: thread.stop() - - # FIXME: if a queue is in more than 1 thread, unlock will be called - # more than once. - for thread in self.threads: - for queue in thread.queues: - queue.unlock() - #for thread in self.threads: - # self.log.debug("Waiting for %r", thread) - # thread.wait() + queues.add(thread.queue) + for queue in queues: + queue.unlock() def event(self, event): if isinstance(event, ProxyToMainEvent): - try: event.call() + try: + event.call() except: - import traceback self.log.error(traceback.format_exc()) return True return False From 7a6f69f64bde6f7e9327241dac39e24f997e429e Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Wed, 20 Jul 2011 15:39:15 -0500 Subject: [PATCH 67/79] Fix [no release info] and exception for releases without a date in the other versions menu --- picard/album.py | 2 ++ picard/ui/itemviews.py | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/picard/album.py b/picard/album.py index 75d1c3a92..53cb1ee5b 100644 --- a/picard/album.py +++ b/picard/album.py @@ -174,6 +174,8 @@ class Album(DataObject, Item): version["mbid"] = release.id if "date" in release.children: version["date"] = release.date[0].text + else: + version["date"] = "" if "country" in release.children: version["country"] = release.country[0].text version["tracks"] = " + ".join([m.track_list[0].count for m in release.medium_list[0].medium]) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index c0d5bfb63..c1bbc5093 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -382,15 +382,17 @@ class BaseTreeView(QtGui.QTreeWidget): actions = [] for i, version in enumerate(obj.other_versions): name = [] - if "date" in version and version["date"]: + if version["date"]: name.append(version["date"]) if "country" in version: name.append(RELEASE_COUNTRIES.get(version["country"], version["country"])) name.append(version["tracks"]) - if "format" in version and version["format"]: + if version["format"]: name.append(version["format"]) + if len(name) == 1: + name.insert(0, _('[no release info]')) version_name = " / ".join(name).replace('&', '&&') - action = releases_menu.addAction(version_name or _('[no release info]')) + action = releases_menu.addAction(version_name) action.setData(QtCore.QVariant(i)) action.setCheckable(True) if obj.id == version["mbid"]: From 5885e7750abbde382f51fe59c2bdd92f2841037f Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 21 Jul 2011 01:23:41 -0500 Subject: [PATCH 68/79] Fix requests breaking at midnight. --- picard/webservice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/webservice.py b/picard/webservice.py index 5e576d08a..95fe32569 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -145,7 +145,7 @@ class XmlWebService(QtCore.QObject): send = self._request_methods[method] reply = send(request, data) if data is not None else send(request) key = (host, port) - self._last_request_times[key] = QtCore.QTime.currentTime() + self._last_request_times[key] = QtCore.QDateTime.currentDateTime() self._active_requests[reply] = (request, handler, xml) return True @@ -209,7 +209,7 @@ class XmlWebService(QtCore.QObject): queue = self._high_priority_queues.get(key) or self._low_priority_queues.get(key) if not queue: continue - now = QtCore.QTime.currentTime() + now = QtCore.QDateTime.currentDateTime() last = self._last_request_times.get(key) request_delay = REQUEST_DELAY[key] last_ms = last.msecsTo(now) if last is not None else request_delay From 57a5aa4589337b1da307043784bd3a68a2eb86fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Lalinsk=C3=BD?= Date: Thu, 21 Jul 2011 20:16:02 +0200 Subject: [PATCH 69/79] Try harder to get the ASIN --- picard/mbxml.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index b61cb1e28..fbea87cec 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -23,6 +23,8 @@ from picard.util import format_time, translate_artist from picard.const import RELEASE_FORMATS +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", "conductor": "conductor", @@ -95,7 +97,13 @@ def _relations_to_metadata(relation_lists, m, config): work = relation.work[0] if 'relation_list' in work.children: _relations_to_metadata(work.relation_list, m, config) - # TODO: Release, Track, URL relations + 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) def artist_credit_from_node(node, config): From 895b690d87ccd1a31311a7d4f60a51f03f0ae349 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Thu, 21 Jul 2011 15:40:44 -0500 Subject: [PATCH 70/79] Use a browse request to load other release versions, and include label/catalog number information. --- picard/album.py | 26 ++++++++++++-------------- picard/ui/itemviews.py | 20 +++++++------------- picard/webservice.py | 15 +++++++++++---- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/picard/album.py b/picard/album.py index 53cb1ee5b..616bb76be 100644 --- a/picard/album.py +++ b/picard/album.py @@ -29,7 +29,7 @@ from picard.script import ScriptParser from picard.ui.item import Item from picard.util import format_time, partial, translate_artist, queue, mbid_validate from picard.cluster import Cluster -from picard.mbxml import release_to_metadata, medium_to_metadata, track_to_metadata, media_formats_from_node +from picard.mbxml import release_to_metadata, medium_to_metadata, track_to_metadata, media_formats_from_node, label_info_from_node from picard.const import VARIOUS_ARTISTS_ID @@ -168,19 +168,17 @@ class Album(DataObject, Item): self._finalize_loading(error) def _parse_release_group(self, document): - releases = document.metadata[0].release_group[0].release_list[0].release - for release in releases: - version = {} - version["mbid"] = release.id - if "date" in release.children: - version["date"] = release.date[0].text - else: - version["date"] = "" - if "country" in release.children: - version["country"] = release.country[0].text - version["tracks"] = " + ".join([m.track_list[0].count for m in release.medium_list[0].medium]) - version["format"] = media_formats_from_node(release.medium_list[0]) - self.other_versions.append(version) + for node in document.metadata[0].release_list[0].release: + v = {} + v["mbid"] = node.id + v["date"] = node.date[0].text if "date" in node.children else "" + v["country"] = node.country[0].text if "country" in node.children else "" + labels, catnums = label_info_from_node(node.label_info_list[0]) + v["labels"] = ", ".join(set(labels)) + v["catnums"] = ", ".join(set(catnums)) + v["tracks"] = " + ".join([m.track_list[0].count for m in node.medium_list[0].medium]) + v["format"] = media_formats_from_node(node.medium_list[0]) + self.other_versions.append(v) self.other_versions.sort(key=lambda x: x["date"]) def _release_group_request_finished(self, document, http, error): diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index c1bbc5093..e1d5d7bae 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -381,18 +381,11 @@ class BaseTreeView(QtGui.QTreeWidget): switch_release_version = partial(self._switch_release_version, obj) actions = [] for i, version in enumerate(obj.other_versions): - name = [] - if version["date"]: - name.append(version["date"]) - if "country" in version: - name.append(RELEASE_COUNTRIES.get(version["country"], version["country"])) - name.append(version["tracks"]) - if version["format"]: - name.append(version["format"]) - if len(name) == 1: - name.insert(0, _('[no release info]')) - version_name = " / ".join(name).replace('&', '&&') - action = releases_menu.addAction(version_name) + keys = ("date", "country", "labels", "catnums", "tracks", "format") + name = " / ".join([version[k] for k in keys if version[k]]) + if name == version["tracks"]: + name = "%s / %s" % (_('[no release info]'), name) + action = releases_menu.addAction(name) action.setData(QtCore.QVariant(i)) action.setCheckable(True) if obj.id == version["mbid"]: @@ -405,7 +398,8 @@ class BaseTreeView(QtGui.QTreeWidget): if not obj.rgloaded: if obj.rgid: self.connect(obj, QtCore.SIGNAL("release_group_loaded"), _add_other_versions) - self.tagger.xmlws.get_release_group_by_id(obj.rgid, obj._release_group_request_finished) + kwargs = {"release-group": obj.rgid, "limit": 100} + self.tagger.xmlws.browse_releases(obj._release_group_request_finished, **kwargs) else: _add_other_versions() diff --git a/picard/webservice.py b/picard/webservice.py index 95fe32569..94514080a 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -260,10 +260,6 @@ class XmlWebService(QtCore.QObject): if params: path += "&" + "&".join(params) return self.get(host, port, path, handler, priority=priority, important=important, mblogin=mblogin) - def get_release_group_by_id(self, releasegroupid, handler, priority=True, important=True): - inc = ['releases', 'media'] - return self._get_by_id('release-group', releasegroupid, handler, inc, priority=priority, important=important) - def get_release_by_id(self, releaseid, handler, inc=[], priority=True, important=False, mblogin=False): return self._get_by_id('release', releaseid, handler, inc, priority=priority, important=important, mblogin=mblogin) @@ -303,6 +299,17 @@ class XmlWebService(QtCore.QObject): def find_tracks(self, handler, **kwargs): return self._find('recording', handler, kwargs) + def _browse(self, entitytype, handler, kwargs, inc=[], priority=False, important=False): + host = self.config.setting["server_host"] + port = self.config.setting["server_port"] + params = "&".join(["%s=%s" % (k, v) for k, v in kwargs.items()]) + path = "/ws/2/%s?%s&inc=%s" % (entitytype, params, "+".join(inc)) + return self.get(host, port, path, handler, priority=priority, important=important) + + def browse_releases(self, handler, priority=True, important=True, **kwargs): + inc = ["media", "labels"] + return self._browse("release", handler, kwargs, inc, priority=priority, important=important) + def submit_puids(self, puids, handler): path = '/ws/2/recording/?client=' + USER_AGENT_STRING recordings = ''.join(['' % i for i in puids.items()]) From dde6b7d9af1e8c6c3b1a0e04f3ec2cb2dd1c4ad2 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 24 Jul 2011 22:00:43 -0500 Subject: [PATCH 71/79] Fix PUID scanning --- picard/musicdns/__init__.py | 5 ++++- picard/tagger.py | 15 --------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/picard/musicdns/__init__.py b/picard/musicdns/__init__.py index 2d1820dd8..01e15e856 100644 --- a/picard/musicdns/__init__.py +++ b/picard/musicdns/__init__.py @@ -127,7 +127,10 @@ class OFA(QtCore.QObject): return # calculate fingerprint if ofa is not None: - self.tagger.analyze_queue.put(file.filename) + self.tagger.analyze_queue.put(( + partial(self.calculate_fingerprint, file.filename), + partial(self._lookup_fingerprint, self.tagger._lookup_puid, file.filename), + QtCore.Qt.LowEventPriority + 1)) return # no PUID next(result=None) diff --git a/picard/tagger.py b/picard/tagger.py index 4123862c7..7b9d3b781 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -129,8 +129,6 @@ class Tagger(QtGui.QApplication): self.load_queue = queue.Queue() self.save_queue = queue.Queue() self.analyze_queue = queue.Queue() - self.analyze_queue.run_item = analyze_thread_run_item - self.analyze_queue.next = self._lookup_puid self.other_queue = queue.Queue() threads = self.thread_pool.threads @@ -175,7 +173,6 @@ class Tagger(QtGui.QApplication): # Initialize fingerprinting self._ofa = musicdns.OFA() self._ofa.init() - self.analyze_queue.ofa = self._ofa # Load plugins self.pluginmanager = PluginManager() @@ -633,18 +630,6 @@ class Tagger(QtGui.QApplication): def num_pending_files(self): return len([file for file in self.files.values() if file.state == File.PENDING]) -def analyze_thread_run_item(thread, queue, filename): - next = partial(queue.ofa._lookup_fingerprint, queue.next, filename) - priority = QtCore.Qt.LowEventPriority + 1 - try: - result = queue.ofa.calculate_fingerprint(filename) - except: - import traceback - thread.log.error(traceback.format_exc()) - thread.to_main(next, priority, error=sys.exc_info()[1]) - else: - thread.to_main(next, priority, result=result) - def help(): print """Usage: %s [OPTIONS] [FILE] [FILE] ... From 699453fa6ebbd11524316eeb8ddde0da043a7934 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Sun, 24 Jul 2011 22:55:39 -0500 Subject: [PATCH 72/79] Use time.time() to time requests, instead of Qt's QDateTime.currentDateTime(), because its 'msecsTo' function wasn't added until 4.7. --- picard/webservice.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/picard/webservice.py b/picard/webservice.py index 94514080a..24d00220f 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -27,6 +27,7 @@ import os import sys import re import traceback +import time from collections import deque, defaultdict from PyQt4 import QtCore, QtNetwork, QtXml from picard import version_string @@ -145,7 +146,7 @@ class XmlWebService(QtCore.QObject): send = self._request_methods[method] reply = send(request, data) if data is not None else send(request) key = (host, port) - self._last_request_times[key] = QtCore.QDateTime.currentDateTime() + self._last_request_times[key] = time.time() self._active_requests[reply] = (request, handler, xml) return True @@ -209,10 +210,10 @@ class XmlWebService(QtCore.QObject): queue = self._high_priority_queues.get(key) or self._low_priority_queues.get(key) if not queue: continue - now = QtCore.QDateTime.currentDateTime() + now = time.time() last = self._last_request_times.get(key) request_delay = REQUEST_DELAY[key] - last_ms = last.msecsTo(now) if last is not None else request_delay + last_ms = (now - last) * 1000 if last is not None else request_delay if last_ms >= request_delay: self.log.debug("Last request to %s was %d ms ago, starting another one", key, last_ms) d = request_delay From cee071288d0f1b5266e481d5e155e21e1ae524f7 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 25 Jul 2011 00:17:07 -0500 Subject: [PATCH 73/79] Stop analyzing files when they're removed from the interface. --- picard/file.py | 1 + picard/musicdns/__init__.py | 22 +++++++++++++++++----- picard/tagger.py | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/picard/file.py b/picard/file.py index 60fc79d2d..627c1bdce 100644 --- a/picard/file.py +++ b/picard/file.py @@ -329,6 +329,7 @@ class File(LockableObject, Item): if parent != self.parent: self.log.debug("Moving %r from %r to %r", self, self.parent, parent) self.clear_lookup_task() + self.tagger._ofa.stop_analyze(self) if self.parent: self.clear_pending() self.parent.remove_file(self) diff --git a/picard/musicdns/__init__.py b/picard/musicdns/__init__.py index 01e15e856..ac1282451 100644 --- a/picard/musicdns/__init__.py +++ b/picard/musicdns/__init__.py @@ -35,6 +35,7 @@ class OFA(QtCore.QObject): self.log.warning( "Libofa not found! Fingerprinting will be disabled.") self._decoders = [] + self._analyze_tasks = {} plugins = ["avcodec", "directshow", "quicktime", "gstreamer"] for name in plugins: try: @@ -86,7 +87,8 @@ class OFA(QtCore.QObject): def _lookup_fingerprint(self, next, filename, result=None, error=None): try: file = self.tagger.files[filename] - except (KeyError): + del self._analyze_tasks[file] + except KeyError: # The file has been removed. do nothing return @@ -127,10 +129,20 @@ class OFA(QtCore.QObject): return # calculate fingerprint if ofa is not None: - self.tagger.analyze_queue.put(( - partial(self.calculate_fingerprint, file.filename), - partial(self._lookup_fingerprint, self.tagger._lookup_puid, file.filename), - QtCore.Qt.LowEventPriority + 1)) + if file not in self._analyze_tasks: + task = (partial(self.calculate_fingerprint, file.filename), + partial(self._lookup_fingerprint, self.tagger._lookup_puid, file.filename), + QtCore.Qt.LowEventPriority + 1) + self._analyze_tasks[file] = task + self.tagger.analyze_queue.put(task) return # no PUID next(result=None) + + def stop_analyze(self, file): + try: + task = self._analyze_tasks[file] + self.tagger.analyze_queue.remove(task) + del self._analyze_tasks[file] + except: + pass diff --git a/picard/tagger.py b/picard/tagger.py index 7b9d3b781..dddd78faf 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -483,7 +483,7 @@ class Tagger(QtGui.QApplication): for file in files: if self.files.has_key(file.filename): file.clear_lookup_task() - self.analyze_queue.remove(file.filename) + self._ofa.stop_analyze(file) del self.files[file.filename] file.remove(from_parent) From fd9cc93554172ffac2d47a0ba17d183de45b4c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Wei=C3=9Fl?= Date: Mon, 25 Jul 2011 23:59:51 +0200 Subject: [PATCH 74/79] Add no_release plugin. --- contrib/plugins/no_release.py | 104 ++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 contrib/plugins/no_release.py diff --git a/contrib/plugins/no_release.py b/contrib/plugins/no_release.py new file mode 100644 index 000000000..f3b628079 --- /dev/null +++ b/contrib/plugins/no_release.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +PLUGIN_NAME = u'No release' +PLUGIN_AUTHOR = u'Johannes Weißl' +PLUGIN_DESCRIPTION = '''Do not store specific release information in releases of unknown origin.''' +PLUGIN_VERSION = '0.1' +PLUGIN_API_VERSIONS = ['0.15'] + +from PyQt4 import QtCore, QtGui + +from picard.album import Album +from picard.metadata import register_album_metadata_processor, register_track_metadata_processor +from picard.ui.options import register_options_page, OptionsPage +from picard.ui.itemviews import BaseAction, register_album_action +from picard.config import BoolOption, TextOption + +class Ui_NoReleaseOptionsPage(object): + def setupUi(self, NoReleaseOptionsPage): + NoReleaseOptionsPage.setObjectName('NoReleaseOptionsPage') + NoReleaseOptionsPage.resize(394, 300) + self.verticalLayout = QtGui.QVBoxLayout(NoReleaseOptionsPage) + self.verticalLayout.setObjectName('verticalLayout') + self.groupBox = QtGui.QGroupBox(NoReleaseOptionsPage) + self.groupBox.setObjectName('groupBox') + self.vboxlayout = QtGui.QVBoxLayout(self.groupBox) + self.vboxlayout.setObjectName('vboxlayout') + self.norelease_enable = QtGui.QCheckBox(self.groupBox) + self.norelease_enable.setObjectName('norelease_enable') + self.vboxlayout.addWidget(self.norelease_enable) + self.label = QtGui.QLabel(self.groupBox) + self.label.setObjectName('label') + self.vboxlayout.addWidget(self.label) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName('horizontalLayout') + self.norelease_strip_tags = QtGui.QLineEdit(self.groupBox) + self.norelease_strip_tags.setObjectName('norelease_strip_tags') + self.horizontalLayout.addWidget(self.norelease_strip_tags) + self.vboxlayout.addLayout(self.horizontalLayout) + self.verticalLayout.addWidget(self.groupBox) + spacerItem = QtGui.QSpacerItem(368, 187, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + + self.retranslateUi(NoReleaseOptionsPage) + QtCore.QMetaObject.connectSlotsByName(NoReleaseOptionsPage) + + def retranslateUi(self, NoReleaseOptionsPage): + self.groupBox.setTitle(QtGui.QApplication.translate('NoReleaseOptionsPage', 'No release', None, QtGui.QApplication.UnicodeUTF8)) + self.norelease_enable.setText(QtGui.QApplication.translate('NoReleaseOptionsPage', _('Enable plugin for all releases by default'), None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate('NoReleaseOptionsPage', _('Tags to strip (comma-separated)'), None, QtGui.QApplication.UnicodeUTF8)) + +def strip_release_specific_metadata(tagger, metadata): + strip_tags = tagger.config.setting['norelease_strip_tags'] + strip_tags = [tag.strip() for tag in strip_tags.split(',')] + for tag in strip_tags: + if tag in metadata: + del metadata[tag] + +class NoReleaseAction(BaseAction): + NAME = _('Remove specific release information...') + def callback(self, objs): + for album in objs: + if isinstance(album, Album): + strip_release_specific_metadata(self.tagger, album.metadata) + for track in album.tracks: + strip_release_specific_metadata(self.tagger, track.metadata) + for file in track.linked_files: + track.update_file_metadata(file) + album.update() + +class NoReleaseOptionsPage(OptionsPage): + NAME = 'norelease' + TITLE = 'No release' + PARENT = 'plugins' + + options = [ + BoolOption('setting', 'norelease_enable', False), + TextOption('setting', 'norelease_strip_tags', 'asin,barcode,catalognumber,date,label,media,releasecountry,releasestatus'), + ] + + def __init__(self, parent=None): + super(NoReleaseOptionsPage, self).__init__(parent) + self.ui = Ui_NoReleaseOptionsPage() + self.ui.setupUi(self) + + def load(self): + self.ui.norelease_strip_tags.setText(self.config.setting['norelease_strip_tags']) + self.ui.norelease_enable.setChecked(self.config.setting['norelease_enable']) + + def save(self): + self.config.setting['norelease_strip_tags'] = unicode(self.ui.norelease_strip_tags.text()) + self.config.setting['norelease_enable'] = self.ui.norelease_enable.isChecked() + +def NoReleaseAlbumProcessor(tagger, metadata, release): + if tagger.config.setting['norelease_enable']: + strip_release_specific_metadata(tagger, metadata) + +def NoReleaseTrackProcessor(tagger, metadata, track, release): + if tagger.config.setting['norelease_enable']: + strip_release_specific_metadata(tagger, metadata) + +register_album_metadata_processor(NoReleaseAlbumProcessor) +register_track_metadata_processor(NoReleaseTrackProcessor) +register_album_action(NoReleaseAction()) +register_options_page(NoReleaseOptionsPage) From 269b92ada9bc57c0297137b4c1ea79ce284cf2f7 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 26 Jul 2011 00:29:31 -0500 Subject: [PATCH 75/79] Stop loading albums when they are removed from the interface. --- picard/album.py | 11 +++++++++-- picard/tagger.py | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/picard/album.py b/picard/album.py index 616bb76be..a4dc797d6 100644 --- a/picard/album.py +++ b/picard/album.py @@ -42,6 +42,7 @@ class Album(DataObject, Item): self.format_str = "" self.tracks_str = "" self.loaded = False + self.load_task = None self.rgloaded = False self.rgid = None self._files = 0 @@ -142,6 +143,7 @@ class Album(DataObject, Item): return True def _release_request_finished(self, document, http, error): + self.load_task = None parsed = False try: if error: @@ -282,8 +284,9 @@ class Album(DataObject, Item): if self.config.setting['enable_ratings']: require_authentication = True inc += ['user-ratings'] - self.tagger.xmlws.get_release_by_id(self.id, self._release_request_finished, inc=inc, - mblogin=require_authentication) + self.load_task = self.tagger.xmlws.get_release_by_id( + self.id, self._release_request_finished, inc=inc, + mblogin=require_authentication) def run_when_loaded(self, func): if self.loaded: @@ -291,6 +294,10 @@ class Album(DataObject, Item): else: self._after_load_callbacks.put(func) + def stop_loading(self): + if self.load_task: + self.tagger.xmlws.remove_task(self.load_task) + def update(self, update_tracks=True): self.tagger.emit(QtCore.SIGNAL("album_updated"), self, update_tracks) diff --git a/picard/tagger.py b/picard/tagger.py index dddd78faf..f5c90d16f 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -490,6 +490,7 @@ class Tagger(QtGui.QApplication): def remove_album(self, album): """Remove the specified album.""" self.log.debug("Removing %r", album) + album.stop_loading() self.remove_files(self.get_files_from_objects([album])) self.albums.remove(album) self.emit(QtCore.SIGNAL("album_removed"), album) From 87e40449a673ed330fc45fb7d16d8a459f5ec8ff Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 26 Jul 2011 00:37:30 -0500 Subject: [PATCH 76/79] Make sure removed albums return if they happen to reach _release_request_finished --- picard/album.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/picard/album.py b/picard/album.py index a4dc797d6..c6d7d689b 100644 --- a/picard/album.py +++ b/picard/album.py @@ -143,6 +143,8 @@ class Album(DataObject, Item): return True def _release_request_finished(self, document, http, error): + if self.load_task is None: + return self.load_task = None parsed = False try: @@ -297,6 +299,7 @@ class Album(DataObject, Item): def stop_loading(self): if self.load_task: self.tagger.xmlws.remove_task(self.load_task) + self.load_task = None def update(self, update_tracks=True): self.tagger.emit(QtCore.SIGNAL("album_updated"), self, update_tracks) From ddbace5b37a16c281aebb9335a6138eca58df214 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 26 Jul 2011 01:43:52 -0500 Subject: [PATCH 77/79] Fix high CPU usage while idle (#5968) --- picard/util/thread.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/picard/util/thread.py b/picard/util/thread.py index 7cc7063cd..ad99a8019 100644 --- a/picard/util/thread.py +++ b/picard/util/thread.py @@ -48,9 +48,7 @@ class Thread(QtCore.QThread): def run(self): while not self.stopping: - item = None - if self.queue.qsize() > 0: - item = self.queue.get() + item = self.queue.get() if item is None: continue self.run_item(item) From 2ef0f904025591f44755a8afad9ac9735f52a030 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 26 Jul 2011 02:53:54 -0500 Subject: [PATCH 78/79] Remove indentation that doesn't match the rest of the code --- picard/util/thread.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/picard/util/thread.py b/picard/util/thread.py index ad99a8019..68a0e5293 100644 --- a/picard/util/thread.py +++ b/picard/util/thread.py @@ -54,14 +54,14 @@ class Thread(QtCore.QThread): self.run_item(item) def run_item(self, item): - func, next, priority = item - try: - result = func() - except: - self.log.error(traceback.format_exc()) - self.to_main(next, priority, error=sys.exc_info()[1]) - else: - self.to_main(next, priority, result=result) + func, next, priority = item + try: + result = func() + except: + self.log.error(traceback.format_exc()) + self.to_main(next, priority, error=sys.exc_info()[1]) + else: + self.to_main(next, priority, result=result) def to_main(self, func, priority, *args, **kwargs): event = ProxyToMainEvent(func, args, kwargs) From 0c62d3784cc1b1a2e46173bcb1eee84ba5868a25 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Tue, 26 Jul 2011 18:07:13 -0500 Subject: [PATCH 79/79] - Fix display of ampersands in the "other versions" menu. - Only display this submenu once the release is actually loaded, otherwise it's confusing. It says "Loading..." but never does, because it doesn't have the release group MBID yet. - Get rid of old logic for adding a "No other versions" item. This is never reached because it will at least always display the one existing version. --- picard/ui/itemviews.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index e1d5d7bae..16c44c2bb 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -369,7 +369,7 @@ class BaseTreeView(QtGui.QTreeWidget): menu.addAction(self.window.save_action) menu.addAction(self.window.remove_action) - if isinstance(obj, Album) and not isinstance(obj, NatAlbum): + if isinstance(obj, Album) and not isinstance(obj, NatAlbum) and obj.loaded: releases_menu = QtGui.QMenu(_("&Other versions"), menu) menu.addSeparator() menu.addMenu(releases_menu) @@ -382,7 +382,7 @@ class BaseTreeView(QtGui.QTreeWidget): actions = [] for i, version in enumerate(obj.other_versions): keys = ("date", "country", "labels", "catnums", "tracks", "format") - name = " / ".join([version[k] for k in keys if version[k]]) + name = " / ".join([version[k] for k in keys if version[k]]).replace("&", "&&") if name == version["tracks"]: name = "%s / %s" % (_('[no release info]'), name) action = releases_menu.addAction(name) @@ -391,9 +391,6 @@ class BaseTreeView(QtGui.QTreeWidget): if obj.id == version["mbid"]: action.setChecked(True) self.connect(action, QtCore.SIGNAL("triggered(bool)"), switch_release_version) - if releases_menu.isEmpty(): - action = releases_menu.addAction(_('No other versions')) - action.setEnabled(False) if not obj.rgloaded: if obj.rgid: