From f06e30682bb32bdc397a212cfbcc3aa71bd94fad Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 19 May 2016 11:25:09 +0530 Subject: [PATCH 01/98] Track search dialog primitive UI --- picard/ui/searchdialog.py | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 picard/ui/searchdialog.py diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py new file mode 100644 index 000000000..a9fc8e231 --- /dev/null +++ b/picard/ui/searchdialog.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# +# 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 QtGui +from picard.ui import PicardDialog + +class SearchDialog(PicardDialog): + + def __init__(self, parent=None): + PicardDialog.__init__(self, parent) + self.selected_object = None + self.setupUi() + + def setupUi(self): + self.setObjectName(_("SearchDialog")) + self.verticalLayout = QtGui.QVBoxLayout(InfoDialog) + self.verticalLayout.setObjectName(_("verticalLayout")) + self.tracksTable = QtGui.QTableWidget(0, 5) + self.tracksTable.setHorizontalHeaderLabels([_("Name"), _("Length"), + _("Artist"), _("Release"), _("Type")]) + self.tracksTable.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.tracksTable.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self.verticalLayout.addWidget(self.tracksTable) + self.buttonBox = QtGui.QDialogButtonBox() + self.buttonBox.addButton(StandardButton(StandardButton.Ok), QtGui.QDialogButtonBox.AcceptRole) + self.buttonBox.addButton(StandardButton(StandardButton.Cancel), QtGui.QDialogButtonBox.RejectRole) + self.buttonBox.accepted(self.load_selection) + self.verticalLayout.addWidget(self.buttonBox) From b7fb30b3f5b1381b795791af2d66351d80ad0b2d Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 27 May 2016 14:32:25 +0530 Subject: [PATCH 02/98] Wrap code --- picard/ui/searchdialog.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index a9fc8e231..d477dee50 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -33,11 +33,17 @@ class SearchDialog(PicardDialog): self.tracksTable = QtGui.QTableWidget(0, 5) self.tracksTable.setHorizontalHeaderLabels([_("Name"), _("Length"), _("Artist"), _("Release"), _("Type")]) - self.tracksTable.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) - self.tracksTable.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self.tracksTable.setSelectionMode( + QtGui.QAbstractItemView.SingleSelection) + self.tracksTable.setSelectionBehavior( + QtGui.QAbstractItemView.SelectRows) self.verticalLayout.addWidget(self.tracksTable) self.buttonBox = QtGui.QDialogButtonBox() - self.buttonBox.addButton(StandardButton(StandardButton.Ok), QtGui.QDialogButtonBox.AcceptRole) - self.buttonBox.addButton(StandardButton(StandardButton.Cancel), QtGui.QDialogButtonBox.RejectRole) + self.buttonBox.addButton( + StandardButton(StandardButton.Ok), + QtGui.QDialogButtonBox.AcceptRole) + self.buttonBox.addButton( + StandardButton(StandardButton.Cancel), + QtGui.QDialogButtonBox.RejectRole) self.buttonBox.accepted(self.load_selection) self.verticalLayout.addWidget(self.buttonBox) From c4d6ea47969f644c579139261b5c204cc9831f39 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 27 May 2016 14:33:19 +0530 Subject: [PATCH 03/98] Make search dialog able to display all results To view all recordings, right click on Track object and select "Display more results". Results can't be loaded into Picard as for now. What's not working: * Search dialog is throwing exceptions sometimes, specifically AttributeError. * Release type is left for now. --- picard/ui/itemviews.py | 1 + picard/ui/mainwindow.py | 11 +++++++ picard/ui/searchdialog.py | 67 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 32f169e75..5c0432841 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -259,6 +259,7 @@ class BaseTreeView(QtGui.QTreeWidget): menu.addAction(self.window.open_folder_action) plugin_actions.extend(_file_actions) menu.addAction(self.window.browser_lookup_action) + menu.addAction(self.window.more_results_action) menu.addSeparator() if isinstance(obj, NonAlbumTrack): menu.addAction(self.window.refresh_action) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index a45595bf3..38168f958 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -34,6 +34,7 @@ from picard.ui.filebrowser import FileBrowser from picard.ui.tagsfromfilenames import TagsFromFileNamesDialog from picard.ui.options.dialog import OptionsDialog from picard.ui.infodialog import FileInfoDialog, AlbumInfoDialog, ClusterInfoDialog +from picard.ui.searchdialog import SearchDialog from picard.ui.infostatus import InfoStatus from picard.ui.passworddialog import PasswordDialog from picard.ui.logview import LogView, HistoryView @@ -381,6 +382,10 @@ class MainWindow(QtGui.QMainWindow): self.browser_lookup_action.setEnabled(False) self.browser_lookup_action.triggered.connect(self.browser_lookup) + self.more_results_action = QtGui.QAction(_(u"Display more results"), self) + self.more_results_action.setStatusTip(_(u"Display more results")) + self.more_results_action.triggered.connect(self.show_more_results) + self.show_file_browser_action = QtGui.QAction(_(u"File &Browser"), self) self.show_file_browser_action.setCheckable(True) if config.persist["view_file_browser"]: @@ -791,6 +796,12 @@ class MainWindow(QtGui.QMainWindow): QtGui.QMessageBox.Yes) return ret == QtGui.QMessageBox.Yes + def show_more_results(self): + if isinstance(self.selected_objects[0], Track): + track = self.selected_objects[0] + dialog = SearchDialog(track, self) + dialog.exec_() + def view_info(self): if isinstance(self.selected_objects[0], Album): album = self.selected_objects[0] diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index d477dee50..d96b5e131 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -17,18 +17,36 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from PyQt4 import QtGui +from operator import itemgetter +from functools import partial +from picard.file import File from picard.ui import PicardDialog +from picard.ui.util import StandardButton +from picard.util import format_time +from picard.mbxml import artist_credit_from_node class SearchDialog(PicardDialog): - def __init__(self, parent=None): + def __init__(self, obj, parent=None): + #obj can be a track/file object PicardDialog.__init__(self, parent) self.selected_object = None self.setupUi() + metadata = obj.metadata + self.tagger.xmlws.find_tracks(partial(self.show_tracks, obj), + track=metadata['title'], + artist=metadata['artist'], + release=metadata['tracknumber'], + tnum=metadata['totaltracks'], + tracks=metadata['totaltracks'], + qdur=str(metadata.length / 2000), + isrc=metadata['isrc'], + limit=25) def setupUi(self): self.setObjectName(_("SearchDialog")) - self.verticalLayout = QtGui.QVBoxLayout(InfoDialog) + self.setWindowTitle(_("Track Search Results")) + self.verticalLayout = QtGui.QVBoxLayout(self) self.verticalLayout.setObjectName(_("verticalLayout")) self.tracksTable = QtGui.QTableWidget(0, 5) self.tracksTable.setHorizontalHeaderLabels([_("Name"), _("Length"), @@ -40,10 +58,49 @@ class SearchDialog(PicardDialog): self.verticalLayout.addWidget(self.tracksTable) self.buttonBox = QtGui.QDialogButtonBox() self.buttonBox.addButton( - StandardButton(StandardButton.Ok), + StandardButton(StandardButton.OK), QtGui.QDialogButtonBox.AcceptRole) self.buttonBox.addButton( - StandardButton(StandardButton.Cancel), + StandardButton(StandardButton.CANCEL), QtGui.QDialogButtonBox.RejectRole) - self.buttonBox.accepted(self.load_selection) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) + + def load_selection(self): + pass + + def show_tracks(self, obj, document, http, error): + try: + tracks = document.metadata[0].recording_list[0].recording + except (AttributeError, IndexError): + tracks = None + + #tracks = sorted((obj.metadata.compare_to_track( + # track, File.comparison_weights) for track in tracks), + # reverse=True, key=itemgetter(0)) + + def insert_values_in_row(row, values): + self.tracksTable.insertRow(row) + item = QtGui.QTableWidgetItem + for i in range(self.tracksTable.columnCount()): + self.tracksTable.setItem(row, i, item(values[i])) + + for row, track in enumerate(tracks): + title = track.title[0].text + length = format_time(track.length[0].text) + artist = artist_credit_from_node(track.artist_credit[0])[0] + if "release_list" in track.children and \ + "release" in track.release_list[0].children: + releases = track.release_list[0].release + for release in releases: + release_title = release.title[0].text + if False and "release_group" in release.children: + release_type = release.release_group[0].type + insert_values_in_row(row, (title, length, artist, + release_title, release_type)) + else: + insert_values_in_row(row, (title, length, artist, + release_title, "")) + else: + insert_values_in_row(row, (title, length, artist, "", "")) From f584cf8282005a18dfc11c0f535c46528cab1977 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 28 May 2016 19:48:54 +0530 Subject: [PATCH 04/98] Avoid AttributeError exception --- picard/ui/searchdialog.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index d96b5e131..f0b28464b 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -88,19 +88,22 @@ class SearchDialog(PicardDialog): for row, track in enumerate(tracks): title = track.title[0].text - length = format_time(track.length[0].text) artist = artist_credit_from_node(track.artist_credit[0])[0] + + try: + length = format_time(track.length[0].text) + except AttributeError: + length = "" + if "release_list" in track.children and \ "release" in track.release_list[0].children: releases = track.release_list[0].release for release in releases: release_title = release.title[0].text - if False and "release_group" in release.children: - release_type = release.release_group[0].type - insert_values_in_row(row, (title, length, artist, - release_title, release_type)) - else: - insert_values_in_row(row, (title, length, artist, - release_title, "")) - else: - insert_values_in_row(row, (title, length, artist, "", "")) + release_type = "" + if "release_group" in release.children: + try: + release_type = release.release_group[0].type + except AttributeError: + pass + insert_values_in_row(row, (title, length, artist, release_title, release_type)) From 7b4c71d2122805ce58cacac7cee4bf6d5be5eb03 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 28 May 2016 20:53:37 +0530 Subject: [PATCH 05/98] Make cells non editable in tracks table --- picard/ui/searchdialog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index f0b28464b..e40e6e668 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -55,6 +55,8 @@ class SearchDialog(PicardDialog): QtGui.QAbstractItemView.SingleSelection) self.tracksTable.setSelectionBehavior( QtGui.QAbstractItemView.SelectRows) + self.tracksTable.setEditTriggers( + QtGui.QAbstractItemView.NoEditTriggers) self.verticalLayout.addWidget(self.tracksTable) self.buttonBox = QtGui.QDialogButtonBox() self.buttonBox.addButton( @@ -68,7 +70,7 @@ class SearchDialog(PicardDialog): self.verticalLayout.addWidget(self.buttonBox) def load_selection(self): - pass + self def show_tracks(self, obj, document, http, error): try: From 3ce4bc0c3c2d35a1e291d59903a051ad9498f295 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 30 May 2016 08:58:52 +0530 Subject: [PATCH 06/98] Sort search results * Search results are sorted using similarity factor returned by `compare_to_track` method in `Metadata` class. * Only use track from tuple returned by `compare_to_track`. Reason being `compare_to_track` selects only best matched release from the release_list associated with a track. For showing more results each of those release are needed. These are extracted from the `tracks` later. --- picard/ui/searchdialog.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index e40e6e668..f39f32af0 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -78,9 +78,13 @@ class SearchDialog(PicardDialog): except (AttributeError, IndexError): tracks = None - #tracks = sorted((obj.metadata.compare_to_track( - # track, File.comparison_weights) for track in tracks), - # reverse=True, key=itemgetter(0)) + sorted_data = sorted((obj.metadata.compare_to_track( + track, File.comparison_weights) for track in tracks), + reverse=True, key=itemgetter(0)) + #Value returned by `compare_to_track` is of type tuple + #(similarity, release_group, release, track) + + tracks = [item[3] for item in sorted_data] def insert_values_in_row(row, values): self.tracksTable.insertRow(row) @@ -97,15 +101,11 @@ class SearchDialog(PicardDialog): except AttributeError: length = "" - if "release_list" in track.children and \ - "release" in track.release_list[0].children: - releases = track.release_list[0].release - for release in releases: - release_title = release.title[0].text + releases = track.release_list[0].release + for release in releases: + release_title = release.title[0].text + try: + release_type = release.release_group[0].type + except AttributeError: release_type = "" - if "release_group" in release.children: - try: - release_type = release.release_group[0].type - except AttributeError: - pass - insert_values_in_row(row, (title, length, artist, release_title, release_type)) + insert_values_in_row(row, (title, length, artist, release_title, release_type)) From 9d1793de5b50763423aedde9e313ab08da11e7f9 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 30 May 2016 14:51:50 +0530 Subject: [PATCH 07/98] Use existing metadata to perform search The `Track` object would have new metadata. The lookup results would be different then. Rather use `orig_metadata` of linked file. --- picard/ui/itemviews.py | 4 +++- picard/ui/mainwindow.py | 9 +++++---- picard/ui/searchdialog.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 5c0432841..c2a226a70 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -257,9 +257,9 @@ class BaseTreeView(QtGui.QTreeWidget): if obj.num_linked_files == 1: menu.addAction(self.window.play_file_action) menu.addAction(self.window.open_folder_action) + menu.addAction(self.window.more_results_action) plugin_actions.extend(_file_actions) menu.addAction(self.window.browser_lookup_action) - menu.addAction(self.window.more_results_action) menu.addSeparator() if isinstance(obj, NonAlbumTrack): menu.addAction(self.window.refresh_action) @@ -286,6 +286,8 @@ class BaseTreeView(QtGui.QTreeWidget): menu.addSeparator() menu.addAction(self.window.autotag_action) menu.addAction(self.window.analyze_action) + if isinstance(obj.parent, Track): + menu.addAction(self.window.more_results_action) plugin_actions = list(_file_actions) elif isinstance(obj, Album): if can_view_info: diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 38168f958..a812cc932 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -797,10 +797,11 @@ class MainWindow(QtGui.QMainWindow): return ret == QtGui.QMessageBox.Yes def show_more_results(self): - if isinstance(self.selected_objects[0], Track): - track = self.selected_objects[0] - dialog = SearchDialog(track, self) - dialog.exec_() + obj = self.selected_objects[0] + if isinstance(obj, Track): + obj = obj.linked_files[0] + dialog = SearchDialog(obj, self) + dialog.exec_() def view_info(self): if isinstance(self.selected_objects[0], Album): diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index f39f32af0..8c7650b7a 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -32,7 +32,7 @@ class SearchDialog(PicardDialog): PicardDialog.__init__(self, parent) self.selected_object = None self.setupUi() - metadata = obj.metadata + metadata = obj.orig_metadata self.tagger.xmlws.find_tracks(partial(self.show_tracks, obj), track=metadata['title'], artist=metadata['artist'], From 9926aef23cb0775bf4eee07d846a91d1c8c71719 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 30 May 2016 21:59:04 +0530 Subject: [PATCH 08/98] Parse track search results * Separate method to parse search results, to reduce complexity a bit. * Two more columns, Date and Country added in results table, to contrast difference between two tracks. * Secondary type included in Type column. --- picard/ui/searchdialog.py | 89 +++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 32 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 8c7650b7a..3f0496102 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -31,6 +31,7 @@ class SearchDialog(PicardDialog): #obj can be a track/file object PicardDialog.__init__(self, parent) self.selected_object = None + self.search_results = [] self.setupUi() metadata = obj.orig_metadata self.tagger.xmlws.find_tracks(partial(self.show_tracks, obj), @@ -48,9 +49,9 @@ class SearchDialog(PicardDialog): self.setWindowTitle(_("Track Search Results")) self.verticalLayout = QtGui.QVBoxLayout(self) self.verticalLayout.setObjectName(_("verticalLayout")) - self.tracksTable = QtGui.QTableWidget(0, 5) + self.tracksTable = QtGui.QTableWidget(0, 7) self.tracksTable.setHorizontalHeaderLabels([_("Name"), _("Length"), - _("Artist"), _("Release"), _("Type")]) + _("Artist"), _("Release"), _("Date"), _("Country"), _("Type")]) self.tracksTable.setSelectionMode( QtGui.QAbstractItemView.SingleSelection) self.tracksTable.setSelectionBehavior( @@ -72,40 +73,64 @@ class SearchDialog(PicardDialog): def load_selection(self): self + def parse_recording_node(self, track): + result = [] + rec_id = track.id + rec_title = track.title[0].text + artist = artist_credit_from_node(track.artist_credit[0])[0] + try: + length = format_time(track.length[0].text) + except AttributeError: + length = "" + if "release_list" in track.children and "release" in \ + track.release_list[0].children: + releases = track.release_list[0].release + for release in releases: + rel_id = release.id + rel_title = release.title[0].text + if "date" in release.children: + date = release.date[0].text + else: + date = None + if "country" in release.children: + country = release.country[0].text + else: + country = "" + rg = release.release_group[0] + rg_id = rg.id + types_list = [] + if "primary_type" in rg.children: + types_list.append(rg.primary_type[0].text) + if "secondary_type_list" in rg.children: + for sec in rg.secondary_type_list: + types_list.append(sec.secondary_type[0].text) + types = "+".join(types_list) + + result.append((rec_id, rel_id, rg_id, rec_title, artist, + length, rel_title, date, country, types)) + else: + result.append((rec_id, "", "", rec_title, artist, length, "", "", + "", "")) + self.search_results.extend(result) + return result + def show_tracks(self, obj, document, http, error): try: tracks = document.metadata[0].recording_list[0].recording except (AttributeError, IndexError): tracks = None - sorted_data = sorted((obj.metadata.compare_to_track( - track, File.comparison_weights) for track in tracks), - reverse=True, key=itemgetter(0)) - #Value returned by `compare_to_track` is of type tuple - #(similarity, release_group, release, track) - - tracks = [item[3] for item in sorted_data] - - def insert_values_in_row(row, values): - self.tracksTable.insertRow(row) - item = QtGui.QTableWidgetItem - for i in range(self.tracksTable.columnCount()): - self.tracksTable.setItem(row, i, item(values[i])) - for row, track in enumerate(tracks): - title = track.title[0].text - artist = artist_credit_from_node(track.artist_credit[0])[0] - - try: - length = format_time(track.length[0].text) - except AttributeError: - length = "" - - releases = track.release_list[0].release - for release in releases: - release_title = release.title[0].text - try: - release_type = release.release_group[0].type - except AttributeError: - release_type = "" - insert_values_in_row(row, (title, length, artist, release_title, release_type)) + result = self.parse_recording_node(track) + for row2, item in enumerate(result): + cur_row = row + row2 + self.tracksTable.insertRow(cur_row) + title, artist, length, release, date, country, type = item[3:] + table_item = QtGui.QTableWidgetItem + self.tracksTable.setItem(cur_row, 0, table_item(title)) + self.tracksTable.setItem(cur_row, 1, table_item(length)) + self.tracksTable.setItem(cur_row, 2, table_item(artist)) + self.tracksTable.setItem(cur_row, 3, table_item(release)) + self.tracksTable.setItem(cur_row, 4, table_item(date)) + self.tracksTable.setItem(cur_row, 5, table_item(country)) + self.tracksTable.setItem(cur_row, 6, table_item(type)) From ab9df85f54fec3994b67cbb773f934baa94659c1 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 30 May 2016 22:12:42 +0530 Subject: [PATCH 09/98] obj can only be a File object. Obsolete comment. --- picard/ui/searchdialog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 3f0496102..0ffd99c72 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -28,7 +28,6 @@ from picard.mbxml import artist_credit_from_node class SearchDialog(PicardDialog): def __init__(self, obj, parent=None): - #obj can be a track/file object PicardDialog.__init__(self, parent) self.selected_object = None self.search_results = [] From 1a23f07b7211892d2fc78dac78584984c3bb59d6 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 30 May 2016 22:34:58 +0530 Subject: [PATCH 10/98] Incorrect logic for counting row --- picard/ui/searchdialog.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 0ffd99c72..5f620ff93 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -119,10 +119,10 @@ class SearchDialog(PicardDialog): except (AttributeError, IndexError): tracks = None - for row, track in enumerate(tracks): + cur_row = 0 + for track in tracks: result = self.parse_recording_node(track) - for row2, item in enumerate(result): - cur_row = row + row2 + for item in result: self.tracksTable.insertRow(cur_row) title, artist, length, release, date, country, type = item[3:] table_item = QtGui.QTableWidgetItem @@ -133,3 +133,4 @@ class SearchDialog(PicardDialog): self.tracksTable.setItem(cur_row, 4, table_item(date)) self.tracksTable.setItem(cur_row, 5, table_item(country)) self.tracksTable.setItem(cur_row, 6, table_item(type)) + cur_row += 1 From 05e5d8cfd885e6c6750888d1c4d74ecbc74292ad Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 1 Jun 2016 07:08:42 +0530 Subject: [PATCH 11/98] Load album from dialog Other than that: * self.selected_object isn't required. Selected track can be looked up in self.search_results using index from selected row. * `tracks` should be an empty list rather than None. To avoid type mismatch/unable to iterate. --- picard/ui/searchdialog.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 5f620ff93..eaeac1a16 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -29,7 +29,7 @@ class SearchDialog(PicardDialog): def __init__(self, obj, parent=None): PicardDialog.__init__(self, parent) - self.selected_object = None + self.obj = obj self.search_results = [] self.setupUi() metadata = obj.orig_metadata @@ -65,12 +65,19 @@ class SearchDialog(PicardDialog): self.buttonBox.addButton( StandardButton(StandardButton.CANCEL), QtGui.QDialogButtonBox.RejectRole) - self.buttonBox.accepted.connect(self.accept) + self.buttonBox.accepted.connect(self.load_selection) self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) def load_selection(self): - self + sel_row = self.tracksTable.selectionModel().selectedRows()[0].row() + track_id, release_id, rg_id = self.search_results[sel_row][:3] + if release_id: + album = self.obj.parent.album + self.tagger.get_release_group_by_id(rg_id).loaded_albums.add(release_id) + self.tagger.move_file_to_track(self.obj, release_id, track_id) + self.tagger.remove_album(album) + self.accept() def parse_recording_node(self, track): result = [] @@ -117,7 +124,7 @@ class SearchDialog(PicardDialog): try: tracks = document.metadata[0].recording_list[0].recording except (AttributeError, IndexError): - tracks = None + tracks = [] cur_row = 0 for track in tracks: From 65a4d3bcbb80de4e118db76a11185860fbef7dfa Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 1 Jun 2016 17:44:47 +0530 Subject: [PATCH 12/98] Load selection on double click --- picard/ui/searchdialog.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index eaeac1a16..e4c9299f0 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -57,6 +57,7 @@ class SearchDialog(PicardDialog): QtGui.QAbstractItemView.SelectRows) self.tracksTable.setEditTriggers( QtGui.QAbstractItemView.NoEditTriggers) + self.tracksTable.cellDoubleClicked.connect(self.load_selection) self.verticalLayout.addWidget(self.tracksTable) self.buttonBox = QtGui.QDialogButtonBox() self.buttonBox.addButton( @@ -69,8 +70,11 @@ class SearchDialog(PicardDialog): self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) - def load_selection(self): - sel_row = self.tracksTable.selectionModel().selectedRows()[0].row() + def load_selection(self, row=None): + if row: + sel_row = row + else: + sel_row = self.tracksTable.selectionModel().selectedRows()[0].row() track_id, release_id, rg_id = self.search_results[sel_row][:3] if release_id: album = self.obj.parent.album From 3eedce15b6ac767b93dd477aeb21f00c36335909 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 1 Jun 2016 23:28:27 +0530 Subject: [PATCH 13/98] Return release list after comparison Rather than returning the best matched release for a track, return a reverse sorted list containing all releases of a track in order. This method can then be re used with the search dialog. --- picard/file.py | 2 +- picard/metadata.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/picard/file.py b/picard/file.py index 74cf91dab..608c27405 100644 --- a/picard/file.py +++ b/picard/file.py @@ -517,7 +517,7 @@ class File(QtCore.QObject, Item): # multiple matches -- calculate similarities to each of them match = sorted((self.metadata.compare_to_track( - track, self.comparison_weights) for track in tracks), + track, self.comparison_weights)[0] for track in tracks), reverse=True, key=itemgetter(0))[0] if lookuptype != 'acoustid': diff --git a/picard/metadata.py b/picard/metadata.py index 18464de98..74ca8e779 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -18,6 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. from PyQt4.QtCore import QObject +from operator import itemgetter from picard import config, log from picard.plugin import PluginFunctions, PluginPriority from picard.similarity import similarity2 @@ -201,13 +202,13 @@ class Metadata(dict): sim = linear_combination_of_weights(parts) return (sim, None, None, track) - result = (-1,) + result = [] for release in releases: release_parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts + release_parts) - if sim > result[0]: - rg = release.release_group[0] if "release_group" in release.children else None - result = (sim, rg, release, track) + rg = release.release_group[0] if "release_group" in release.children else None + result.append((sim, rg, release, track)) + result.sort(key=itemgetter(0), reverse=True) return result From aa55c3fe480cc689f62b8a0e0dc092f02f0d55dc Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 4 Jun 2016 11:16:07 +0530 Subject: [PATCH 14/98] Display sorted results * Sort the search results according to similarity measured by `metadata.compare_to_tracks`. * Display results above the threshold only. * Return list in case there is no release (in metadata.py). --- picard/metadata.py | 2 +- picard/ui/searchdialog.py | 96 ++++++++++++++++++++------------------- 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/picard/metadata.py b/picard/metadata.py index 74ca8e779..fc376f566 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -200,7 +200,7 @@ class Metadata(dict): if not releases: sim = linear_combination_of_weights(parts) - return (sim, None, None, track) + return [(sim, None, None, track)] result = [] for release in releases: diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index e4c9299f0..b4b4574f6 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -19,6 +19,7 @@ from PyQt4 import QtGui from operator import itemgetter from functools import partial +from picard import config from picard.file import File from picard.ui import PicardDialog from picard.ui.util import StandardButton @@ -78,13 +79,14 @@ class SearchDialog(PicardDialog): track_id, release_id, rg_id = self.search_results[sel_row][:3] if release_id: album = self.obj.parent.album - self.tagger.get_release_group_by_id(rg_id).loaded_albums.add(release_id) + self.tagger.get_release_group_by_id(rg_id).loaded_albums.add( + release_id) self.tagger.move_file_to_track(self.obj, release_id, track_id) self.tagger.remove_album(album) self.accept() - def parse_recording_node(self, track): - result = [] + def parse_match(self, match): + rg, release, track = match[1:] rec_id = track.id rec_title = track.title[0].text artist = artist_credit_from_node(track.artist_credit[0])[0] @@ -92,56 +94,58 @@ class SearchDialog(PicardDialog): length = format_time(track.length[0].text) except AttributeError: length = "" - if "release_list" in track.children and "release" in \ - track.release_list[0].children: - releases = track.release_list[0].release - for release in releases: - rel_id = release.id - rel_title = release.title[0].text - if "date" in release.children: - date = release.date[0].text - else: - date = None - if "country" in release.children: - country = release.country[0].text - else: - country = "" - rg = release.release_group[0] - rg_id = rg.id - types_list = [] - if "primary_type" in rg.children: - types_list.append(rg.primary_type[0].text) - if "secondary_type_list" in rg.children: - for sec in rg.secondary_type_list: - types_list.append(sec.secondary_type[0].text) - types = "+".join(types_list) + if release: + rel_id = release.id + rel_title = release.title[0].text + if "date" in release.children: + date = release.date[0].text + else: + date = None + if "country" in release.children: + country = release.country[0].text + else: + country = "" + rg_id = rg.id + types_list = [] + if "primary_type" in rg.children: + types_list.append(rg.primary_type[0].text) + if "secondary_type_list" in rg.children: + for sec in rg.secondary_type_list: + types_list.append(sec.secondary_type[0].text) + types = "+".join(types_list) - result.append((rec_id, rel_id, rg_id, rec_title, artist, - length, rel_title, date, country, types)) + result = (rec_id, rel_id, rg_id, rec_title, artist, length, + rel_title, date, country, types) else: - result.append((rec_id, "", "", rec_title, artist, length, "", "", - "", "")) - self.search_results.extend(result) + result = (rec_id, "", "", rec_title, artist, length, "", "", "", + "") + self.search_results.append(result) return result def show_tracks(self, obj, document, http, error): try: tracks = document.metadata[0].recording_list[0].recording except (AttributeError, IndexError): - tracks = [] + # No results to show + # To be done: Notify user about that, or just close the dialog + return - cur_row = 0 + tmp = [] for track in tracks: - result = self.parse_recording_node(track) - for item in result: - self.tracksTable.insertRow(cur_row) - title, artist, length, release, date, country, type = item[3:] - table_item = QtGui.QTableWidgetItem - self.tracksTable.setItem(cur_row, 0, table_item(title)) - self.tracksTable.setItem(cur_row, 1, table_item(length)) - self.tracksTable.setItem(cur_row, 2, table_item(artist)) - self.tracksTable.setItem(cur_row, 3, table_item(release)) - self.tracksTable.setItem(cur_row, 4, table_item(date)) - self.tracksTable.setItem(cur_row, 5, table_item(country)) - self.tracksTable.setItem(cur_row, 6, table_item(type)) - cur_row += 1 + tmp.extend(self.obj.orig_metadata.compare_to_track(track, + File.comparison_weights)) + + sorted_matches = [i for i in sorted(tmp, key=itemgetter(0), reverse=True) + if i[0] > config.setting['file_lookup_threshold']] + for row, match in enumerate(sorted_matches): + result = self.parse_match(match) + title, artist, length, release, date, country, type = result[3:] + table_item = QtGui.QTableWidgetItem + self.tracksTable.insertRow(row) + self.tracksTable.setItem(row, 0, table_item(title)) + self.tracksTable.setItem(row, 1, table_item(length)) + self.tracksTable.setItem(row, 2, table_item(artist)) + self.tracksTable.setItem(row, 3, table_item(release)) + self.tracksTable.setItem(row, 4, table_item(date)) + self.tracksTable.setItem(row, 5, table_item(country)) + self.tracksTable.setItem(row, 6, table_item(type)) From 55eebfbb7e97f7c959c6b70a13aa033500668a05 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 4 Jun 2016 11:54:17 +0530 Subject: [PATCH 15/98] Remove album on certain condition Remove album from picard if the track for which more results lookup was performed, was the only track in it. Otherwise load the new album without removing existing one. --- picard/ui/searchdialog.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index b4b4574f6..bb9e3a741 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -82,7 +82,11 @@ class SearchDialog(PicardDialog): self.tagger.get_release_group_by_id(rg_id).loaded_albums.add( release_id) self.tagger.move_file_to_track(self.obj, release_id, track_id) - self.tagger.remove_album(album) + if album._files == 0: + # Remove album if the selected file was the only one in album + # Compared to 0 because file has already moved to another album + # by move_file_to_track + self.tagger.remove_album(album) self.accept() def parse_match(self, match): From 17688031659b7dab4087849f71ec9594507a54ad Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 6 Jun 2016 17:25:23 +0530 Subject: [PATCH 16/98] Stretch rows to fit size --- picard/ui/searchdialog.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index bb9e3a741..eb68ef948 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -52,13 +52,22 @@ class SearchDialog(PicardDialog): self.tracksTable = QtGui.QTableWidget(0, 7) self.tracksTable.setHorizontalHeaderLabels([_("Name"), _("Length"), _("Artist"), _("Release"), _("Date"), _("Country"), _("Type")]) + self.tracksTable.setSelectionMode( QtGui.QAbstractItemView.SingleSelection) self.tracksTable.setSelectionBehavior( QtGui.QAbstractItemView.SelectRows) self.tracksTable.setEditTriggers( QtGui.QAbstractItemView.NoEditTriggers) + + self.tracksTable.horizontalHeader().setResizeMode( + QtGui.QHeaderView.Interactive | QtGui.QHeaderView.Stretch) + self.tracksTable.horizontalHeader().setStretchLastSection(True) + + self.tracksTable.resize(740, 360) + self.tracksTable.cellDoubleClicked.connect(self.load_selection) + self.verticalLayout.addWidget(self.tracksTable) self.buttonBox = QtGui.QDialogButtonBox() self.buttonBox.addButton( From 5170334a715c9c9e3b9db3d60965403d5a947d65 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 8 Jun 2016 16:39:33 +0530 Subject: [PATCH 17/98] Restore dialog state * Restore the window size of dialog. * Restore the column width, if modified by user. * Reflect changes on each call to dialog --- picard/ui/searchdialog.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index eb68ef948..80379d6cc 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -16,7 +16,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -from PyQt4 import QtGui +from PyQt4 import QtGui, QtCore from operator import itemgetter from functools import partial from picard import config @@ -28,11 +28,17 @@ from picard.mbxml import artist_credit_from_node class SearchDialog(PicardDialog): + options = [ + config.Option("persist", "searchdialog_window_size", QtCore.QSize(720, 360)), + config.Option("persist", "searchdialog_header_state", QtCore.QByteArray()) + ] + def __init__(self, obj, parent=None): PicardDialog.__init__(self, parent) self.obj = obj self.search_results = [] self.setupUi() + self.restore_state() metadata = obj.orig_metadata self.tagger.xmlws.find_tracks(partial(self.show_tracks, obj), track=metadata['title'], @@ -60,9 +66,11 @@ class SearchDialog(PicardDialog): self.tracksTable.setEditTriggers( QtGui.QAbstractItemView.NoEditTriggers) - self.tracksTable.horizontalHeader().setResizeMode( - QtGui.QHeaderView.Interactive | QtGui.QHeaderView.Stretch) self.tracksTable.horizontalHeader().setStretchLastSection(True) + self.tracksTable.horizontalHeader().setResizeMode( + QtGui.QHeaderView.Stretch) + self.tracksTable.horizontalHeader().setResizeMode( + QtGui.QHeaderView.Interactive) self.tracksTable.resize(740, 360) @@ -162,3 +170,18 @@ class SearchDialog(PicardDialog): self.tracksTable.setItem(row, 4, table_item(date)) self.tracksTable.setItem(row, 5, table_item(country)) self.tracksTable.setItem(row, 6, table_item(type)) + + def restore_state(self): + header = self.tracksTable.horizontalHeader() + state = config.persist["searchdialog_header_state"] + if state: + header.restoreState(state) + size = config.persist["searchdialog_window_size"] + if size: + self.resize(size) + header.setResizeMode(QtGui.QHeaderView.Interactive) + + def save_state(self): + header = self.tracksTable.horizontalHeader() + config.persist["searchdialog_header_state"] = header.saveState() + config.persist["searchdialog_window_size"] = self.size() From d555e082c26ae257e74d43831ef6a5305e831885 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 8 Jun 2016 16:56:25 +0530 Subject: [PATCH 18/98] Use separate methods for handling selection method --- picard/ui/searchdialog.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 80379d6cc..25b7fd3ca 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -74,7 +74,7 @@ class SearchDialog(PicardDialog): self.tracksTable.resize(740, 360) - self.tracksTable.cellDoubleClicked.connect(self.load_selection) + self.tracksTable.cellDoubleClicked.connect(self.track_double_clicked) self.verticalLayout.addWidget(self.tracksTable) self.buttonBox = QtGui.QDialogButtonBox() @@ -84,16 +84,12 @@ class SearchDialog(PicardDialog): self.buttonBox.addButton( StandardButton(StandardButton.CANCEL), QtGui.QDialogButtonBox.RejectRole) - self.buttonBox.accepted.connect(self.load_selection) + self.buttonBox.accepted.connect(self.track_selected) self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) def load_selection(self, row=None): - if row: - sel_row = row - else: - sel_row = self.tracksTable.selectionModel().selectedRows()[0].row() - track_id, release_id, rg_id = self.search_results[sel_row][:3] + track_id, release_id, rg_id = self.search_results[row][:3] if release_id: album = self.obj.parent.album self.tagger.get_release_group_by_id(rg_id).loaded_albums.add( @@ -104,7 +100,26 @@ class SearchDialog(PicardDialog): # Compared to 0 because file has already moved to another album # by move_file_to_track self.tagger.remove_album(album) - self.accept() + self.save_state() + self.closeEvent() + + def track_double_clicked(self, row): + self.load_selection(row) + + def track_selected(self): + sel_rows = self.tracksTable.selectionModel().selectedRows() + if sel_rows: + sel_row = sel_rows[0].row() + self.load_selection(sel_row) + else: + self.closeEvent() + + def closeEvent(self, event=None): + self.save_state() + if event: + event.accept() + else: + self.accept() def parse_match(self, match): rg, release, track = match[1:] From 295291f9e4b6da562234cfbeb96841244231615d Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 9 Jun 2016 23:45:29 +0530 Subject: [PATCH 19/98] Split restoring dialog state in different methods Search results table isn't setup immediately after dialog is displayed. Thus it's header state needs to be restored later, and hence a separate method for that. --- picard/ui/searchdialog.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 25b7fd3ca..13875eaa5 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -38,7 +38,8 @@ class SearchDialog(PicardDialog): self.obj = obj self.search_results = [] self.setupUi() - self.restore_state() + self.restore_window_state() + metadata = obj.orig_metadata self.tagger.xmlws.find_tracks(partial(self.show_tracks, obj), track=metadata['title'], @@ -186,16 +187,19 @@ class SearchDialog(PicardDialog): self.tracksTable.setItem(row, 5, table_item(country)) self.tracksTable.setItem(row, 6, table_item(type)) - def restore_state(self): + def restore_window_state(self): + size = config.persist["searchdialog_window_size"] + if size: + self.resize(size) + + def restore_table_header_state(self): header = self.tracksTable.horizontalHeader() state = config.persist["searchdialog_header_state"] if state: header.restoreState(state) - size = config.persist["searchdialog_window_size"] - if size: - self.resize(size) header.setResizeMode(QtGui.QHeaderView.Interactive) + def save_state(self): header = self.tracksTable.horizontalHeader() config.persist["searchdialog_header_state"] = header.saveState() From 2d5cfddc786068b671ce512ecb1e79a49e918918 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 9 Jun 2016 23:59:09 +0530 Subject: [PATCH 20/98] Move results table ui properties into separate class --- picard/ui/searchdialog.py | 58 +++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 13875eaa5..3a512120f 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -26,6 +26,28 @@ from picard.ui.util import StandardButton from picard.util import format_time from picard.mbxml import artist_credit_from_node + +class TracksTable(QtGui.QTableWidget): + + def __init__(self, parent=None): + QtGui.QTableWidget.__init__(self, 0, 7) + self.setHorizontalHeaderLabels([_("Name"), _("Length"), + _("Artist"), _("Release"), _("Date"), _("Country"), _("Type")]) + + self.setSelectionMode( + QtGui.QAbstractItemView.SingleSelection) + self.setSelectionBehavior( + QtGui.QAbstractItemView.SelectRows) + self.setEditTriggers( + QtGui.QAbstractItemView.NoEditTriggers) + + self.horizontalHeader().setStretchLastSection(True) + self.horizontalHeader().setResizeMode( + QtGui.QHeaderView.Stretch) + self.horizontalHeader().setResizeMode( + QtGui.QHeaderView.Interactive) + + class SearchDialog(PicardDialog): options = [ @@ -35,6 +57,8 @@ class SearchDialog(PicardDialog): def __init__(self, obj, parent=None): PicardDialog.__init__(self, parent) + self.setObjectName(_("SearchDialog")) + self.setWindowTitle(_("Track Search Results")) self.obj = obj self.search_results = [] self.setupUi() @@ -52,33 +76,10 @@ class SearchDialog(PicardDialog): limit=25) def setupUi(self): - self.setObjectName(_("SearchDialog")) - self.setWindowTitle(_("Track Search Results")) self.verticalLayout = QtGui.QVBoxLayout(self) self.verticalLayout.setObjectName(_("verticalLayout")) - self.tracksTable = QtGui.QTableWidget(0, 7) - self.tracksTable.setHorizontalHeaderLabels([_("Name"), _("Length"), - _("Artist"), _("Release"), _("Date"), _("Country"), _("Type")]) - self.tracksTable.setSelectionMode( - QtGui.QAbstractItemView.SingleSelection) - self.tracksTable.setSelectionBehavior( - QtGui.QAbstractItemView.SelectRows) - self.tracksTable.setEditTriggers( - QtGui.QAbstractItemView.NoEditTriggers) - - self.tracksTable.horizontalHeader().setStretchLastSection(True) - self.tracksTable.horizontalHeader().setResizeMode( - QtGui.QHeaderView.Stretch) - self.tracksTable.horizontalHeader().setResizeMode( - QtGui.QHeaderView.Interactive) - - self.tracksTable.resize(740, 360) - - self.tracksTable.cellDoubleClicked.connect(self.track_double_clicked) - - self.verticalLayout.addWidget(self.tracksTable) - self.buttonBox = QtGui.QDialogButtonBox() + self.buttonBox = QtGui.QDialogButtonBox(self) self.buttonBox.addButton( StandardButton(StandardButton.OK), QtGui.QDialogButtonBox.AcceptRole) @@ -89,6 +90,14 @@ class SearchDialog(PicardDialog): self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) + + def show_table(self): + self.tracksTable = TracksTable() + self.tracksTable.cellDoubleClicked.connect(self.track_double_clicked) + self.verticalLayout.removeWidget(self.label) + self.verticalLayout.insertWidget(0, self.tracksTable) + self.restore_table_header_state() + def load_selection(self, row=None): track_id, release_id, rg_id = self.search_results[row][:3] if release_id: @@ -160,6 +169,7 @@ class SearchDialog(PicardDialog): return result def show_tracks(self, obj, document, http, error): + self.show_table() try: tracks = document.metadata[0].recording_list[0].recording except (AttributeError, IndexError): From 53f84c1ed6ef61edaccc28b84fb87455bdf386b2 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 10 Jun 2016 00:01:47 +0530 Subject: [PATCH 21/98] Rename method that starts metadata download process --- picard/ui/searchdialog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 3a512120f..a50f14171 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -62,8 +62,10 @@ class SearchDialog(PicardDialog): self.obj = obj self.search_results = [] self.setupUi() + self.load_similar_tracks(self.obj) self.restore_window_state() + def load_similar_tracks(self, obj): metadata = obj.orig_metadata self.tagger.xmlws.find_tracks(partial(self.show_tracks, obj), track=metadata['title'], From 57f194fe651a961dc68889db95688fe801d71cc7 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 10 Jun 2016 00:02:11 +0530 Subject: [PATCH 22/98] Show feedback to user during loading --- picard/ui/searchdialog.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index a50f14171..defaaee9f 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -67,6 +67,7 @@ class SearchDialog(PicardDialog): def load_similar_tracks(self, obj): metadata = obj.orig_metadata + self.show_progress() self.tagger.xmlws.find_tracks(partial(self.show_tracks, obj), track=metadata['title'], artist=metadata['artist'], @@ -92,6 +93,10 @@ class SearchDialog(PicardDialog): self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) + def show_progress(self): + self.label = QtGui.QLabel('Loading....') + self.label.setAlignment(QtCore.Qt.AlignCenter) + self.verticalLayout.insertWidget(0, self.label) def show_table(self): self.tracksTable = TracksTable() From 6e2bb3a8d0c63b51117a34569c741736ad984f3f Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 13 Jun 2016 16:45:38 +0530 Subject: [PATCH 23/98] Add option for choice of lookup If checked, use the search dialog to display results. Otherwise perform regular browser lookup. --- picard/ui/options/interface.py | 3 +++ picard/ui/ui_options_interface.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/picard/ui/options/interface.py b/picard/ui/options/interface.py index c603cb746..65dd7c723 100644 --- a/picard/ui/options/interface.py +++ b/picard/ui/options/interface.py @@ -40,6 +40,7 @@ class InterfaceOptionsPage(OptionsPage): options = [ config.BoolOption("setting", "toolbar_show_labels", True), config.BoolOption("setting", "toolbar_multiselect", False), + config.BoolOption("setting", "builtin_search", False), config.BoolOption("setting", "use_adv_search_syntax", False), config.BoolOption("setting", "quit_confirmation", True), config.TextOption("setting", "ui_language", u""), @@ -77,6 +78,7 @@ class InterfaceOptionsPage(OptionsPage): def load(self): self.ui.toolbar_show_labels.setChecked(config.setting["toolbar_show_labels"]) self.ui.toolbar_multiselect.setChecked(config.setting["toolbar_multiselect"]) + self.ui.builtin_search.setChecked(config.setting["builtin_search"]) self.ui.use_adv_search_syntax.setChecked(config.setting["use_adv_search_syntax"]) self.ui.quit_confirmation.setChecked(config.setting["quit_confirmation"]) current_ui_language = config.setting["ui_language"] @@ -87,6 +89,7 @@ class InterfaceOptionsPage(OptionsPage): def save(self): config.setting["toolbar_show_labels"] = self.ui.toolbar_show_labels.isChecked() config.setting["toolbar_multiselect"] = self.ui.toolbar_multiselect.isChecked() + config.setting["builtin_search"] = self.ui.builtin_search.isChecked() config.setting["use_adv_search_syntax"] = self.ui.use_adv_search_syntax.isChecked() config.setting["quit_confirmation"] = self.ui.quit_confirmation.isChecked() self.tagger.window.update_toolbar_style() diff --git a/picard/ui/ui_options_interface.py b/picard/ui/ui_options_interface.py index 9f6bb3a1f..3e3d4f1c3 100644 --- a/picard/ui/ui_options_interface.py +++ b/picard/ui/ui_options_interface.py @@ -26,6 +26,9 @@ class Ui_InterfaceOptionsPage(object): self.toolbar_multiselect = QtGui.QCheckBox(self.groupBox_2) self.toolbar_multiselect.setObjectName(_fromUtf8("toolbar_multiselect")) self.vboxlayout1.addWidget(self.toolbar_multiselect) + self.builtin_search = QtGui.QCheckBox(self.groupBox_2) + self.builtin_search.setObjectName("builtin_search") + self.vboxlayout1.addWidget(self.builtin_search) self.use_adv_search_syntax = QtGui.QCheckBox(self.groupBox_2) self.use_adv_search_syntax.setObjectName(_fromUtf8("use_adv_search_syntax")) self.vboxlayout1.addWidget(self.use_adv_search_syntax) @@ -69,6 +72,7 @@ class Ui_InterfaceOptionsPage(object): self.groupBox_2.setTitle(_("Miscellaneous")) self.toolbar_show_labels.setText(_("Show text labels under icons")) self.toolbar_multiselect.setText(_("Allow selection of multiple directories")) + self.builtin_search.setText(_("Use builtin search rather than looking in browser")) self.use_adv_search_syntax.setText(_("Use advanced query syntax")) self.quit_confirmation.setText(_("Show a quit confirmation dialog for unsaved changes")) self.starting_directory.setText(_("Begin browsing in the following directory:")) From c2f2342ffb069645d6720583e418acd8460f44ce Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 14 Jun 2016 14:44:51 +0530 Subject: [PATCH 24/98] Revert metadata.py & file.py to commit -> 816e942 Don't return list of release. Release list can be extracted in searchdialog.py. This is to make searchdialog.py more generic. Same method (i.e. parse_tracks) can than be used to parse track node for both displaying similar tracks and searching. --- picard/file.py | 2 +- picard/metadata.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/picard/file.py b/picard/file.py index 608c27405..74cf91dab 100644 --- a/picard/file.py +++ b/picard/file.py @@ -517,7 +517,7 @@ class File(QtCore.QObject, Item): # multiple matches -- calculate similarities to each of them match = sorted((self.metadata.compare_to_track( - track, self.comparison_weights)[0] for track in tracks), + track, self.comparison_weights) for track in tracks), reverse=True, key=itemgetter(0))[0] if lookuptype != 'acoustid': diff --git a/picard/metadata.py b/picard/metadata.py index fc376f566..18464de98 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -18,7 +18,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. from PyQt4.QtCore import QObject -from operator import itemgetter from picard import config, log from picard.plugin import PluginFunctions, PluginPriority from picard.similarity import similarity2 @@ -200,15 +199,15 @@ class Metadata(dict): if not releases: sim = linear_combination_of_weights(parts) - return [(sim, None, None, track)] + return (sim, None, None, track) - result = [] + result = (-1,) for release in releases: release_parts = self.compare_to_release_parts(release, weights) sim = linear_combination_of_weights(parts + release_parts) - rg = release.release_group[0] if "release_group" in release.children else None - result.append((sim, rg, release, track)) - result.sort(key=itemgetter(0), reverse=True) + if sim > result[0]: + rg = release.release_group[0] if "release_group" in release.children else None + result = (sim, rg, release, track) return result From 3d3257f8dc974d919b6282acb46720f82dcf8319 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 15 Jun 2016 00:42:10 +0530 Subject: [PATCH 25/98] Display track search results in dialog If user chooses to, i.e. if config.setting["builtin_search"] is set. Don't pass the file object to SearchDialog.__init__(), as same class is used for displaying search results, for which there are no linked files. --- picard/ui/mainwindow.py | 10 ++- picard/ui/searchdialog.py | 141 +++++++++++++++++++++----------------- 2 files changed, 85 insertions(+), 66 deletions(-) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index a812cc932..6701c6964 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -679,7 +679,12 @@ class MainWindow(QtGui.QMainWindow): """Search for album, artist or track on the MusicBrainz website.""" text = self.search_edit.text() type = self.search_combo.itemData(self.search_combo.currentIndex()) - self.tagger.search(text, type, config.setting["use_adv_search_syntax"]) + if config.setting["builtin_search"] and type == "track": + dialog = SearchDialog(self) + dialog.search(text) + dialog.exec_() + else: + self.tagger.search(text, type, config.setting["use_adv_search_syntax"]) def add_files(self): """Add files to the tagger.""" @@ -800,7 +805,8 @@ class MainWindow(QtGui.QMainWindow): obj = self.selected_objects[0] if isinstance(obj, Track): obj = obj.linked_files[0] - dialog = SearchDialog(obj, self) + dialog = SearchDialog(self) + dialog.load_similar_tracks(obj) dialog.exec_() def view_info(self): diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index defaaee9f..f6551fc98 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -55,20 +55,26 @@ class SearchDialog(PicardDialog): config.Option("persist", "searchdialog_header_state", QtCore.QByteArray()) ] - def __init__(self, obj, parent=None): + def __init__(self, parent=None): PicardDialog.__init__(self, parent) self.setObjectName(_("SearchDialog")) self.setWindowTitle(_("Track Search Results")) - self.obj = obj + self.file_ = None self.search_results = [] self.setupUi() - self.load_similar_tracks(self.obj) self.restore_window_state() - def load_similar_tracks(self, obj): - metadata = obj.orig_metadata + def search(self, query): self.show_progress() - self.tagger.xmlws.find_tracks(partial(self.show_tracks, obj), + self.tagger.xmlws.find_tracks(self.handle_reply, + track=query, + limit=25) + + def load_similar_tracks(self, file_): + self.file_ = file_ + metadata = file_.orig_metadata + self.show_progress() + self.tagger.xmlws.find_tracks(self.handle_reply, track=metadata['title'], artist=metadata['artist'], release=metadata['tracknumber'], @@ -108,15 +114,18 @@ class SearchDialog(PicardDialog): def load_selection(self, row=None): track_id, release_id, rg_id = self.search_results[row][:3] if release_id: - album = self.obj.parent.album self.tagger.get_release_group_by_id(rg_id).loaded_albums.add( release_id) - self.tagger.move_file_to_track(self.obj, release_id, track_id) - if album._files == 0: - # Remove album if the selected file was the only one in album - # Compared to 0 because file has already moved to another album - # by move_file_to_track - self.tagger.remove_album(album) + if self.file_: + album = self.file_.parent.album + self.tagger.move_file_to_track(self.file_, release_id, track_id) + if album._files == 0: + # Remove album if the selected file was the only one in album + # Compared to 0 because file has already moved to another album + # by move_file_to_track + self.tagger.remove_album(album) + else: + self.tagger.load_album(release_id) self.save_state() self.closeEvent() @@ -133,50 +142,52 @@ class SearchDialog(PicardDialog): def closeEvent(self, event=None): self.save_state() - if event: - event.accept() - else: - self.accept() + self.accept() - def parse_match(self, match): - rg, release, track = match[1:] - rec_id = track.id - rec_title = track.title[0].text - artist = artist_credit_from_node(track.artist_credit[0])[0] - try: - length = format_time(track.length[0].text) - except AttributeError: - length = "" - if release: - rel_id = release.id - rel_title = release.title[0].text - if "date" in release.children: - date = release.date[0].text + def parse_tracks(self, tracks): + for track in tracks: + rec_id = track.id + rec_title = track.title[0].text + artist = artist_credit_from_node(track.artist_credit[0])[0] + try: + length = format_time(track.length[0].text) + except AttributeError: + length = "" + try: + releases = track.release_list[0].release + except AttributeError: + pass + if releases: + for release in releases: + rel_id = release.id + rel_title = release.title[0].text + if "date" in release.children: + date = release.date[0].text + else: + date = None + if "country" in release.children: + country = release.country[0].text + else: + country = "" + rg = release.release_group[0] + rg_id = rg.id + types_list = [] + if "primary_type" in rg.children: + types_list.append(rg.primary_type[0].text) + if "secondary_type_list" in rg.children: + for sec in rg.secondary_type_list: + types_list.append(sec.secondary_type[0].text) + types = "+".join(types_list) + + result = (rec_id, rel_id, rg_id, rec_title, artist, length, + rel_title, date, country, types) + self.search_results.append(result) else: - date = None - if "country" in release.children: - country = release.country[0].text - else: - country = "" - rg_id = rg.id - types_list = [] - if "primary_type" in rg.children: - types_list.append(rg.primary_type[0].text) - if "secondary_type_list" in rg.children: - for sec in rg.secondary_type_list: - types_list.append(sec.secondary_type[0].text) - types = "+".join(types_list) + result = (rec_id, "", "", rec_title, artist, length, "", "", "", + "") + self.search_results.append(result) - result = (rec_id, rel_id, rg_id, rec_title, artist, length, - rel_title, date, country, types) - else: - result = (rec_id, "", "", rec_title, artist, length, "", "", "", - "") - self.search_results.append(result) - return result - - def show_tracks(self, obj, document, http, error): - self.show_table() + def handle_reply(self, document, http, error): try: tracks = document.metadata[0].recording_list[0].recording except (AttributeError, IndexError): @@ -184,16 +195,19 @@ class SearchDialog(PicardDialog): # To be done: Notify user about that, or just close the dialog return - tmp = [] - for track in tracks: - tmp.extend(self.obj.orig_metadata.compare_to_track(track, - File.comparison_weights)) + if self.file_: + tmp = sorted((self.file_.orig_metadata.compare_to_track(track, + File.comparison_weights) for track in tracks), reverse=True, + key=itemgetter(0)) + tracks = [item[3] for item in tmp] - sorted_matches = [i for i in sorted(tmp, key=itemgetter(0), reverse=True) - if i[0] > config.setting['file_lookup_threshold']] - for row, match in enumerate(sorted_matches): - result = self.parse_match(match) - title, artist, length, release, date, country, type = result[3:] + self.parse_tracks(tracks) + self.display_results() + + def display_results(self): + self.show_table() + for row, tup in enumerate(self.search_results): + title, artist, length, release, date, country, type = tup[3:] table_item = QtGui.QTableWidgetItem self.tracksTable.insertRow(row) self.tracksTable.setItem(row, 0, table_item(title)) @@ -216,7 +230,6 @@ class SearchDialog(PicardDialog): header.restoreState(state) header.setResizeMode(QtGui.QHeaderView.Interactive) - def save_state(self): header = self.tracksTable.horizontalHeader() config.persist["searchdialog_header_state"] = header.saveState() From c6dda84743ee9baec45c1b5f2964cfa33696df89 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 15 Jun 2016 22:58:35 +0530 Subject: [PATCH 26/98] Use dismax filter for search request --- picard/ui/searchdialog.py | 1 + picard/webservice.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index f6551fc98..82c06a129 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -68,6 +68,7 @@ class SearchDialog(PicardDialog): self.show_progress() self.tagger.xmlws.find_tracks(self.handle_reply, track=query, + dismax="true", limit=25) def load_similar_tracks(self, file_): diff --git a/picard/webservice.py b/picard/webservice.py index 293fbac55..a3ea6b124 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -460,7 +460,7 @@ class XmlWebService(QtCore.QObject): filters = [] query = [] for name, value in kwargs.items(): - if name == 'limit': + if name in ('limit', 'dismax'): filters.append((name, str(value))) else: value = _escape_lucene_query(value).strip().lower() From a140abbf04d8e9051c86aede060a6407d18c756d Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 16 Jun 2016 18:23:27 +0530 Subject: [PATCH 27/98] Show loading gif while fetching results --- picard/resources.py | 260 ++++++++++++++++++++++++------------ picard/ui/searchdialog.py | 17 ++- resources/images/loader.gif | Bin 0 -> 673 bytes resources/makeqrc.py | 13 +- resources/picard.qrc | 1 + 5 files changed, 199 insertions(+), 92 deletions(-) create mode 100644 resources/images/loader.gif diff --git a/picard/resources.py b/picard/resources.py index 2c30635c9..d5da1502d 100644 --- a/picard/resources.py +++ b/picard/resources.py @@ -1262,6 +1262,51 @@ qt_resource_data = "\ \xb3\xa9\xe4\x06\xaa\xfb\x62\x36\x86\x02\x46\x8a\x63\x13\x00\x29\ \x51\x09\x03\x00\x20\x62\x2f\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ \x42\x60\x82\ +\x00\x00\x02\xa1\ +\x47\ +\x49\x46\x38\x39\x61\x10\x00\x10\x00\xf2\x00\x00\xff\xff\xff\x00\ +\x00\x00\xc2\xc2\xc2\x42\x42\x42\x00\x00\x00\x62\x62\x62\x82\x82\ +\x82\x92\x92\x92\x21\xfe\x1a\x43\x72\x65\x61\x74\x65\x64\x20\x77\ +\x69\x74\x68\x20\x61\x6a\x61\x78\x6c\x6f\x61\x64\x2e\x69\x6e\x66\ +\x6f\x00\x21\xf9\x04\x00\x0a\x00\x00\x00\x21\xff\x0b\x4e\x45\x54\ +\x53\x43\x41\x50\x45\x32\x2e\x30\x03\x01\x00\x00\x00\x2c\x00\x00\ +\x00\x00\x10\x00\x10\x00\x00\x03\x33\x08\xba\xdc\xfe\x30\xca\x49\ +\x6b\x13\x63\x08\x3a\x08\x19\x9c\x07\x4e\x98\x66\x09\x45\xb1\x31\ +\xc2\xba\x14\x99\xc1\xb6\x2e\x60\xc4\xc2\x71\xd0\x2d\x5b\x18\x39\ +\xdd\xa6\x07\x39\x18\x0c\x07\x4a\x6b\xe7\x48\x00\x00\x21\xf9\x04\ +\x00\x0a\x00\x01\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\ +\x34\x08\xba\xdc\xfe\x4e\x8c\x21\x20\x1b\x84\x0c\xbb\xb0\xe6\x8a\ +\x44\x71\x42\x51\x54\x60\x31\x19\x20\x60\x4c\x45\x5b\x1a\xa8\x7c\ +\x1c\xb5\x75\xdf\xed\x61\x18\x07\x80\x20\xd7\x18\xe2\x86\x43\x19\ +\xb2\x25\x24\x2a\x12\x00\x21\xf9\x04\x00\x0a\x00\x02\x00\x2c\x00\ +\x00\x00\x00\x10\x00\x10\x00\x00\x03\x36\x08\xba\x32\x23\x2b\xca\ +\x41\xc8\x90\xcc\x94\x56\x2f\x06\x85\x63\x1c\x0e\xf4\x19\x4e\xf1\ +\x49\x42\x61\x98\xab\x70\x1c\xf0\x0a\xcc\xb3\xbd\x1c\xc6\xa8\x2b\ +\x02\x59\xed\x17\xfc\x01\x83\xc3\x0f\x32\xa9\x64\x1a\x9f\xbf\x04\ +\x00\x21\xf9\x04\x00\x0a\x00\x03\x00\x2c\x00\x00\x00\x00\x10\x00\ +\x10\x00\x00\x03\x33\x08\xba\x62\x25\x2b\xca\x32\x86\x91\xec\x9c\ +\x56\x5f\x85\x8b\xa6\x09\x85\x21\x0c\x04\x31\x44\x87\x61\x1c\x11\ +\xaa\x46\x82\xb0\xd1\x1f\x03\x62\x52\x5d\xf3\x3d\x1f\x30\x38\x2c\ +\x1a\x8f\xc8\xa4\x72\x39\x4c\x00\x00\x21\xf9\x04\x00\x0a\x00\x04\ +\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\x32\x08\xba\x72\ +\x27\x2b\x4a\xe7\x64\x14\xf0\x18\xf3\x4c\x81\x0c\x26\x76\xc3\x60\ +\x5c\x62\x54\x94\x85\x84\xb9\x1e\x68\x59\x42\x29\xcf\xca\x40\x10\ +\x03\x1e\xe9\x3c\x1f\xc3\x26\x2c\x1a\x8f\xc8\xa4\x52\x92\x00\x00\ +\x21\xf9\x04\x00\x0a\x00\x05\x00\x2c\x00\x00\x00\x00\x10\x00\x10\ +\x00\x00\x03\x33\x08\xba\x20\xc2\x90\x39\x17\xe3\x74\xe7\xbc\xda\ +\x9e\x30\x19\xc7\x1c\xe0\x21\x2e\x42\xb6\x9d\xca\x57\xac\xa2\x31\ +\x0c\x06\x0b\x14\x73\x61\xbb\xb0\x35\xf7\x95\x01\x81\x30\xb0\x09\ +\x89\xbb\x9f\x6d\x29\x4a\x00\x00\x21\xf9\x04\x00\x0a\x00\x06\x00\ +\x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\x32\x08\xba\xdc\xfe\ +\xf0\x09\x11\xd9\x9c\x55\x5d\x9a\x01\xee\xda\x71\x70\x95\x60\x88\ +\xdd\x61\x9c\xdd\x34\x96\x85\x41\x46\xc5\x30\x14\x90\x60\x9b\xb6\ +\x01\x0d\x04\xc2\x40\x10\x9b\x31\x80\xc2\xd6\xce\x91\x00\x00\x21\ +\xf9\x04\x00\x0a\x00\x07\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\ +\x00\x03\x32\x08\xba\xdc\xfe\x30\xca\x49\xab\x65\x42\xd4\x9c\x29\ +\xd7\x1e\x08\x08\xc3\x20\x8e\xc7\x71\x0e\x04\x31\x30\xa9\xca\xb0\ +\xae\x50\x18\xc2\x61\x18\x07\x56\xda\xa5\x02\x20\x75\x62\x18\x82\ +\x9e\x5b\x11\x90\x00\x00\x3b\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\ \x00\x00\x00\x83\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -3419,31 +3464,75 @@ qt_resource_data = "\ \x8f\xc4\xad\x06\x0f\xc4\xcd\x1e\x8f\x8e\x7f\x01\xd7\x2b\x79\xd4\ \xea\x76\x04\x5f\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \ -\x00\x00\x01\x62\ +\x00\x00\x04\x2d\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x01\x29\x49\x44\x41\x54\x78\x01\x63\x18\xa2\xe0\x3f\x63\ -\xb9\x74\x6b\x5c\x99\x74\x5b\xfc\x9b\x62\xeb\xf8\x37\xa5\x36\x71\ -\xff\x19\x18\x18\xe9\x62\x75\x85\x64\xbb\x71\x99\x74\xeb\x11\xa0\ -\x03\xfe\x57\xc8\xb4\x96\xbf\x29\xb1\x29\x7f\x5d\x62\xfd\x1f\x88\ -\xcf\xbc\x2c\xb2\xb4\xa2\x99\xc5\x95\xd2\x6d\xc2\x15\x52\x6d\x13\ -\x81\x16\xff\x01\x59\x8e\xee\x00\x28\xfe\xf7\xba\xc4\x6a\xd1\xab\ -\x52\x07\x09\xaa\x59\x9c\x66\x3c\x93\xb5\x5c\xa6\x35\x1f\x68\xe1\ -\x07\x98\xc5\x38\x1d\x80\xc0\x5f\x5e\x97\xd8\x34\xdc\xce\xf5\x64\ -\xa7\xc8\x72\x60\x50\x3b\x97\x4b\xb5\x5d\x81\x59\x48\xc8\x01\x98\ -\xd8\xea\xd6\xeb\x52\x1b\x1f\xd2\xe3\x59\xa6\x5d\xa5\x5c\xba\x6d\ -\x15\xcc\x22\xb2\x1d\x80\xc0\xbb\xdf\x16\xdb\x6a\x11\xb4\xb8\x44\ -\xbc\x9b\x1b\x68\x68\x03\xd0\xf0\x1f\x50\x4b\x28\x77\x00\x02\xff\ -\x02\xaa\x9d\xf8\x36\xd7\x9c\x0f\xab\xe5\xa5\xd2\xad\x06\x40\x43\ -\x9f\xc1\x0c\xa7\xba\x03\x10\xf8\xd9\xab\x52\x2b\x03\xcc\x60\x97\ -\x6e\x09\x05\x19\x4a\x07\x07\xfc\x7f\x55\x6a\x13\x3a\xe0\x0e\x18\ -\x75\xc0\xa8\x03\x46\x1d\x30\xea\x80\x51\x07\x8c\x3a\x60\xc0\xab\ -\x63\x42\x0d\x92\xef\xf4\x69\x90\xd0\xbf\x49\x46\xff\x46\x29\x10\ -\x7b\x0f\x68\xb3\x7c\xc8\x77\x4c\x30\x73\x8b\x54\xab\x11\x8e\xae\ -\xd9\xe9\xb7\x25\x56\x96\x43\xbd\x73\x4a\x7f\x00\x00\x00\x0b\xb8\ -\x4b\xac\x5f\x46\xcf\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ -\x82\ +\x00\x00\x20\x00\x00\x00\x20\x08\x03\x00\x00\x00\x44\xa4\x8a\xc6\ +\x00\x00\x02\x1c\x50\x4c\x54\x45\x00\x00\x00\x00\x00\xff\xff\x00\ +\x00\x80\x00\x80\xff\x80\x40\x71\x1c\x8e\xea\x6a\x40\x78\x1e\x87\ +\x71\x1c\x80\xe8\x74\x3a\x7b\x1c\x84\xec\x71\x39\xe7\x78\x38\x75\ +\x1b\x85\xec\x76\x39\xec\x74\x3c\x75\x1c\x86\xe9\x73\x39\x77\x1a\ +\x84\x77\x1b\x85\xec\x75\x3c\xea\x73\x3c\x78\x1c\x85\x77\x1b\x84\ +\xec\x75\x3a\x78\x1b\x85\xec\x74\x3c\xea\x74\x3a\x78\x1a\x86\x76\ +\x1b\x85\xeb\x75\x3a\x78\x1b\x84\xeb\x74\x3a\xb3\x80\xac\xf6\xbe\ +\x92\xeb\x73\x3a\xcb\xa8\xbb\xf8\xcc\xa1\x77\x1a\x85\x78\x1c\x85\ +\xeb\x73\x3c\x78\x1b\x85\xeb\x74\x3b\x77\x1c\x85\xeb\x74\x3c\x77\ +\x1b\x85\xeb\x74\x3b\x77\x1b\x85\xeb\x74\x3b\x77\x1b\x85\xeb\x74\ +\x3b\x77\x1b\x85\x77\x1b\x85\x78\x1b\x85\x79\x1e\x87\x79\x1e\x88\ +\x79\x1f\x86\x7a\x20\x87\x7a\x20\x89\x7d\x22\x8a\x7d\x23\x8c\x7e\ +\x24\x8c\x7f\x25\x8c\x80\x26\x8d\x84\x2c\x92\x85\x2c\x92\x88\x30\ +\x95\x88\x31\x96\x8a\x33\x97\x8c\x34\x98\x8c\x37\x9a\x8e\x38\x9c\ +\x8f\x39\x9b\x90\x3a\x9d\x94\x40\xa2\x97\x51\x9a\x99\x46\xa6\x9a\ +\x49\xa7\x9c\x49\xa9\x9f\x4e\xac\xa0\x4f\xac\xa5\x55\xb2\xa8\x58\ +\xb4\xac\x5d\xb8\xac\x5e\xb8\xad\x60\xba\xaf\x62\xbb\xb0\x64\xbc\ +\xb3\x66\xbe\xb4\x68\xc0\xb4\x6a\xc1\xb5\x69\xc1\xb5\x6a\xc2\xb6\ +\x6b\xc2\xb9\x82\xb3\xbb\x75\xc3\xc0\x80\xc6\xc2\x84\xc6\xc7\x8e\ +\xc8\xc8\xa2\xb9\xca\xa2\xbb\xcd\x99\xca\xcf\x9d\xcb\xd1\xb1\xbe\ +\xd5\xaa\xcd\xd8\xb1\xce\xda\xb4\xce\xda\xbd\xc5\xe1\xc8\xcb\xe4\ +\xd0\xca\xe6\xca\xd3\xea\xd3\xd3\xeb\x74\x3b\xeb\x75\x3d\xeb\x76\ +\x3d\xec\x78\x40\xec\x79\x42\xec\x7b\x43\xec\x7c\x44\xec\x7e\x46\ +\xec\x7e\x47\xed\x7f\x47\xed\x7f\x48\xed\x81\x4a\xed\x82\x4a\xed\ +\x82\x4b\xed\x84\x4d\xee\x87\x51\xee\x88\x52\xee\x8b\x55\xee\x8b\ +\x56\xf0\x94\x60\xf0\x97\x64\xf0\x98\x65\xf0\x9a\x66\xf1\x9c\x69\ +\xf1\x9e\x6b\xf1\xe6\xd4\xf2\xa1\x6f\xf2\xa2\x70\xf2\xa4\x73\xf2\ +\xa7\x75\xf2\xe3\xd7\xf2\xe9\xd3\xf3\xae\x7d\xf5\xb8\x88\xf5\xea\ +\xd8\xf6\xbc\x8f\xf6\xee\xd6\xf7\xc9\x9e\xf8\xcb\xa0\xf8\xcf\xa4\ +\xf8\xd0\xa6\xf9\xd6\xac\xf9\xd7\xae\xf9\xf4\xd8\xfb\xe0\xb9\xfb\ +\xe2\xba\xfb\xe2\xbb\xfb\xe3\xbc\xfb\xf7\xda\xfc\xe9\xc3\xfc\xeb\ +\xc5\xfc\xed\xc7\xfc\xf8\xda\xfd\xef\xca\xfd\xf0\xcb\xfd\xf3\xce\ +\xfd\xfb\xda\xfe\xf4\xcf\xfe\xf6\xd1\xfe\xf6\xd2\xfe\xf7\xd3\xfe\ +\xf8\xd5\xfe\xf9\xd4\xff\xfb\xd7\xff\xfc\xd9\xff\xfd\xda\xff\xfd\ +\xdb\xff\xfe\xdb\xa6\x92\x1f\xb1\x00\x00\x00\x34\x74\x52\x4e\x53\ +\x00\x01\x01\x02\x04\x09\x0c\x11\x12\x16\x1b\x1b\x20\x30\x36\x37\ +\x3f\x47\x4d\x5e\x5e\x6f\x77\x83\x83\x84\x84\x88\x91\x99\x99\xa2\ +\xa2\xa8\xb1\xb3\xbb\xbd\xc1\xc2\xc9\xd1\xd7\xde\xe3\xea\xee\xf3\ +\xf6\xfc\xfd\xfe\x98\x06\x23\x55\x00\x00\x01\x8c\x49\x44\x41\x54\ +\x38\xcb\x63\x60\x40\x01\x8c\x5c\xdc\x4c\x0c\x78\x00\xaf\xba\xa4\ +\x94\x06\x3f\x4e\x69\x0e\x19\x43\x13\x49\xa9\x02\x23\x05\x4e\xac\ +\xd2\xcc\xc2\xba\x26\x26\x20\x05\x05\x05\x7a\xa2\x2c\x98\xf2\x02\ +\x9a\x26\x26\x30\x05\x05\x05\x5a\x82\x68\xd2\x5c\xf2\xc6\x26\xc8\ +\x0a\x0a\x0a\x94\x79\x90\xa4\x59\xc5\xf4\x4d\x4c\xd0\x14\x14\x18\ +\x48\xb3\xc1\xe4\x85\xb4\x4d\x4c\x30\x15\x14\x14\xe8\x88\x40\xe4\ +\xc5\x21\x52\x36\xae\x26\xde\xc9\xb9\x2a\x48\x0a\x0a\x0a\x24\xc0\ +\x0a\x64\x41\xd2\x76\xbe\x91\xf1\x39\x9b\xfb\xd3\x15\x21\x0a\x8a\ +\xda\x9a\x0b\x81\x94\x1c\x5c\x81\x47\x74\x5a\xfe\xa4\x25\x71\x16\ +\x30\x2b\x66\x6c\xde\x3c\x15\x45\x41\x40\xd2\xa6\xec\x29\x79\x51\ +\x0e\x50\x05\x75\x9b\xbb\x7a\x36\x56\x22\x2b\x08\xce\x98\x6d\x99\ +\xd2\x17\x0b\x53\x50\xba\x6e\xda\xf4\xd5\x25\x48\x0a\xcc\xa2\x13\ +\x16\x74\xae\xc8\x8c\x31\x87\x2a\xa8\x5f\xbe\x6e\x61\x07\xb2\x15\ +\x8e\xb1\xb1\x89\x59\xa9\xb1\x21\x50\x6f\x36\x2e\x5e\x5a\x0b\x76\ +\x0a\x5c\x81\x67\x2c\x18\x04\x80\x15\xd4\x4c\x5e\xbf\xac\xa1\x00\ +\x55\x81\x1f\x44\x81\x17\x50\x81\xd2\xdc\x0d\x9b\x37\x6f\x9c\xd5\ +\x84\xaa\x20\x08\xa2\xc0\x19\xa8\x40\x75\xde\xc4\xea\xf2\xde\x45\ +\x6b\x26\xb4\x17\x21\x14\x98\x46\x40\x14\x58\xc3\x83\xba\x62\xd5\ +\xc6\x0d\xd3\x11\x0a\xec\x21\xf2\xe1\x88\xb8\x68\xd8\xdc\xde\xbd\ +\xb9\x0a\xae\xc0\x35\x36\x36\x34\x2c\x36\x36\x10\xa1\xa0\x6c\xed\ +\x8c\x99\x2b\x8b\xe1\x0a\x9c\xdc\x6d\x4d\xac\xfc\x63\x7d\x90\x62\ +\xb3\x75\xce\xfc\x16\xe4\x90\x04\x01\x37\x17\xb4\xe8\x86\x29\x10\ +\x37\x31\xc1\x9e\x1e\x60\xd1\x4d\x30\xc1\x10\x4e\x72\x84\x13\x2d\ +\x11\xc9\x9e\x88\x8c\x03\x04\xec\xf8\xb3\x1e\x38\xf3\xaa\x01\x33\ +\x2f\x1f\xbe\xec\x8d\x99\xfd\x01\x12\xd5\xd3\xad\x82\xe8\xbe\xc1\ +\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x33\x3f\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -4759,6 +4848,10 @@ qt_resource_name = "\ \x00\x6d\ \x00\x61\x00\x74\x00\x63\x00\x68\x00\x2d\x00\x70\x00\x65\x00\x6e\x00\x64\x00\x69\x00\x6e\x00\x67\x00\x2d\x00\x36\x00\x30\x00\x2e\ \x00\x70\x00\x6e\x00\x67\ +\x00\x0a\ +\x0a\xcb\x27\x16\ +\x00\x6c\ +\x00\x6f\x00\x61\x00\x64\x00\x65\x00\x72\x00\x2e\x00\x67\x00\x69\x00\x66\ \x00\x05\ \x00\x35\x9b\x52\ \x00\x32\ @@ -4935,78 +5028,79 @@ qt_resource_name = "\ qt_resource_struct = "\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1d\x00\x00\x00\x02\ -\x00\x00\x02\x72\x00\x00\x00\x00\x00\x01\x00\x00\x4d\x8d\ -\x00\x00\x02\xf8\x00\x02\x00\x00\x00\x12\x00\x00\x00\x37\ -\x00\x00\x02\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x24\ -\x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x23\ -\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x22\ -\x00\x00\x01\x4e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1e\x00\x00\x00\x02\ +\x00\x00\x02\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x50\x32\ +\x00\x00\x03\x12\x00\x02\x00\x00\x00\x12\x00\x00\x00\x38\ +\x00\x00\x02\x30\x00\x02\x00\x00\x00\x13\x00\x00\x00\x25\ +\x00\x00\x00\x12\x00\x02\x00\x00\x00\x01\x00\x00\x00\x24\ +\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x23\ +\x00\x00\x01\x4e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x22\ \x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x03\x4a\x00\x00\x00\x00\x00\x01\x00\x00\x54\x26\ -\x00\x00\x03\x26\x00\x00\x00\x00\x00\x01\x00\x00\x53\x90\ -\x00\x00\x00\x94\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ -\x00\x00\x01\x06\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1f\ -\x00\x00\x02\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x50\xac\ +\x00\x00\x03\x64\x00\x00\x00\x00\x00\x01\x00\x00\x56\xcb\ +\x00\x00\x03\x40\x00\x00\x00\x00\x00\x01\x00\x00\x56\x35\ +\x00\x00\x00\x94\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\ +\x00\x00\x01\x06\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ +\x00\x00\x02\xee\x00\x00\x00\x00\x00\x01\x00\x00\x53\x51\ \x00\x00\x01\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x47\x52\ \x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x46\xe5\ \x00\x00\x00\x48\x00\x00\x00\x00\x00\x01\x00\x00\x01\x3e\ \x00\x00\x00\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x03\x8e\ \x00\x00\x00\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x43\x17\ -\x00\x00\x02\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x50\x23\ +\x00\x00\x02\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x52\xc8\ \x00\x00\x00\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x40\xf4\ -\x00\x00\x02\x44\x00\x00\x00\x00\x00\x01\x00\x00\x4d\x14\ +\x00\x00\x02\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x4f\xb9\ \x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x4c\x05\ \x00\x00\x01\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x4a\xf5\ -\x00\x00\x03\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x54\xa3\ +\x00\x00\x02\x16\x00\x00\x00\x00\x00\x01\x00\x00\x4c\x8d\ +\x00\x00\x03\x84\x00\x00\x00\x00\x00\x01\x00\x00\x57\x48\ \x00\x00\x01\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x48\x77\ -\x00\x00\x03\x08\x00\x00\x00\x00\x00\x01\x00\x00\x52\xfc\ -\x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x4f\x8a\ -\x00\x00\x02\x26\x00\x00\x00\x00\x00\x01\x00\x00\x4c\x8d\ +\x00\x00\x03\x22\x00\x00\x00\x00\x00\x01\x00\x00\x55\xa1\ +\x00\x00\x02\xa2\x00\x00\x00\x00\x00\x01\x00\x00\x52\x2f\ +\x00\x00\x02\x40\x00\x00\x00\x00\x00\x01\x00\x00\x4f\x32\ \x00\x00\x01\xca\x00\x00\x00\x00\x00\x01\x00\x00\x4b\x6b\ \x00\x00\x01\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x6e\ -\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x00\xce\x2d\ -\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x01\x01\x70\ -\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x00\xc7\x62\ -\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x00\xcc\xc7\ -\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x01\x19\xca\ -\x00\x00\x06\x4a\x00\x00\x00\x00\x00\x01\x00\x00\xbb\x5b\ -\x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x00\x92\x3c\ -\x00\x00\x06\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x85\x4d\ -\x00\x00\x07\x44\x00\x00\x00\x00\x00\x01\x00\x00\xa2\xb9\ -\x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x00\xae\x5d\ -\x00\x00\x07\x6e\x00\x00\x00\x00\x00\x01\x00\x00\xa4\xe2\ -\x00\x00\x06\x94\x00\x00\x00\x00\x00\x01\x00\x00\x7e\x20\ -\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x80\xde\ -\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\x87\x7b\ -\x00\x00\x06\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x8c\xc7\ -\x00\x00\x07\xbe\x00\x00\x00\x00\x00\x01\x00\x00\xc0\x5d\ -\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x95\xcc\ -\x00\x00\x04\x00\x00\x00\x00\x00\x00\x01\x00\x00\x82\xad\ -\x00\x00\x05\x3a\x00\x00\x00\x00\x00\x01\x00\x00\xaa\xde\ -\x00\x00\x06\x72\x00\x00\x00\x00\x00\x01\x00\x00\xc3\x00\ -\x00\x00\x05\x06\x00\x00\x00\x00\x00\x01\x00\x00\x9d\xe9\ -\x00\x00\x07\x16\x00\x00\x00\x00\x00\x01\x00\x00\x9a\x4a\ -\x00\x00\x07\x96\x00\x00\x00\x00\x00\x01\x00\x00\xb7\x08\ -\x00\x00\x06\x22\x00\x00\x00\x00\x00\x01\x00\x00\xb3\x89\ -\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x59\x46\ -\x00\x00\x06\x4a\x00\x00\x00\x00\x00\x01\x00\x00\x78\xd0\ -\x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x00\x5f\x84\ -\x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x00\x70\xcf\ -\x00\x00\x03\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x56\x1d\ -\x00\x00\x06\x04\x00\x00\x00\x00\x00\x01\x00\x00\x73\xd5\ -\x00\x00\x05\x5c\x00\x00\x00\x00\x00\x01\x00\x00\x69\xf6\ -\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\x5c\x7b\ -\x00\x00\x04\x4a\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xa2\ -\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x61\x46\ -\x00\x00\x04\x00\x00\x00\x00\x00\x00\x01\x00\x00\x57\xa1\ -\x00\x00\x05\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x6e\xf8\ -\x00\x00\x05\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x67\xd3\ -\x00\x00\x03\x98\x00\x00\x00\x00\x00\x01\x00\x00\x55\x27\ -\x00\x00\x06\x72\x00\x00\x00\x00\x00\x01\x00\x00\x7c\x0e\ -\x00\x00\x05\x06\x00\x00\x00\x00\x00\x01\x00\x00\x64\xbf\ -\x00\x00\x05\x82\x00\x00\x00\x00\x00\x01\x00\x00\x6c\xd0\ -\x00\x00\x06\x22\x00\x00\x00\x00\x00\x01\x00\x00\x76\x36\ +\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\xd3\x9d\ +\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x01\x06\xe0\ +\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\xca\x07\ +\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\xcf\x6c\ +\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x01\x1f\x3a\ +\x00\x00\x06\x64\x00\x00\x00\x00\x00\x01\x00\x00\xbe\x00\ +\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x94\xe1\ +\x00\x00\x06\xda\x00\x00\x00\x00\x00\x01\x00\x00\x87\xf2\ +\x00\x00\x07\x5e\x00\x00\x00\x00\x00\x01\x00\x00\xa5\x5e\ +\x00\x00\x05\xe4\x00\x00\x00\x00\x00\x01\x00\x00\xb1\x02\ +\x00\x00\x07\x88\x00\x00\x00\x00\x00\x01\x00\x00\xa7\x87\ +\x00\x00\x06\xae\x00\x00\x00\x00\x00\x01\x00\x00\x80\xc5\ +\x00\x00\x03\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x83\x83\ +\x00\x00\x04\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x8a\x20\ +\x00\x00\x06\xfe\x00\x00\x00\x00\x00\x01\x00\x00\x8f\x6c\ +\x00\x00\x07\xd8\x00\x00\x00\x00\x00\x01\x00\x00\xc3\x02\ +\x00\x00\x04\xda\x00\x00\x00\x00\x00\x01\x00\x00\x98\x71\ +\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x85\x52\ +\x00\x00\x05\x54\x00\x00\x00\x00\x00\x01\x00\x00\xad\x83\ +\x00\x00\x06\x8c\x00\x00\x00\x00\x00\x01\x00\x00\xc5\xa5\ +\x00\x00\x05\x20\x00\x00\x00\x00\x00\x01\x00\x00\xa0\x8e\ +\x00\x00\x07\x30\x00\x00\x00\x00\x00\x01\x00\x00\x9c\xef\ +\x00\x00\x07\xb0\x00\x00\x00\x00\x00\x01\x00\x00\xb9\xad\ +\x00\x00\x06\x3c\x00\x00\x00\x00\x00\x01\x00\x00\xb6\x2e\ +\x00\x00\x04\x34\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xeb\ +\x00\x00\x06\x64\x00\x00\x00\x00\x00\x01\x00\x00\x7b\x75\ +\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x62\x29\ +\x00\x00\x05\xe4\x00\x00\x00\x00\x00\x01\x00\x00\x73\x74\ +\x00\x00\x03\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x58\xc2\ +\x00\x00\x06\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x76\x7a\ +\x00\x00\x05\x76\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x9b\ +\x00\x00\x04\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x5f\x20\ +\x00\x00\x04\x64\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x47\ +\x00\x00\x04\xda\x00\x00\x00\x00\x00\x01\x00\x00\x63\xeb\ +\x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x5a\x46\ +\x00\x00\x05\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x71\x9d\ +\x00\x00\x05\x54\x00\x00\x00\x00\x00\x01\x00\x00\x6a\x78\ +\x00\x00\x03\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x57\xcc\ +\x00\x00\x06\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x7e\xb3\ +\x00\x00\x05\x20\x00\x00\x00\x00\x00\x01\x00\x00\x67\x64\ +\x00\x00\x05\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x6f\x75\ +\x00\x00\x06\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x78\xdb\ " def qInitResources(): diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 82c06a129..3533ddd5a 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -101,9 +101,20 @@ class SearchDialog(PicardDialog): self.verticalLayout.addWidget(self.buttonBox) def show_progress(self): - self.label = QtGui.QLabel('Loading....') - self.label.setAlignment(QtCore.Qt.AlignCenter) - self.verticalLayout.insertWidget(0, self.label) + widget = QtGui.QWidget(self) + layout = QtGui.QVBoxLayout(widget) + text_label = QtGui.QLabel('Fetching results...', widget) + text_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom) + gif_label = QtGui.QLabel(widget) + movie = QtGui.QMovie(":/images/loader.gif") + gif_label.setMovie(movie) + movie.start() + gif_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop) + layout.addWidget(text_label) + layout.addWidget(gif_label) + layout.setMargin(1) + widget.setLayout(layout) + self.verticalLayout.insertWidget(0, widget) def show_table(self): self.tracksTable = TracksTable() diff --git a/resources/images/loader.gif b/resources/images/loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..f2a1bc0c6f545e20e631a96e8e92f9822e75d046 GIT binary patch literal 673 zcmZ?wbhEHb6krfw_{6~Q|Nnmm28Kh24mmkF0U1e2Nli^nlO|14{3qpHl$uzQnxasi zS(2fUn3Y(Olb@KPmzkHA&!G5|g@FsGT=74*pKD04vtxj(k)8oFBTz^Oh=E26FfcG1 zbL_hF&)}42ws10s6^G;;cE1^EoUR)U5A70}d2pLv!jVIT7j&Z~EblI3x0K*v_sV|m z0W=b9G$XP(CLnYCdK49;TX=SFc-G}o=oA=|U?{1O;Nu!CwW3C5Yw7*Bi4yD$3fCnb zwK+>}QdQ9sf*QnxY>*kpE+b{_Q;sJloS71)&(@kO!}mqf@1v(v;*8Y=G9S3kY~Cw# zY=t&c z;3~JK4HxB^lY(MD+sYeQ=t%XSSW;x^1M?dTvN=W^yNcAcy`HCte31C;)5xP%b~qs> zDP&4(%TBqBNGHwnryK;BdMI$fEg xd0mc!C@j^ZpLxYv4HmnPfI0THYuv<%+6iSmMn&w3dPGDfL1|=LY008wP(boU~ literal 0 HcmV?d00001 diff --git a/resources/makeqrc.py b/resources/makeqrc.py index edcaffb71..63ec68911 100755 --- a/resources/makeqrc.py +++ b/resources/makeqrc.py @@ -21,14 +21,15 @@ def natsort_key(s): return [ tryint(c) for c in re.split('(\d+)', s) ] -def find_files(topdir, directory, pattern): +def find_files(topdir, directory, patterns): tdir = os.path.join(topdir, directory) for root, dirs, files in os.walk(tdir): for basename in files: - if fnmatch.fnmatch(basename, pattern): - filepath = os.path.join(root, basename) - filename = os.path.relpath(filepath, topdir) - yield filename + for pattern in patterns: + if fnmatch.fnmatch(basename, pattern): + filepath = os.path.join(root, basename) + filename = os.path.relpath(filepath, topdir) + yield filename def main(): @@ -36,7 +37,7 @@ def main(): topdir = os.path.abspath(os.path.join(scriptdir, "..")) resourcesdir = os.path.join(topdir, "resources") qrcfile = os.path.join(resourcesdir, "picard.qrc") - images = [i for i in find_files(resourcesdir, 'images', '*.png')] + images = [i for i in find_files(resourcesdir, 'images', ['*.png', '*.gif'])] newimages = 0 for filename in images: filepath = os.path.join(resourcesdir, filename) diff --git a/resources/picard.qrc b/resources/picard.qrc index 29a5c57b0..13e00f0a9 100644 --- a/resources/picard.qrc +++ b/resources/picard.qrc @@ -46,6 +46,7 @@ images/arrow.png images/file-pending.png images/file.png + images/loader.gif images/match-50.png images/match-60.png images/match-70.png From 90b1359db026aa48cff7ce58eadbd5abac54f126 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 17 Jun 2016 12:49:07 +0530 Subject: [PATCH 28/98] Make progress_widget attribute of class * As it makes more sense. * It is required to be removed later, when tracks are loaded or some error occurs. So a local widget won't do. --- picard/ui/searchdialog.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 3533ddd5a..a4ec3dd51 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -101,11 +101,11 @@ class SearchDialog(PicardDialog): self.verticalLayout.addWidget(self.buttonBox) def show_progress(self): - widget = QtGui.QWidget(self) - layout = QtGui.QVBoxLayout(widget) - text_label = QtGui.QLabel('Fetching results...', widget) + self.progress_widget = QtGui.QWidget(self) + layout = QtGui.QVBoxLayout(self.progress_widget) + text_label = QtGui.QLabel('Fetching results...', self.progress_widget) text_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom) - gif_label = QtGui.QLabel(widget) + gif_label = QtGui.QLabel(self.progress_widget) movie = QtGui.QMovie(":/images/loader.gif") gif_label.setMovie(movie) movie.start() @@ -113,13 +113,14 @@ class SearchDialog(PicardDialog): layout.addWidget(text_label) layout.addWidget(gif_label) layout.setMargin(1) - widget.setLayout(layout) - self.verticalLayout.insertWidget(0, widget) + self.progress_widget.setLayout(layout) + self.verticalLayout.insertWidget(0, self.progress_widget) def show_table(self): self.tracksTable = TracksTable() self.tracksTable.cellDoubleClicked.connect(self.track_double_clicked) - self.verticalLayout.removeWidget(self.label) + self.verticalLayout.removeWidget(self.progress_widget) + self.progress_widget.deleteLater() self.verticalLayout.insertWidget(0, self.tracksTable) self.restore_table_header_state() From ce3e09a11138a07dea87f90db1b7a7010531eb30 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 17 Jun 2016 12:57:53 +0530 Subject: [PATCH 29/98] Display some informative text when tracks won't load --- picard/ui/searchdialog.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index a4ec3dd51..d375d2250 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -124,6 +124,14 @@ class SearchDialog(PicardDialog): self.verticalLayout.insertWidget(0, self.tracksTable) self.restore_table_header_state() + def show_error(self, error): + self.error_widget = QtGui.QLabel(_("" + error + "")) + self.error_widget.setAlignment(QtCore.Qt.AlignCenter) + self.error_widget.setWordWrap(True) + self.verticalLayout.removeWidget(self.progress_widget) + self.progress_widget.deleteLater() + self.verticalLayout.insertWidget(0, self.error_widget) + def load_selection(self, row=None): track_id, release_id, rg_id = self.search_results[row][:3] if release_id: @@ -201,11 +209,17 @@ class SearchDialog(PicardDialog): self.search_results.append(result) def handle_reply(self, document, http, error): + if error: + error_msg = _("Unable to fetch results. Close the dialog and try" + "again. See debug logs for more details.") + self.show_error(error_msg) + return + try: tracks = document.metadata[0].recording_list[0].recording except (AttributeError, IndexError): - # No results to show - # To be done: Notify user about that, or just close the dialog + error_msg = _("No results found. Please try a different search query.") + self.show_error(error_msg) return if self.file_: From 68ee7fc1b8b47470f6f9576f85cb4b9b3bdac598 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 17 Jun 2016 18:12:09 +0530 Subject: [PATCH 30/98] Handle dialog close exception * AttributeError will be raised if the tracks are loading ( self.tracksTable will be undefined) and accept is triggerred. Handle this case. * Merge track_selected() and closeEvent() functionality into self.accept(). --- picard/ui/searchdialog.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index d375d2250..1912c9bb8 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -96,7 +96,7 @@ class SearchDialog(PicardDialog): self.buttonBox.addButton( StandardButton(StandardButton.CANCEL), QtGui.QDialogButtonBox.RejectRole) - self.buttonBox.accepted.connect(self.track_selected) + self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) @@ -147,24 +147,23 @@ class SearchDialog(PicardDialog): self.tagger.remove_album(album) else: self.tagger.load_album(release_id) - self.save_state() - self.closeEvent() def track_double_clicked(self, row): self.load_selection(row) - - def track_selected(self): - sel_rows = self.tracksTable.selectionModel().selectedRows() - if sel_rows: - sel_row = sel_rows[0].row() - self.load_selection(sel_row) - else: - self.closeEvent() - - def closeEvent(self, event=None): - self.save_state() self.accept() + def accept(self): + try: + sel_rows = self.tracksTable.selectionModel().selectedRows() + if sel_rows: + sel_row = sel_rows[0].row() + self.load_selection(sel_row) + self.save_state(True) + QtGui.QDialog.accept(self) + except AttributeError: + self.save_state(False) + QtGui.QDialog.accept(self) + def parse_tracks(self, tracks): for track in tracks: rec_id = track.id @@ -257,7 +256,8 @@ class SearchDialog(PicardDialog): header.restoreState(state) header.setResizeMode(QtGui.QHeaderView.Interactive) - def save_state(self): - header = self.tracksTable.horizontalHeader() - config.persist["searchdialog_header_state"] = header.saveState() + def save_state(self, table_loaded=True): + if table_loaded: + header = self.tracksTable.horizontalHeader() + config.persist["searchdialog_header_state"] = header.saveState() config.persist["searchdialog_window_size"] = self.size() From a77f4a46cf877b5abf47a9bee2c1296368b313f1 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sun, 19 Jun 2016 12:46:12 +0530 Subject: [PATCH 31/98] Add a search bar in dialog * For further searching without closing the dialog. * Pressing after inserting a search query in the search box would result in closing the dialog rather than searching. This needs to be fixed. --- picard/ui/searchdialog.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 1912c9bb8..ee90dbb4b 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -22,8 +22,8 @@ from functools import partial from picard import config from picard.file import File from picard.ui import PicardDialog -from picard.ui.util import StandardButton -from picard.util import format_time +from picard.ui.util import StandardButton, ButtonLineEdit +from picard.util import format_time, icontheme from picard.mbxml import artist_credit_from_node @@ -48,6 +48,35 @@ class TracksTable(QtGui.QTableWidget): QtGui.QHeaderView.Interactive) +class SearchBox(QtGui.QWidget): + + def __init__(self, parent): + self.parent = parent + QtGui.QWidget.__init__(self, parent) + self.search_action = QtGui.QAction(icontheme.lookup('system-search'), + _(u"Search"), self) + self.search_action.triggered.connect(self.search) + self.setupUi() + + def setupUi(self): + self.setMaximumHeight(35) + layout = QtGui.QHBoxLayout(self) + layout.setMargin(1) + layout.setSpacing(1) + self.search_edit = ButtonLineEdit(self) + layout.addWidget(self.search_edit) + self.search_button = QtGui.QToolButton(self) + self.search_button.setAutoRaise(True) + self.search_button.setDefaultAction(self.search_action) + self.search_button.setIconSize(QtCore.QSize(22, 22)) + layout.addWidget(self.search_button) + self.setLayout(layout) + + def search(self): + text = self.search_edit.text() + self.parent.search(text) + + class SearchDialog(PicardDialog): options = [ @@ -89,6 +118,8 @@ class SearchDialog(PicardDialog): self.verticalLayout = QtGui.QVBoxLayout(self) self.verticalLayout.setObjectName(_("verticalLayout")) + self.search_box = SearchBox(self) + self.verticalLayout.addWidget(self.search_box) self.buttonBox = QtGui.QDialogButtonBox(self) self.buttonBox.addButton( StandardButton(StandardButton.OK), From c0caa037bb3ca1fd8e104d015a998e7cfc393918 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sun, 19 Jun 2016 12:53:55 +0530 Subject: [PATCH 32/98] Better updation of results space By "result space" I mean the area where feedback to user is shown , i.e. progress, error and search results. As it is updated frequently, rather than inserting and deleting widgets directly into main layout, i.e. verticalLayout, updated a dummy widget (i.e. center_widget), instead. This helps in two ways: * Specifying widget properties for this one only. * Layout remains unaffected. --- picard/ui/searchdialog.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index ee90dbb4b..335e20d5b 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -120,6 +120,10 @@ class SearchDialog(PicardDialog): self.search_box = SearchBox(self) self.verticalLayout.addWidget(self.search_box) + self.center_widget = QtGui.QWidget(self) + self.center_layout = QtGui.QVBoxLayout(self.center_widget) + self.center_widget.setLayout(self.center_layout) + self.verticalLayout.addWidget(self.center_widget) self.buttonBox = QtGui.QDialogButtonBox(self) self.buttonBox.addButton( StandardButton(StandardButton.OK), @@ -131,6 +135,12 @@ class SearchDialog(PicardDialog): self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) + def add_widget_to_center_layout(self, widget): + wid = self.center_layout.itemAt(0) + if wid: + wid.widget().deleteLater() + self.center_layout.addWidget(widget) + def show_progress(self): self.progress_widget = QtGui.QWidget(self) layout = QtGui.QVBoxLayout(self.progress_widget) @@ -145,23 +155,19 @@ class SearchDialog(PicardDialog): layout.addWidget(gif_label) layout.setMargin(1) self.progress_widget.setLayout(layout) - self.verticalLayout.insertWidget(0, self.progress_widget) + self.add_widget_to_center_layout(self.progress_widget) def show_table(self): self.tracksTable = TracksTable() self.tracksTable.cellDoubleClicked.connect(self.track_double_clicked) - self.verticalLayout.removeWidget(self.progress_widget) - self.progress_widget.deleteLater() - self.verticalLayout.insertWidget(0, self.tracksTable) self.restore_table_header_state() + self.add_widget_to_center_layout(self.tracksTable) def show_error(self, error): self.error_widget = QtGui.QLabel(_("" + error + "")) self.error_widget.setAlignment(QtCore.Qt.AlignCenter) self.error_widget.setWordWrap(True) - self.verticalLayout.removeWidget(self.progress_widget) - self.progress_widget.deleteLater() - self.verticalLayout.insertWidget(0, self.error_widget) + self.add_widget_to_center_layout(self.error_widget) def load_selection(self, row=None): track_id, release_id, rg_id = self.search_results[row][:3] From 4528f489a3f1dca9d7d9d35cef1d70df6e83a30c Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sun, 19 Jun 2016 19:42:18 +0530 Subject: [PATCH 33/98] Clear search results list before reusing --- picard/ui/searchdialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 335e20d5b..169684d4f 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -264,6 +264,7 @@ class SearchDialog(PicardDialog): key=itemgetter(0)) tracks = [item[3] for item in tmp] + del self.search_results[:] # Clear existing data self.parse_tracks(tracks) self.display_results() From c2da1026d7843055ec41f4f4397da6e7b17c80ca Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 21 Jun 2016 00:21:42 +0530 Subject: [PATCH 34/98] Support advanced query syntax Handle query searches differently from lookups. This would allow to use the advanced query syntax. --- picard/ui/searchdialog.py | 8 +++++--- picard/webservice.py | 27 +++++++++++++++++++-------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 169684d4f..2368be55b 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -93,11 +93,13 @@ class SearchDialog(PicardDialog): self.setupUi() self.restore_window_state() - def search(self, query): + def search(self, text): self.show_progress() self.tagger.xmlws.find_tracks(self.handle_reply, - track=query, - dismax="true", + query=text, + dismax=True, + search=True, + adv=config.setting["use_adv_search_syntax"], limit=25) def load_similar_tracks(self, file_): diff --git a/picard/webservice.py b/picard/webservice.py index a3ea6b124..fb8ae1e6c 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -459,15 +459,26 @@ class XmlWebService(QtCore.QObject): port = config.setting["server_port"] filters = [] query = [] - for name, value in kwargs.items(): - if name in ('limit', 'dismax'): - filters.append((name, str(value))) + if kwargs.get("search"): + # Only for text searches. Auto lookup is handled by 'else' case. + if kwargs["adv"]: + # Lucene search syntax is enabled by default for /ws/2 requests. + # Simply pass the query if user has enabled advanced query syntax. + filters.append(("query", kwargs["query"])) else: - value = _escape_lucene_query(value).strip().lower() - if value: - query.append('%s:(%s)' % (name, value)) - if query: - filters.append(('query', ' '.join(query))) + value = _escape_lucene_query(kwargs["query"]).strip().lower() + filters.append(("query", value)) + filters.append(("dismax", kwargs["dismax"])) + else: + for name, value in kwargs.items(): + if name in ('limit', 'dismax'): + filters.append((name, str(value))) + else: + value = _escape_lucene_query(value).strip().lower() + if value: + query.append('%s:(%s)' % (name, value)) + if query: + filters.append(('query', ' '.join(query))) queryargs = {} for name, value in filters: value = QUrl.toPercentEncoding(unicode(value)) From a8fa61d5d43a23b8da33ac4161d72746998bf7af Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 21 Jun 2016 17:01:36 +0530 Subject: [PATCH 35/98] Update status tip for 'Display more results' --- picard/ui/mainwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 6701c6964..51465d229 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -383,7 +383,7 @@ class MainWindow(QtGui.QMainWindow): self.browser_lookup_action.triggered.connect(self.browser_lookup) self.more_results_action = QtGui.QAction(_(u"Display more results"), self) - self.more_results_action.setStatusTip(_(u"Display more results")) + self.more_results_action.setStatusTip(_(u"View similar tracks and optionally choose a different release")) self.more_results_action.triggered.connect(self.show_more_results) self.show_file_browser_action = QtGui.QAction(_(u"File &Browser"), self) From a4437a4d748c520513d03cf922b25fcb0cd68429 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 21 Jun 2016 17:02:11 +0530 Subject: [PATCH 36/98] Fix missing limit filter for searches And avoid using dismax filter for lookups. --- picard/webservice.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/picard/webservice.py b/picard/webservice.py index fb8ae1e6c..45f1e58b2 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -469,9 +469,11 @@ class XmlWebService(QtCore.QObject): value = _escape_lucene_query(kwargs["query"]).strip().lower() filters.append(("query", value)) filters.append(("dismax", kwargs["dismax"])) + if kwargs.get("limit"): + filters.append("limit", kwargs["limit"]) else: for name, value in kwargs.items(): - if name in ('limit', 'dismax'): + if name in ('limit'): filters.append((name, str(value))) else: value = _escape_lucene_query(value).strip().lower() From 0b1079791e1bdac9dc758069ba39e917a6abb846 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 21 Jun 2016 17:04:38 +0530 Subject: [PATCH 37/98] Minor code style fixes --- picard/ui/searchdialog.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 2368be55b..3bc494ff4 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -31,8 +31,15 @@ class TracksTable(QtGui.QTableWidget): def __init__(self, parent=None): QtGui.QTableWidget.__init__(self, 0, 7) - self.setHorizontalHeaderLabels([_("Name"), _("Length"), - _("Artist"), _("Release"), _("Date"), _("Country"), _("Type")]) + self.setHorizontalHeaderLabels([ + _("Name"), + _("Length"), + _("Artist"), + _("Release"), + _("Date"), + _("Country"), + _("Type") + ]) self.setSelectionMode( QtGui.QAbstractItemView.SingleSelection) @@ -73,8 +80,7 @@ class SearchBox(QtGui.QWidget): self.setLayout(layout) def search(self): - text = self.search_edit.text() - self.parent.search(text) + self.parent.search(self.search_edit.text()) class SearchDialog(PicardDialog): @@ -198,10 +204,10 @@ class SearchDialog(PicardDialog): sel_row = sel_rows[0].row() self.load_selection(sel_row) self.save_state(True) - QtGui.QDialog.accept(self) except AttributeError: self.save_state(False) - QtGui.QDialog.accept(self) + + QtGui.QDialog.accept(self) def parse_tracks(self, tracks): for track in tracks: From 0e58f0383ace2f5f10e123888c15009b47389204 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 21 Jun 2016 20:37:13 +0530 Subject: [PATCH 38/98] Need to append tuple to filter --- picard/webservice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/webservice.py b/picard/webservice.py index 45f1e58b2..bdccf0a18 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -470,7 +470,7 @@ class XmlWebService(QtCore.QObject): filters.append(("query", value)) filters.append(("dismax", kwargs["dismax"])) if kwargs.get("limit"): - filters.append("limit", kwargs["limit"]) + filters.append(("limit", kwargs["limit"])) else: for name, value in kwargs.items(): if name in ('limit'): From 3051e8e6bcefd5cbb29432e7ce540450f507497a Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 22 Jun 2016 02:20:04 +0530 Subject: [PATCH 39/98] Refactor code for handling search requests Some noticeable points: * No need to separate code for auto lookups and searches. Only the dismax filter setting and escaping lucene syntax part is additional in searches. Rest is same for both. * No need to pass advance query setting as argument. It can be retrieved directly. --- picard/ui/searchdialog.py | 4 +--- picard/webservice.py | 38 ++++++++++++++++++-------------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 3bc494ff4..d475c1d5a 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -102,10 +102,8 @@ class SearchDialog(PicardDialog): def search(self, text): self.show_progress() self.tagger.xmlws.find_tracks(self.handle_reply, - query=text, - dismax=True, + track=text, search=True, - adv=config.setting["use_adv_search_syntax"], limit=25) def load_similar_tracks(self, file_): diff --git a/picard/webservice.py b/picard/webservice.py index bdccf0a18..90dc995eb 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -459,28 +459,26 @@ class XmlWebService(QtCore.QObject): port = config.setting["server_port"] filters = [] query = [] - if kwargs.get("search"): - # Only for text searches. Auto lookup is handled by 'else' case. - if kwargs["adv"]: - # Lucene search syntax is enabled by default for /ws/2 requests. - # Simply pass the query if user has enabled advanced query syntax. - filters.append(("query", kwargs["query"])) + escape_lucene_query = True + + is_search = kwargs.pop("search", False) + if is_search: + if config.setting["use_adv_search_syntax"]: + escape_lucene_query = False else: - value = _escape_lucene_query(kwargs["query"]).strip().lower() - filters.append(("query", value)) - filters.append(("dismax", kwargs["dismax"])) - if kwargs.get("limit"): - filters.append(("limit", kwargs["limit"])) - else: - for name, value in kwargs.items(): - if name in ('limit'): - filters.append((name, str(value))) - else: + filters.append(('dismax', 'true')) + + for name, value in kwargs.items(): + if name in ('limit'): + filters.append((name, str(value))) + else: + if escape_lucene_query: value = _escape_lucene_query(value).strip().lower() - if value: - query.append('%s:(%s)' % (name, value)) - if query: - filters.append(('query', ' '.join(query))) + if value: + query.append('%s:(%s)' % (name, value)) + if query: + filters.append(('query', ' '.join(query))) + queryargs = {} for name, value in filters: value = QUrl.toPercentEncoding(unicode(value)) From 4ca5dd84b3a8d0aaed6f6e6caf078419a1dd77c4 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 23 Jun 2016 00:31:43 +0530 Subject: [PATCH 40/98] Fix missing space --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index d475c1d5a..0ce053e0b 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -252,7 +252,7 @@ class SearchDialog(PicardDialog): def handle_reply(self, document, http, error): if error: - error_msg = _("Unable to fetch results. Close the dialog and try" + error_msg = _("Unable to fetch results. Close the dialog and try " "again. See debug logs for more details.") self.show_error(error_msg) return From 2b790d94c1b5210ae9bb71e3db0a83acc9f2c247 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 23 Jun 2016 23:12:45 +0530 Subject: [PATCH 41/98] Move track properties in a separate class And some renaming of functions and variables. --- picard/ui/searchdialog.py | 74 ++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 0ce053e0b..d68436ccd 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -27,6 +27,21 @@ from picard.util import format_time, icontheme from picard.mbxml import artist_credit_from_node +class Track(object): + + def __init__(self, **kwargs): + self.id = kwargs.get("track_id") + self.release_id = kwargs.get("release_id") + self.rg_id = kwargs.get("rg_id") + self.title = kwargs.get("title") + self.length = kwargs.get("length") + self.release = kwargs.get("release") + self.artist = kwargs.get("artist") + self.date = kwargs.get("date") + self.country = kwargs.get("country") + self.release_type = kwargs.get("release_type") + + class TracksTable(QtGui.QTableWidget): def __init__(self, parent=None): @@ -176,20 +191,20 @@ class SearchDialog(PicardDialog): self.add_widget_to_center_layout(self.error_widget) def load_selection(self, row=None): - track_id, release_id, rg_id = self.search_results[row][:3] - if release_id: - self.tagger.get_release_group_by_id(rg_id).loaded_albums.add( - release_id) + track = self.search_results[row] + if track.release_id: + self.tagger.get_release_group_by_id(track.rg_id).loaded_albums.add( + track.release_id) if self.file_: album = self.file_.parent.album - self.tagger.move_file_to_track(self.file_, release_id, track_id) + self.tagger.move_file_to_track(self.file_, track.release_id, track.track_id) if album._files == 0: # Remove album if the selected file was the only one in album # Compared to 0 because file has already moved to another album # by move_file_to_track self.tagger.remove_album(album) else: - self.tagger.load_album(release_id) + self.tagger.load_album(track.release_id) def track_double_clicked(self, row): self.load_selection(row) @@ -207,17 +222,17 @@ class SearchDialog(PicardDialog): QtGui.QDialog.accept(self) - def parse_tracks(self, tracks): - for track in tracks: - rec_id = track.id - rec_title = track.title[0].text - artist = artist_credit_from_node(track.artist_credit[0])[0] + def parse_tracks_from_xml(self, tracks_xml): + for obj in tracks_xml: + rec_id = obj.id + rec_title = obj.title[0].text + artist = artist_credit_from_node(obj.artist_credit[0])[0] try: - length = format_time(track.length[0].text) + length = format_time(obj.length[0].text) except AttributeError: length = "" try: - releases = track.release_list[0].release + releases = obj.release_list[0].release except AttributeError: pass if releases: @@ -242,13 +257,15 @@ class SearchDialog(PicardDialog): types_list.append(sec.secondary_type[0].text) types = "+".join(types_list) - result = (rec_id, rel_id, rg_id, rec_title, artist, length, - rel_title, date, country, types) - self.search_results.append(result) + track = Track(id=rec_id, release_id=rel_id, rg_id=rg_id, + title=rec_title, artist=artist, length=length, + release=rel_title, date=date, country=country, + release_type=types) + self.search_results.append(track) else: - result = (rec_id, "", "", rec_title, artist, length, "", "", "", - "") - self.search_results.append(result) + track = Track(id=rec_id, artist=artist, length=length, + title=rec_title) + self.search_results.append(track) def handle_reply(self, document, http, error): if error: @@ -271,22 +288,21 @@ class SearchDialog(PicardDialog): tracks = [item[3] for item in tmp] del self.search_results[:] # Clear existing data - self.parse_tracks(tracks) + self.parse_tracks_from_xml(tracks) self.display_results() def display_results(self): self.show_table() - for row, tup in enumerate(self.search_results): - title, artist, length, release, date, country, type = tup[3:] + for row, track in enumerate(self.search_results): table_item = QtGui.QTableWidgetItem self.tracksTable.insertRow(row) - self.tracksTable.setItem(row, 0, table_item(title)) - self.tracksTable.setItem(row, 1, table_item(length)) - self.tracksTable.setItem(row, 2, table_item(artist)) - self.tracksTable.setItem(row, 3, table_item(release)) - self.tracksTable.setItem(row, 4, table_item(date)) - self.tracksTable.setItem(row, 5, table_item(country)) - self.tracksTable.setItem(row, 6, table_item(type)) + self.tracksTable.setItem(row, 0, table_item(track.title)) + self.tracksTable.setItem(row, 1, table_item(track.length)) + self.tracksTable.setItem(row, 2, table_item(track.artist)) + self.tracksTable.setItem(row, 3, table_item(track.release)) + self.tracksTable.setItem(row, 4, table_item(track.date)) + self.tracksTable.setItem(row, 5, table_item(track.country)) + self.tracksTable.setItem(row, 6, table_item(track.release_type)) def restore_window_state(self): size = config.persist["searchdialog_window_size"] From b3ee89835a3a0005b29525ae77c01ba754db2c71 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 27 Jun 2016 23:47:54 +0530 Subject: [PATCH 42/98] Restructure some code Reason being, I'm planning to add two more builtin search dialogs. One for album and one for artist. It makes more sense to move (possibly) common code to a separate class. --- picard/ui/mainwindow.py | 13 ++- picard/ui/searchdialog.py | 233 +++++++++++++++++++------------------- 2 files changed, 126 insertions(+), 120 deletions(-) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 51465d229..a07a069ff 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -34,7 +34,7 @@ from picard.ui.filebrowser import FileBrowser from picard.ui.tagsfromfilenames import TagsFromFileNamesDialog from picard.ui.options.dialog import OptionsDialog from picard.ui.infodialog import FileInfoDialog, AlbumInfoDialog, ClusterInfoDialog -from picard.ui.searchdialog import SearchDialog +from picard.ui.searchdialog import TrackSearchDialog from picard.ui.infostatus import InfoStatus from picard.ui.passworddialog import PasswordDialog from picard.ui.logview import LogView, HistoryView @@ -679,10 +679,11 @@ class MainWindow(QtGui.QMainWindow): """Search for album, artist or track on the MusicBrainz website.""" text = self.search_edit.text() type = self.search_combo.itemData(self.search_combo.currentIndex()) - if config.setting["builtin_search"] and type == "track": - dialog = SearchDialog(self) - dialog.search(text) - dialog.exec_() + if config.setting["builtin_search"]: + if type == "track": + dialog = TrackSearchDialog(self) + dialog.search(text) + dialog.exec_() else: self.tagger.search(text, type, config.setting["use_adv_search_syntax"]) @@ -805,7 +806,7 @@ class MainWindow(QtGui.QMainWindow): obj = self.selected_objects[0] if isinstance(obj, Track): obj = obj.linked_files[0] - dialog = SearchDialog(self) + dialog = TrackSearchDialog(self) dialog.load_similar_tracks(obj) dialog.exec_() diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index d68436ccd..261a9168f 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -42,20 +42,11 @@ class Track(object): self.release_type = kwargs.get("release_type") -class TracksTable(QtGui.QTableWidget): - - def __init__(self, parent=None): - QtGui.QTableWidget.__init__(self, 0, 7) - self.setHorizontalHeaderLabels([ - _("Name"), - _("Length"), - _("Artist"), - _("Release"), - _("Date"), - _("Country"), - _("Type") - ]) +class ResultTable(QtGui.QTableWidget): + def __init__(self, column_titles): + QtGui.QTableWidget.__init__(self, 0, len(column_titles)) + self.setHorizontalHeaderLabels(column_titles) self.setSelectionMode( QtGui.QAbstractItemView.SingleSelection) self.setSelectionBehavior( @@ -107,34 +98,11 @@ class SearchDialog(PicardDialog): def __init__(self, parent=None): PicardDialog.__init__(self, parent) - self.setObjectName(_("SearchDialog")) - self.setWindowTitle(_("Track Search Results")) self.file_ = None self.search_results = [] self.setupUi() self.restore_window_state() - def search(self, text): - self.show_progress() - self.tagger.xmlws.find_tracks(self.handle_reply, - track=text, - search=True, - limit=25) - - def load_similar_tracks(self, file_): - self.file_ = file_ - metadata = file_.orig_metadata - self.show_progress() - self.tagger.xmlws.find_tracks(self.handle_reply, - track=metadata['title'], - artist=metadata['artist'], - release=metadata['tracknumber'], - tnum=metadata['totaltracks'], - tracks=metadata['totaltracks'], - qdur=str(metadata.length / 2000), - isrc=metadata['isrc'], - limit=25) - def setupUi(self): self.verticalLayout = QtGui.QVBoxLayout(self) self.verticalLayout.setObjectName(_("verticalLayout")) @@ -178,41 +146,19 @@ class SearchDialog(PicardDialog): self.progress_widget.setLayout(layout) self.add_widget_to_center_layout(self.progress_widget) - def show_table(self): - self.tracksTable = TracksTable() - self.tracksTable.cellDoubleClicked.connect(self.track_double_clicked) - self.restore_table_header_state() - self.add_widget_to_center_layout(self.tracksTable) - def show_error(self, error): self.error_widget = QtGui.QLabel(_("" + error + "")) self.error_widget.setAlignment(QtCore.Qt.AlignCenter) self.error_widget.setWordWrap(True) self.add_widget_to_center_layout(self.error_widget) - def load_selection(self, row=None): - track = self.search_results[row] - if track.release_id: - self.tagger.get_release_group_by_id(track.rg_id).loaded_albums.add( - track.release_id) - if self.file_: - album = self.file_.parent.album - self.tagger.move_file_to_track(self.file_, track.release_id, track.track_id) - if album._files == 0: - # Remove album if the selected file was the only one in album - # Compared to 0 because file has already moved to another album - # by move_file_to_track - self.tagger.remove_album(album) - else: - self.tagger.load_album(track.release_id) - - def track_double_clicked(self, row): + def row_double_clicked(self, row): self.load_selection(row) self.accept() def accept(self): try: - sel_rows = self.tracksTable.selectionModel().selectedRows() + sel_rows = self.table.selectionModel().selectedRows() if sel_rows: sel_row = sel_rows[0].row() self.load_selection(sel_row) @@ -222,6 +168,104 @@ class SearchDialog(PicardDialog): QtGui.QDialog.accept(self) + def restore_window_state(self): + size = config.persist["searchdialog_window_size"] + if size: + self.resize(size) + + def restore_table_header_state(self): + header = self.table.horizontalHeader() + state = config.persist["searchdialog_header_state"] + if state: + header.restoreState(state) + header.setResizeMode(QtGui.QHeaderView.Interactive) + + def save_state(self, table_loaded=True): + if table_loaded: + header = self.table.horizontalHeader() + config.persist["searchdialog_header_state"] = header.saveState() + config.persist["searchdialog_window_size"] = self.size() + + +class TrackSearchDialog(SearchDialog): + + def __init__(self, parent): + super(TrackSearchDialog, self).__init__(parent) + self.setWindowTitle(_("Track Search Results")) + self.table_headers = [ + _("Name"), + _("Length"), + _("Artist"), + _("Release"), + _("Date"), + _("Country"), + _("Type") + ] + + def search(self, text): + self.show_progress() + self.tagger.xmlws.find_tracks(self.handle_reply, + track=text, + search=True, + limit=25) + + def load_similar_tracks(self, file_): + self.file_ = file_ + metadata = file_.orig_metadata + self.show_progress() + self.tagger.xmlws.find_tracks(self.handle_reply, + track=metadata['title'], + artist=metadata['artist'], + release=metadata['tracknumber'], + tnum=metadata['totaltracks'], + tracks=metadata['totaltracks'], + qdur=str(metadata.length / 2000), + isrc=metadata['isrc'], + limit=25) + + def handle_reply(self, document, http, error): + if error: + error_msg = _("Unable to fetch results. Close the dialog and try " + "again. See debug logs for more details.") + self.show_error(error_msg) + return + + try: + tracks = document.metadata[0].recording_list[0].recording + except (AttributeError, IndexError): + error_msg = _("No results found. Please try a different search query.") + self.show_error(error_msg) + return + + if self.file_: + tmp = sorted((self.file_.orig_metadata.compare_to_track(track, + File.comparison_weights) for track in tracks), reverse=True, + key=itemgetter(0)) + tracks = [item[3] for item in tmp] + + del self.search_results[:] # Clear existing data + self.parse_tracks_from_xml(tracks) + self.display_results() + + def show_table(self): + self.table = ResultTable(self.table_headers) + self.table.cellDoubleClicked.connect(self.row_double_clicked) + self.restore_table_header_state() + self.add_widget_to_center_layout(self.table) + + def display_results(self): + self.show_table() + for row, track in enumerate(self.search_results): + table_item = QtGui.QTableWidgetItem + self.table.insertRow(row) + self.table.setItem(row, 0, table_item(track.title)) + self.table.setItem(row, 1, table_item(track.length)) + self.table.setItem(row, 2, table_item(track.artist)) + self.table.setItem(row, 3, table_item(track.release)) + self.table.setItem(row, 4, table_item(track.date)) + self.table.setItem(row, 5, table_item(track.country)) + self.table.setItem(row, 6, table_item(track.release_type)) + def parse_tracks_from_xml(self, tracks_xml): for obj in tracks_xml: rec_id = obj.id @@ -267,57 +311,18 @@ class SearchDialog(PicardDialog): title=rec_title) self.search_results.append(track) - def handle_reply(self, document, http, error): - if error: - error_msg = _("Unable to fetch results. Close the dialog and try " - "again. See debug logs for more details.") - self.show_error(error_msg) - return - - try: - tracks = document.metadata[0].recording_list[0].recording - except (AttributeError, IndexError): - error_msg = _("No results found. Please try a different search query.") - self.show_error(error_msg) - return - - if self.file_: - tmp = sorted((self.file_.orig_metadata.compare_to_track(track, - File.comparison_weights) for track in tracks), reverse=True, - key=itemgetter(0)) - tracks = [item[3] for item in tmp] - - del self.search_results[:] # Clear existing data - self.parse_tracks_from_xml(tracks) - self.display_results() - - def display_results(self): - self.show_table() - for row, track in enumerate(self.search_results): - table_item = QtGui.QTableWidgetItem - self.tracksTable.insertRow(row) - self.tracksTable.setItem(row, 0, table_item(track.title)) - self.tracksTable.setItem(row, 1, table_item(track.length)) - self.tracksTable.setItem(row, 2, table_item(track.artist)) - self.tracksTable.setItem(row, 3, table_item(track.release)) - self.tracksTable.setItem(row, 4, table_item(track.date)) - self.tracksTable.setItem(row, 5, table_item(track.country)) - self.tracksTable.setItem(row, 6, table_item(track.release_type)) - - def restore_window_state(self): - size = config.persist["searchdialog_window_size"] - if size: - self.resize(size) - - def restore_table_header_state(self): - header = self.tracksTable.horizontalHeader() - state = config.persist["searchdialog_header_state"] - if state: - header.restoreState(state) - header.setResizeMode(QtGui.QHeaderView.Interactive) - - def save_state(self, table_loaded=True): - if table_loaded: - header = self.tracksTable.horizontalHeader() - config.persist["searchdialog_header_state"] = header.saveState() - config.persist["searchdialog_window_size"] = self.size() + def load_selection(self, row=None): + track = self.search_results[row] + if track.release_id: + self.tagger.get_release_group_by_id(track.rg_id).loaded_albums.add( + track.release_id) + if self.file_: + album = self.file_.parent.album + self.tagger.move_file_to_track(self.file_, track.release_id, track.track_id) + if album._files == 0: + # Remove album if the selected file was the only one in album + # Compared to 0 because file has already moved to another album + # by move_file_to_track + self.tagger.remove_album(album) + else: + self.tagger.load_album(track.release_id) From 1c0550f58a8a0d014456a805fbf3b39af6a13644 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 28 Jun 2016 19:50:08 +0530 Subject: [PATCH 43/98] Add support for NATs To load an NAT, track's xml node is required. So I've created a tuple wth track's xml node alongside with a Track(local) object, and appended them to the search_results list. --- picard/ui/searchdialog.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 261a9168f..78a210481 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -255,7 +255,8 @@ class TrackSearchDialog(SearchDialog): def display_results(self): self.show_table() - for row, track in enumerate(self.search_results): + for row, obj in enumerate(self.search_results): + track = obj[0] table_item = QtGui.QTableWidgetItem self.table.insertRow(row) self.table.setItem(row, 0, table_item(track.title)) @@ -305,14 +306,14 @@ class TrackSearchDialog(SearchDialog): title=rec_title, artist=artist, length=length, release=rel_title, date=date, country=country, release_type=types) - self.search_results.append(track) + self.search_results.append((track, node)) else: track = Track(id=rec_id, artist=artist, length=length, - title=rec_title) - self.search_results.append(track) + title=rec_title, release="(Standalone Recording)") + self.search_results.append((track, node)) def load_selection(self, row=None): - track = self.search_results[row] + track, node = self.search_results[row] if track.release_id: self.tagger.get_release_group_by_id(track.rg_id).loaded_albums.add( track.release_id) @@ -326,3 +327,11 @@ class TrackSearchDialog(SearchDialog): self.tagger.remove_album(album) else: self.tagger.load_album(track.release_id) + else: + if self.file_: + album = self.file_.parent.album + self.tagger.move_file_to_nat(track.id) + if album._files == 0: + self.tagger.remove_album(album) + else: + self.tagger.load_nat(track.id, node) From a4b11864530ff9d91d17cf18e10728b792332811 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 28 Jun 2016 19:56:23 +0530 Subject: [PATCH 44/98] More precise (& compact) comment --- picard/ui/searchdialog.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 78a210481..6041f4844 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -321,9 +321,7 @@ class TrackSearchDialog(SearchDialog): album = self.file_.parent.album self.tagger.move_file_to_track(self.file_, track.release_id, track.track_id) if album._files == 0: - # Remove album if the selected file was the only one in album - # Compared to 0 because file has already moved to another album - # by move_file_to_track + # Remove album if it has no more files associated self.tagger.remove_album(album) else: self.tagger.load_album(track.release_id) From 39189df8350cfb9f381e61c338335f0a729c9b62 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 28 Jun 2016 19:57:51 +0530 Subject: [PATCH 45/98] Rename obj->node --- picard/ui/searchdialog.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 6041f4844..1daacd4a5 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -268,18 +268,16 @@ class TrackSearchDialog(SearchDialog): self.table.setItem(row, 6, table_item(track.release_type)) def parse_tracks_from_xml(self, tracks_xml): - for obj in tracks_xml: - rec_id = obj.id - rec_title = obj.title[0].text - artist = artist_credit_from_node(obj.artist_credit[0])[0] + for node in tracks_xml: + rec_id = node.id + rec_title = node.title[0].text + artist = artist_credit_from_node(node.artist_credit[0])[0] try: - length = format_time(obj.length[0].text) + length = format_time(node.length[0].text) except AttributeError: length = "" try: - releases = obj.release_list[0].release - except AttributeError: - pass + releases = node.release_list[0].release if releases: for release in releases: rel_id = release.id From 6038c19874dbb82fe172f5cf7a3625bb71d47227 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 28 Jun 2016 19:59:13 +0530 Subject: [PATCH 46/98] Unnecessary if/else logic --- picard/ui/searchdialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 1daacd4a5..d3875838f 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -278,7 +278,6 @@ class TrackSearchDialog(SearchDialog): length = "" try: releases = node.release_list[0].release - if releases: for release in releases: rel_id = release.id rel_title = release.title[0].text @@ -305,7 +304,8 @@ class TrackSearchDialog(SearchDialog): release=rel_title, date=date, country=country, release_type=types) self.search_results.append((track, node)) - else: + + except AttributeError: track = Track(id=rec_id, artist=artist, length=length, title=rec_title, release="(Standalone Recording)") self.search_results.append((track, node)) From e7a03cb9d1082247717887330f1dd5d19f827fcb Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 29 Jun 2016 16:54:46 +0530 Subject: [PATCH 47/98] Replace loader gif with a transparent one --- picard/resources.py | 22 +++++++++++----------- resources/images/loader.gif | Bin 673 -> 673 bytes 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/picard/resources.py b/picard/resources.py index d5da1502d..cb58e55e2 100644 --- a/picard/resources.py +++ b/picard/resources.py @@ -1266,42 +1266,42 @@ qt_resource_data = "\ \x47\ \x49\x46\x38\x39\x61\x10\x00\x10\x00\xf2\x00\x00\xff\xff\xff\x00\ \x00\x00\xc2\xc2\xc2\x42\x42\x42\x00\x00\x00\x62\x62\x62\x82\x82\ -\x82\x92\x92\x92\x21\xfe\x1a\x43\x72\x65\x61\x74\x65\x64\x20\x77\ -\x69\x74\x68\x20\x61\x6a\x61\x78\x6c\x6f\x61\x64\x2e\x69\x6e\x66\ -\x6f\x00\x21\xf9\x04\x00\x0a\x00\x00\x00\x21\xff\x0b\x4e\x45\x54\ -\x53\x43\x41\x50\x45\x32\x2e\x30\x03\x01\x00\x00\x00\x2c\x00\x00\ +\x82\x92\x92\x92\x21\xff\x0b\x4e\x45\x54\x53\x43\x41\x50\x45\x32\ +\x2e\x30\x03\x01\x00\x00\x00\x21\xfe\x1a\x43\x72\x65\x61\x74\x65\ +\x64\x20\x77\x69\x74\x68\x20\x61\x6a\x61\x78\x6c\x6f\x61\x64\x2e\ +\x69\x6e\x66\x6f\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\ \x00\x00\x10\x00\x10\x00\x00\x03\x33\x08\xba\xdc\xfe\x30\xca\x49\ \x6b\x13\x63\x08\x3a\x08\x19\x9c\x07\x4e\x98\x66\x09\x45\xb1\x31\ \xc2\xba\x14\x99\xc1\xb6\x2e\x60\xc4\xc2\x71\xd0\x2d\x5b\x18\x39\ \xdd\xa6\x07\x39\x18\x0c\x07\x4a\x6b\xe7\x48\x00\x00\x21\xf9\x04\ -\x00\x0a\x00\x01\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\ +\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\ \x34\x08\xba\xdc\xfe\x4e\x8c\x21\x20\x1b\x84\x0c\xbb\xb0\xe6\x8a\ \x44\x71\x42\x51\x54\x60\x31\x19\x20\x60\x4c\x45\x5b\x1a\xa8\x7c\ \x1c\xb5\x75\xdf\xed\x61\x18\x07\x80\x20\xd7\x18\xe2\x86\x43\x19\ -\xb2\x25\x24\x2a\x12\x00\x21\xf9\x04\x00\x0a\x00\x02\x00\x2c\x00\ +\xb2\x25\x24\x2a\x12\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\ \x00\x00\x00\x10\x00\x10\x00\x00\x03\x36\x08\xba\x32\x23\x2b\xca\ \x41\xc8\x90\xcc\x94\x56\x2f\x06\x85\x63\x1c\x0e\xf4\x19\x4e\xf1\ \x49\x42\x61\x98\xab\x70\x1c\xf0\x0a\xcc\xb3\xbd\x1c\xc6\xa8\x2b\ \x02\x59\xed\x17\xfc\x01\x83\xc3\x0f\x32\xa9\x64\x1a\x9f\xbf\x04\ -\x00\x21\xf9\x04\x00\x0a\x00\x03\x00\x2c\x00\x00\x00\x00\x10\x00\ +\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x10\x00\ \x10\x00\x00\x03\x33\x08\xba\x62\x25\x2b\xca\x32\x86\x91\xec\x9c\ \x56\x5f\x85\x8b\xa6\x09\x85\x21\x0c\x04\x31\x44\x87\x61\x1c\x11\ \xaa\x46\x82\xb0\xd1\x1f\x03\x62\x52\x5d\xf3\x3d\x1f\x30\x38\x2c\ -\x1a\x8f\xc8\xa4\x72\x39\x4c\x00\x00\x21\xf9\x04\x00\x0a\x00\x04\ +\x1a\x8f\xc8\xa4\x72\x39\x4c\x00\x00\x21\xf9\x04\x09\x0a\x00\x00\ \x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\x32\x08\xba\x72\ \x27\x2b\x4a\xe7\x64\x14\xf0\x18\xf3\x4c\x81\x0c\x26\x76\xc3\x60\ \x5c\x62\x54\x94\x85\x84\xb9\x1e\x68\x59\x42\x29\xcf\xca\x40\x10\ \x03\x1e\xe9\x3c\x1f\xc3\x26\x2c\x1a\x8f\xc8\xa4\x52\x92\x00\x00\ -\x21\xf9\x04\x00\x0a\x00\x05\x00\x2c\x00\x00\x00\x00\x10\x00\x10\ +\x21\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x10\x00\x10\ \x00\x00\x03\x33\x08\xba\x20\xc2\x90\x39\x17\xe3\x74\xe7\xbc\xda\ \x9e\x30\x19\xc7\x1c\xe0\x21\x2e\x42\xb6\x9d\xca\x57\xac\xa2\x31\ \x0c\x06\x0b\x14\x73\x61\xbb\xb0\x35\xf7\x95\x01\x81\x30\xb0\x09\ -\x89\xbb\x9f\x6d\x29\x4a\x00\x00\x21\xf9\x04\x00\x0a\x00\x06\x00\ +\x89\xbb\x9f\x6d\x29\x4a\x00\x00\x21\xf9\x04\x09\x0a\x00\x00\x00\ \x2c\x00\x00\x00\x00\x10\x00\x10\x00\x00\x03\x32\x08\xba\xdc\xfe\ \xf0\x09\x11\xd9\x9c\x55\x5d\x9a\x01\xee\xda\x71\x70\x95\x60\x88\ \xdd\x61\x9c\xdd\x34\x96\x85\x41\x46\xc5\x30\x14\x90\x60\x9b\xb6\ \x01\x0d\x04\xc2\x40\x10\x9b\x31\x80\xc2\xd6\xce\x91\x00\x00\x21\ -\xf9\x04\x00\x0a\x00\x07\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\ +\xf9\x04\x09\x0a\x00\x00\x00\x2c\x00\x00\x00\x00\x10\x00\x10\x00\ \x00\x03\x32\x08\xba\xdc\xfe\x30\xca\x49\xab\x65\x42\xd4\x9c\x29\ \xd7\x1e\x08\x08\xc3\x20\x8e\xc7\x71\x0e\x04\x31\x30\xa9\xca\xb0\ \xae\x50\x18\xc2\x61\x18\x07\x56\xda\xa5\x02\x20\x75\x62\x18\x82\ diff --git a/resources/images/loader.gif b/resources/images/loader.gif index f2a1bc0c6f545e20e631a96e8e92f9822e75d046..d0bce1542342e912da81a2c260562df172f30d73 100644 GIT binary patch delta 95 zcmZ3;x{!5(n$Ul4Ki808XU70nBRvCVMg|6kiK>cBoLm#5-B>uefV`!y5c-`vn4WCO W=mKFhGCD&TCtxB1Oi+<2lNSKU2NE^_ From e3415bdce6ee02c7dea39dd840ccb92849a45903 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 29 Jun 2016 16:55:09 +0530 Subject: [PATCH 48/98] Move file_ attribute to TrackSearchDialog --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index d3875838f..0c71604fa 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -98,7 +98,6 @@ class SearchDialog(PicardDialog): def __init__(self, parent=None): PicardDialog.__init__(self, parent) - self.file_ = None self.search_results = [] self.setupUi() self.restore_window_state() @@ -191,6 +190,7 @@ class TrackSearchDialog(SearchDialog): def __init__(self, parent): super(TrackSearchDialog, self).__init__(parent) + self.file_ = None self.setWindowTitle(_("Track Search Results")) self.table_headers = [ _("Name"), From cf0c159d712a646652b7ec62a9fb3f94e8c88d38 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 29 Jun 2016 16:55:53 +0530 Subject: [PATCH 49/98] Some code style fixes --- picard/ui/searchdialog.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 0c71604fa..322ec57c2 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -200,7 +200,7 @@ class TrackSearchDialog(SearchDialog): _("Date"), _("Country"), _("Type") - ] + ] def search(self, text): self.show_progress() @@ -213,7 +213,8 @@ class TrackSearchDialog(SearchDialog): self.file_ = file_ metadata = file_.orig_metadata self.show_progress() - self.tagger.xmlws.find_tracks(self.handle_reply, + self.tagger.xmlws.find_tracks( + self.handle_reply, track=metadata['title'], artist=metadata['artist'], release=metadata['tracknumber'], @@ -238,8 +239,9 @@ class TrackSearchDialog(SearchDialog): return if self.file_: - tmp = sorted((self.file_.orig_metadata.compare_to_track(track, - File.comparison_weights) for track in tracks), reverse=True, + tmp = sorted((self.file_.orig_metadata.compare_to_track( + track, File.comparison_weights) for track in tracks), + reverse=True, key=itemgetter(0)) tracks = [item[3] for item in tmp] @@ -299,15 +301,26 @@ class TrackSearchDialog(SearchDialog): types_list.append(sec.secondary_type[0].text) types = "+".join(types_list) - track = Track(id=rec_id, release_id=rel_id, rg_id=rg_id, - title=rec_title, artist=artist, length=length, - release=rel_title, date=date, country=country, + track = Track( + id=rec_id, + release_id=rel_id, + rg_id=rg_id, + title=rec_title, + artist=artist, + length=length, + release=rel_title, + date=date, + country=country, release_type=types) self.search_results.append((track, node)) except AttributeError: - track = Track(id=rec_id, artist=artist, length=length, - title=rec_title, release="(Standalone Recording)") + track = Track( + id=rec_id, + artist=artist, + length=length, + title=rec_title, + release="(Standalone Recording)") self.search_results.append((track, node)) def load_selection(self, row=None): From 4118defc04c13f37dc8410e7464592e62a96a0e5 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 30 Jun 2016 03:17:08 +0530 Subject: [PATCH 50/98] Track object have no track_id attribute --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 322ec57c2..ead2ac915 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -330,10 +330,10 @@ class TrackSearchDialog(SearchDialog): track.release_id) if self.file_: album = self.file_.parent.album - self.tagger.move_file_to_track(self.file_, track.release_id, track.track_id) if album._files == 0: # Remove album if it has no more files associated self.tagger.remove_album(album) + self.tagger.move_file_to_track(self.file_, track.release_id, track.id) else: self.tagger.load_album(track.release_id) else: From 21be45a457921f32587cf41c6abcf8f75c8a0f99 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 30 Jun 2016 03:34:07 +0530 Subject: [PATCH 51/98] Searching similar metadata for unmatched files --- picard/ui/itemviews.py | 3 +-- picard/ui/searchdialog.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index c2a226a70..bacc084fb 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -286,8 +286,7 @@ class BaseTreeView(QtGui.QTreeWidget): menu.addSeparator() menu.addAction(self.window.autotag_action) menu.addAction(self.window.analyze_action) - if isinstance(obj.parent, Track): - menu.addAction(self.window.more_results_action) + menu.addAction(self.window.more_results_action) plugin_actions = list(_file_actions) elif isinstance(obj, Album): if can_view_info: diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index ead2ac915..e58153d79 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -326,17 +326,25 @@ class TrackSearchDialog(SearchDialog): def load_selection(self, row=None): track, node = self.search_results[row] if track.release_id: + # The track is not an NAT self.tagger.get_release_group_by_id(track.rg_id).loaded_albums.add( track.release_id) if self.file_: - album = self.file_.parent.album - if album._files == 0: - # Remove album if it has no more files associated - self.tagger.remove_album(album) - self.tagger.move_file_to_track(self.file_, track.release_id, track.id) + # Search is performed for a file + # Have to move that file from its existing album to the new one + if type(self.file_.parent).__name__ == "Track": + album = self.file_.parent.album + self.tagger.move_file_to_track(self.file_, track.release_id, track.id) + if album._files == 0: + # Remove album if it has no more files associated + self.tagger.remove_album(album) + else: + self.tagger.move_file_to_track(self.file_, track.release_id, track.id) else: + # No files associated. Just a normal search. self.tagger.load_album(track.release_id) else: + # The track is an NAT if self.file_: album = self.file_.parent.album self.tagger.move_file_to_nat(track.id) From f26eb4f0e3b7204dce50c77b92b3376b1cf31923 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 30 Jun 2016 03:36:06 +0530 Subject: [PATCH 52/98] Rename similar tracks search action And add an icon to it. --- picard/ui/mainwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index a07a069ff..8583a7932 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -382,7 +382,7 @@ class MainWindow(QtGui.QMainWindow): self.browser_lookup_action.setEnabled(False) self.browser_lookup_action.triggered.connect(self.browser_lookup) - self.more_results_action = QtGui.QAction(_(u"Display more results"), self) + self.more_results_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search similar tracks..."), self) self.more_results_action.setStatusTip(_(u"View similar tracks and optionally choose a different release")) self.more_results_action.triggered.connect(self.show_more_results) From c66488c96de559a6ab90ed69bf174b4c6b7c67a9 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 1 Jul 2016 00:24:03 +0530 Subject: [PATCH 53/98] Display entered query when opening search dialog --- picard/ui/searchdialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index e58153d79..2b3ecbcf1 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -203,6 +203,7 @@ class TrackSearchDialog(SearchDialog): ] def search(self, text): + self.search_box.search_edit.setText(text) self.show_progress() self.tagger.xmlws.find_tracks(self.handle_reply, track=text, From 32b672440a174098c1e797b3db50d35e07f989de Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 2 Jul 2016 16:53:18 +0530 Subject: [PATCH 54/98] Fix margin of results table --- picard/ui/searchdialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 2b3ecbcf1..299af16ea 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -110,6 +110,7 @@ class SearchDialog(PicardDialog): self.verticalLayout.addWidget(self.search_box) self.center_widget = QtGui.QWidget(self) self.center_layout = QtGui.QVBoxLayout(self.center_widget) + self.center_layout.setMargin(1) self.center_widget.setLayout(self.center_layout) self.verticalLayout.addWidget(self.center_widget) self.buttonBox = QtGui.QDialogButtonBox(self) From 2fc9bf70d2d9132194495720f86ab6f75b903fdc Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 2 Jul 2016 22:21:54 +0530 Subject: [PATCH 55/98] Move show_table method to base class It is supposed to be common among multiple search dialogs. Just pass the table headers as arguments. --- picard/ui/searchdialog.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 299af16ea..29250548a 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -152,6 +152,12 @@ class SearchDialog(PicardDialog): self.error_widget.setWordWrap(True) self.add_widget_to_center_layout(self.error_widget) + def show_table(self, column_headers): + self.table = ResultTable(self.table_headers) + self.table.cellDoubleClicked.connect(self.row_double_clicked) + self.restore_table_header_state() + self.add_widget_to_center_layout(self.table) + def row_double_clicked(self, row): self.load_selection(row) self.accept() @@ -251,14 +257,8 @@ class TrackSearchDialog(SearchDialog): self.parse_tracks_from_xml(tracks) self.display_results() - def show_table(self): - self.table = ResultTable(self.table_headers) - self.table.cellDoubleClicked.connect(self.row_double_clicked) - self.restore_table_header_state() - self.add_widget_to_center_layout(self.table) - def display_results(self): - self.show_table() + self.show_table(self.table_headers) for row, obj in enumerate(self.search_results): track = obj[0] table_item = QtGui.QTableWidgetItem From 8aaea54c8ecccd9709bb282338ce5f584ab352cd Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 2 Jul 2016 23:24:15 +0530 Subject: [PATCH 56/98] Rename loading button and enable only on select --- picard/ui/searchdialog.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 29250548a..e7b1ddf91 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -114,8 +114,10 @@ class SearchDialog(PicardDialog): self.center_widget.setLayout(self.center_layout) self.verticalLayout.addWidget(self.center_widget) self.buttonBox = QtGui.QDialogButtonBox(self) + self.load_button = QtGui.QPushButton("Load into Picard") + self.load_button.setEnabled(False) self.buttonBox.addButton( - StandardButton(StandardButton.OK), + self.load_button, QtGui.QDialogButtonBox.AcceptRole) self.buttonBox.addButton( StandardButton(StandardButton.CANCEL), @@ -157,6 +159,10 @@ class SearchDialog(PicardDialog): self.table.cellDoubleClicked.connect(self.row_double_clicked) self.restore_table_header_state() self.add_widget_to_center_layout(self.table) + def enable_loading_button(): + self.load_button.setEnabled(True) + self.table.itemSelectionChanged.connect( + enable_loading_button) def row_double_clicked(self, row): self.load_selection(row) From 022f7fedaab82a0e34a213f7b26f7e2f74864d65 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sun, 3 Jul 2016 00:17:58 +0530 Subject: [PATCH 57/98] Make button title translatable --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index e7b1ddf91..483b88028 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -114,7 +114,7 @@ class SearchDialog(PicardDialog): self.center_widget.setLayout(self.center_layout) self.verticalLayout.addWidget(self.center_widget) self.buttonBox = QtGui.QDialogButtonBox(self) - self.load_button = QtGui.QPushButton("Load into Picard") + self.load_button = QtGui.QPushButton(_("Load into Picard")) self.load_button.setEnabled(False) self.buttonBox.addButton( self.load_button, From dc2827efac0926c56a99110472a6ca959684e3f6 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 4 Jul 2016 22:43:51 +0530 Subject: [PATCH 58/98] No need to use a tuple for single filter Besides it wasn't a tuple, but was intended to be. --- picard/webservice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/webservice.py b/picard/webservice.py index 90dc995eb..5b659035b 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -469,7 +469,7 @@ class XmlWebService(QtCore.QObject): filters.append(('dismax', 'true')) for name, value in kwargs.items(): - if name in ('limit'): + if name == "limit": filters.append((name, str(value))) else: if escape_lucene_query: From b8616176e27509f26a17e77193f68204605c0381 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 4 Jul 2016 22:45:29 +0530 Subject: [PATCH 59/98] Replace try/except with if/else Exception is difficult to follow. Instead use if/else logic. --- picard/ui/searchdialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 483b88028..916a424c2 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -286,7 +286,7 @@ class TrackSearchDialog(SearchDialog): length = format_time(node.length[0].text) except AttributeError: length = "" - try: + if "release_list" in node.children and "release" in node.release_list[0].children: releases = node.release_list[0].release for release in releases: rel_id = release.id @@ -322,7 +322,7 @@ class TrackSearchDialog(SearchDialog): release_type=types) self.search_results.append((track, node)) - except AttributeError: + else: track = Track( id=rec_id, artist=artist, From aab3b11a26871ce686255d5c7948763bdb6ed520 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 4 Jul 2016 22:46:58 +0530 Subject: [PATCH 60/98] Make release title for NAT translatable --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 916a424c2..70cb10138 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -328,7 +328,7 @@ class TrackSearchDialog(SearchDialog): artist=artist, length=length, title=rec_title, - release="(Standalone Recording)") + release=_("(Standalone Recording)")) self.search_results.append((track, node)) def load_selection(self, row=None): From 8524f9dd50e7f7cd59bc016d32089940a3c3287d Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 4 Jul 2016 22:48:19 +0530 Subject: [PATCH 61/98] Display track's title on dialog's startup --- picard/ui/searchdialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 70cb10138..4272569e7 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -226,6 +226,7 @@ class TrackSearchDialog(SearchDialog): def load_similar_tracks(self, file_): self.file_ = file_ metadata = file_.orig_metadata + self.search_box.search_edit.setText(metadata['title']) self.show_progress() self.tagger.xmlws.find_tracks( self.handle_reply, From d3b610ca7ff5f0b47ab67fef8bc25c1839e97b64 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 4 Jul 2016 23:46:18 +0530 Subject: [PATCH 62/98] Make release type translatable --- picard/ui/searchdialog.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 4272569e7..0ba0061e4 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -25,6 +25,7 @@ from picard.ui import PicardDialog from picard.ui.util import StandardButton, ButtonLineEdit from picard.util import format_time, icontheme from picard.mbxml import artist_credit_from_node +from picard.i18n import ugettext_attr class Track(object): @@ -304,10 +305,14 @@ class TrackSearchDialog(SearchDialog): rg_id = rg.id types_list = [] if "primary_type" in rg.children: - types_list.append(rg.primary_type[0].text) + types_list.append(ugettext_attr( + rg.primary_type[0].text, + 'release_group_primary_type')) if "secondary_type_list" in rg.children: for sec in rg.secondary_type_list: - types_list.append(sec.secondary_type[0].text) + types_list.append(ugettext_attr( + sec.secondary_type[0].text, + "release_group_secondary_type")) types = "+".join(types_list) track = Track( From b92cdba212110f089b173a1e35c42f7e03bfb0e1 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 5 Jul 2016 17:38:45 +0530 Subject: [PATCH 63/98] Display actual query when adv search is enabled Don't allow modifying limit filter. Reason being: 1. As it is unrelated to search. 2. It will be irregular to allow changing the limit when advance search syntax is enabled but not in the other case. If it's that necessary, an option can be added to change the limit. --- picard/ui/searchdialog.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 0ba0061e4..fdebab58c 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -227,18 +227,25 @@ class TrackSearchDialog(SearchDialog): def load_similar_tracks(self, file_): self.file_ = file_ metadata = file_.orig_metadata - self.search_box.search_edit.setText(metadata['title']) + query = { + 'track': metadata['title'], + 'artist': metadata['artist'], + 'release': metadata['album'], + 'tnum': metadata['tracknumber'], + 'tracks': metadata['totaltracks'], + 'qdur': str(metadata.length / 2000), + 'isrc': metadata['isrc'], + } + if config.setting["use_adv_search_syntax"]: + query_str = ' '.join(['%s:(%s)' % (item, value) for item, value in query.iteritems()]) + else: + query_str = query["track"] + query["limit"] = 25 + self.search_box.search_edit.setText(query_str) self.show_progress() self.tagger.xmlws.find_tracks( self.handle_reply, - track=metadata['title'], - artist=metadata['artist'], - release=metadata['tracknumber'], - tnum=metadata['totaltracks'], - tracks=metadata['totaltracks'], - qdur=str(metadata.length / 2000), - isrc=metadata['isrc'], - limit=25) + **query) def handle_reply(self, document, http, error): if error: From 13fe7d25c59a40e8cddd60eb372d105c0ef63548 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 6 Jul 2016 14:55:26 +0530 Subject: [PATCH 64/98] Fix incorrect query generation on search --- picard/ui/searchdialog.py | 2 +- picard/webservice.py | 27 ++++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index fdebab58c..31ae6a662 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -220,7 +220,7 @@ class TrackSearchDialog(SearchDialog): self.search_box.search_edit.setText(text) self.show_progress() self.tagger.xmlws.find_tracks(self.handle_reply, - track=text, + query=text, search=True, limit=25) diff --git a/picard/webservice.py b/picard/webservice.py index 5b659035b..b4e554e0f 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -458,27 +458,28 @@ class XmlWebService(QtCore.QObject): host = config.setting["server_host"] port = config.setting["server_port"] filters = [] - query = [] - escape_lucene_query = True + + limit = kwargs.pop("limit") + if limit: + filters.append(("limit", limit)) is_search = kwargs.pop("search", False) if is_search: if config.setting["use_adv_search_syntax"]: - escape_lucene_query = False + query = kwargs["query"] else: - filters.append(('dismax', 'true')) - - for name, value in kwargs.items(): - if name == "limit": - filters.append((name, str(value))) - else: - if escape_lucene_query: - value = _escape_lucene_query(value).strip().lower() + query = _escape_lucene_query(kwargs["query"]).strip().lower() + filters.append(("dismax", 'true')) + else: + query = [] + for name, value in kwargs.items(): + value = _escape_lucene_query(value).strip().lower() if value: query.append('%s:(%s)' % (name, value)) - if query: - filters.append(('query', ' '.join(query))) + query = ' '.join(query) + if query: + filters.append(("query", query)) queryargs = {} for name, value in filters: value = QUrl.toPercentEncoding(unicode(value)) From 6329678ecf7c2030473fe61c1e35fe0b85da761f Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 6 Jul 2016 15:00:57 +0530 Subject: [PATCH 65/98] Make country name translatable --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 31ae6a662..54f7e7bd4 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -305,7 +305,7 @@ class TrackSearchDialog(SearchDialog): else: date = None if "country" in release.children: - country = release.country[0].text + country = ugettext_countries(release.country[0].text) else: country = "" rg = release.release_group[0] From 508e7a5a285e06e3e4a53c571c56465e9b5c686f Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 6 Jul 2016 16:58:23 +0530 Subject: [PATCH 66/98] Use dictionary to store track details The existing `Track` class is just a wrapper around a dictionary. So use a dictionary instead. --- picard/ui/searchdialog.py | 92 ++++++++++++--------------------------- 1 file changed, 27 insertions(+), 65 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 54f7e7bd4..58c624f13 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -28,21 +28,6 @@ from picard.mbxml import artist_credit_from_node from picard.i18n import ugettext_attr -class Track(object): - - def __init__(self, **kwargs): - self.id = kwargs.get("track_id") - self.release_id = kwargs.get("release_id") - self.rg_id = kwargs.get("rg_id") - self.title = kwargs.get("title") - self.length = kwargs.get("length") - self.release = kwargs.get("release") - self.artist = kwargs.get("artist") - self.date = kwargs.get("date") - self.country = kwargs.get("country") - self.release_type = kwargs.get("release_type") - - class ResultTable(QtGui.QTableWidget): def __init__(self, column_titles): @@ -278,38 +263,35 @@ class TrackSearchDialog(SearchDialog): track = obj[0] table_item = QtGui.QTableWidgetItem self.table.insertRow(row) - self.table.setItem(row, 0, table_item(track.title)) - self.table.setItem(row, 1, table_item(track.length)) - self.table.setItem(row, 2, table_item(track.artist)) - self.table.setItem(row, 3, table_item(track.release)) - self.table.setItem(row, 4, table_item(track.date)) - self.table.setItem(row, 5, table_item(track.country)) - self.table.setItem(row, 6, table_item(track.release_type)) + self.table.setItem(row, 0, table_item(track.get("title", ""))) + self.table.setItem(row, 1, table_item(track.get("length", ""))) + self.table.setItem(row, 2, table_item(track.get("artist", ""))) + self.table.setItem(row, 3, table_item(track.get("release", ""))) + self.table.setItem(row, 4, table_item(track.get("date", ""))) + self.table.setItem(row, 5, table_item(track.get("country", ""))) + self.table.setItem(row, 6, table_item(track.get("release_type", ""))) def parse_tracks_from_xml(self, tracks_xml): for node in tracks_xml: - rec_id = node.id - rec_title = node.title[0].text - artist = artist_credit_from_node(node.artist_credit[0])[0] + track = dict() + track["id"] = node.id + track["title"] = node.title[0].text + track["artist"] = artist_credit_from_node(node.artist_credit[0])[0] try: - length = format_time(node.length[0].text) + track["length"] = format_time(node.length[0].text) except AttributeError: - length = "" + track["length"] = "" if "release_list" in node.children and "release" in node.release_list[0].children: releases = node.release_list[0].release for release in releases: - rel_id = release.id - rel_title = release.title[0].text + track["release_id"] = release.id + track["release"] = release.title[0].text if "date" in release.children: - date = release.date[0].text - else: - date = None + track["date"] = release.date[0].text if "country" in release.children: - country = ugettext_countries(release.country[0].text) - else: - country = "" + track["country"] = ugettext_countries(release.country[0].text) rg = release.release_group[0] - rg_id = rg.id + track["rg_id"] = rg.id types_list = [] if "primary_type" in rg.children: types_list.append(ugettext_attr( @@ -320,56 +302,36 @@ class TrackSearchDialog(SearchDialog): types_list.append(ugettext_attr( sec.secondary_type[0].text, "release_group_secondary_type")) - types = "+".join(types_list) + track["release_type"] = "+".join(types_list) - track = Track( - id=rec_id, - release_id=rel_id, - rg_id=rg_id, - title=rec_title, - artist=artist, - length=length, - release=rel_title, - date=date, - country=country, - release_type=types) self.search_results.append((track, node)) - else: - track = Track( - id=rec_id, - artist=artist, - length=length, - title=rec_title, - release=_("(Standalone Recording)")) - self.search_results.append((track, node)) - def load_selection(self, row=None): track, node = self.search_results[row] - if track.release_id: + if track["release_id"]: # The track is not an NAT - self.tagger.get_release_group_by_id(track.rg_id).loaded_albums.add( - track.release_id) + self.tagger.get_release_group_by_id(track["rg_id"]).loaded_albums.add( + track["release_id"]) if self.file_: # Search is performed for a file # Have to move that file from its existing album to the new one if type(self.file_.parent).__name__ == "Track": album = self.file_.parent.album - self.tagger.move_file_to_track(self.file_, track.release_id, track.id) + self.tagger.move_file_to_track(self.file_, track["release_id"], track["id"]) if album._files == 0: # Remove album if it has no more files associated self.tagger.remove_album(album) else: - self.tagger.move_file_to_track(self.file_, track.release_id, track.id) + self.tagger.move_file_to_track(self.file_, track["release_id"], track["id"]) else: # No files associated. Just a normal search. - self.tagger.load_album(track.release_id) + self.tagger.load_album(track["release_id"]) else: # The track is an NAT if self.file_: album = self.file_.parent.album - self.tagger.move_file_to_nat(track.id) + self.tagger.move_file_to_nat(track["id"]) if album._files == 0: self.tagger.remove_album(album) else: - self.tagger.load_nat(track.id, node) + self.tagger.load_nat(track["id"], node) From 6fbc47b9c0673b572b359cb0d97eca35bb201bcd Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 7 Jul 2016 00:21:23 +0530 Subject: [PATCH 67/98] Add checkbox to enable/disable adv query syntax --- picard/ui/searchdialog.py | 51 ++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 58c624f13..9e6b204c8 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -58,22 +58,49 @@ class SearchBox(QtGui.QWidget): self.setupUi() def setupUi(self): - self.setMaximumHeight(35) - layout = QtGui.QHBoxLayout(self) - layout.setMargin(1) - layout.setSpacing(1) - self.search_edit = ButtonLineEdit(self) - layout.addWidget(self.search_edit) - self.search_button = QtGui.QToolButton(self) + self.layout = QtGui.QVBoxLayout(self) + self.search_row_widget = QtGui.QWidget() + self.search_row_layout = QtGui.QHBoxLayout(self.search_row_widget) + self.search_row_layout.setMargin(1) + self.search_row_layout.setSpacing(1) + self.search_edit = ButtonLineEdit(self.search_row_widget) + self.search_row_layout.addWidget(self.search_edit) + self.search_button = QtGui.QToolButton(self.search_row_widget) self.search_button.setAutoRaise(True) self.search_button.setDefaultAction(self.search_action) self.search_button.setIconSize(QtCore.QSize(22, 22)) - layout.addWidget(self.search_button) - self.setLayout(layout) + self.search_row_layout.addWidget(self.search_button) + self.search_row_widget.setLayout(self.search_row_layout) + self.layout.addWidget(self.search_row_widget) + self.adv_opt_row_widget = QtGui.QWidget() + self.adv_opt_row_layout = QtGui.QHBoxLayout(self.adv_opt_row_widget) + self.adv_opt_row_layout.setAlignment(QtCore.Qt.AlignLeft) + self.adv_opt_row_layout.setMargin(1) + self.adv_opt_row_layout.setSpacing(1) + self.use_adv_search_syntax = QtGui.QCheckBox(self.adv_opt_row_widget) + self.use_adv_search_syntax.setText(_("Use advance query syntax")) + self.adv_opt_row_layout.addWidget(self.use_adv_search_syntax) + self.adv_syntax_help = QtGui.QLabel(self.adv_opt_row_widget) + self.adv_syntax_help.setOpenExternalLinks(True) + self.adv_syntax_help.setText(_( + "" + "Syntax Help")) + self.adv_opt_row_layout.addWidget(self.adv_syntax_help) + self.adv_opt_row_widget.setLayout(self.adv_opt_row_layout) + self.layout.addWidget(self.adv_opt_row_widget) + self.layout.setMargin(1) + self.layout.setSpacing(1) + self.setMaximumHeight(60) def search(self): self.parent.search(self.search_edit.text()) + def restore_checkbox_state(self): + self.use_adv_search_syntax.setChecked(config.setting["use_adv_search_syntax"]) + + def save_checkbox_state(self): + config.setting["use_adv_search_syntax"] = self.use_adv_search_syntax.isChecked() + class SearchDialog(PicardDialog): @@ -86,7 +113,7 @@ class SearchDialog(PicardDialog): PicardDialog.__init__(self, parent) self.search_results = [] self.setupUi() - self.restore_window_state() + self.restore_state() def setupUi(self): self.verticalLayout = QtGui.QVBoxLayout(self) @@ -166,10 +193,11 @@ class SearchDialog(PicardDialog): QtGui.QDialog.accept(self) - def restore_window_state(self): + def restore_state(self): size = config.persist["searchdialog_window_size"] if size: self.resize(size) + self.search_box.restore_checkbox_state() def restore_table_header_state(self): header = self.table.horizontalHeader() @@ -183,6 +211,7 @@ class SearchDialog(PicardDialog): header = self.table.horizontalHeader() config.persist["searchdialog_header_state"] = header.saveState() config.persist["searchdialog_window_size"] = self.size() + self.search_box.save_checkbox_state() class TrackSearchDialog(SearchDialog): From e68f31a8bb61f11d29401afef4498221de683011 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 7 Jul 2016 00:37:03 +0530 Subject: [PATCH 68/98] Do not display filters with null value This leads to HTTP 400 error, server unable to parse query. --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 9e6b204c8..faab7f235 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -251,7 +251,7 @@ class TrackSearchDialog(SearchDialog): 'isrc': metadata['isrc'], } if config.setting["use_adv_search_syntax"]: - query_str = ' '.join(['%s:(%s)' % (item, value) for item, value in query.iteritems()]) + query_str = ' '.join(['%s:(%s)' % (item, value) for item, value in query.iteritems() if value]) else: query_str = query["track"] query["limit"] = 25 From a28da55af2f255817b19ad917f5d97ba41194870 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 12 Jul 2016 15:24:17 +0530 Subject: [PATCH 69/98] Separate track (dict) object for each release --- picard/ui/searchdialog.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index faab7f235..e917e894b 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -302,17 +302,21 @@ class TrackSearchDialog(SearchDialog): def parse_tracks_from_xml(self, tracks_xml): for node in tracks_xml: - track = dict() - track["id"] = node.id - track["title"] = node.title[0].text - track["artist"] = artist_credit_from_node(node.artist_credit[0])[0] + track_id = node.id + track_title = node.title[0].text + track_artist = artist_credit_from_node(node.artist_credit[0])[0] try: - track["length"] = format_time(node.length[0].text) + track_length = format_time(node.length[0].text) except AttributeError: - track["length"] = "" + track_length = "" if "release_list" in node.children and "release" in node.release_list[0].children: releases = node.release_list[0].release for release in releases: + track = dict() + track["id"] = track_id + track["title"] = track_title + track["artist"] = track_artist + track["length"] = track_length track["release_id"] = release.id track["release"] = release.title[0].text if "date" in release.children: From a2e5f8aae95999832ec8a7ab4f7ee936a96e8e04 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 12 Jul 2016 15:26:01 +0530 Subject: [PATCH 70/98] Fix NAT support This was mistakenly removed in f667b04. This also fixes a minor grammatical mistake. --- picard/ui/searchdialog.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index e917e894b..da61e574b 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -336,12 +336,19 @@ class TrackSearchDialog(SearchDialog): sec.secondary_type[0].text, "release_group_secondary_type")) track["release_type"] = "+".join(types_list) - self.search_results.append((track, node)) + else: + track = dict() + track["id"] = track_id + track["title"] = track_title + track["artist"] = track_artist + track["length"] = track_length + track["release"] = _("Standalone Recording") + self.search_results.append((track, node)) def load_selection(self, row=None): track, node = self.search_results[row] - if track["release_id"]: + if track.get("release_id"): # The track is not an NAT self.tagger.get_release_group_by_id(track["rg_id"]).loaded_albums.add( track["release_id"]) @@ -360,7 +367,7 @@ class TrackSearchDialog(SearchDialog): # No files associated. Just a normal search. self.tagger.load_album(track["release_id"]) else: - # The track is an NAT + # The track is a NAT if self.file_: album = self.file_.parent.album self.tagger.move_file_to_nat(track["id"]) From dd3b385b7e80b758936f736149d348a998c345cb Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 12 Jul 2016 23:32:48 +0530 Subject: [PATCH 71/98] Use iso_3166_1_code element for country The `country` element (used till now) is obsolete. It contains information of a single country where album is released. Instead extract all country codes from iso_3166_1_code_list element and display them. --- picard/ui/searchdialog.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index da61e574b..2bfbd20d6 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -321,8 +321,15 @@ class TrackSearchDialog(SearchDialog): track["release"] = release.title[0].text if "date" in release.children: track["date"] = release.date[0].text - if "country" in release.children: - track["country"] = ugettext_countries(release.country[0].text) + if "release_event_list" in release.children: + country = [] + for re in release.release_event_list[0].release_event: + try: + country.append( + re.area[0].iso_3166_1_code_list[0].iso_3166_1_code[0].text) + except AttributeError: + pass + track["country"] = ", ".join(country) rg = release.release_group[0] track["rg_id"] = rg.id types_list = [] From 5b0b17ce06c664cb7c1d321af92c7318d7624a22 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 13 Jul 2016 22:44:40 +0530 Subject: [PATCH 72/98] Set object name for most top level widgets --- picard/ui/searchdialog.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 2bfbd20d6..41bc96aa3 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -117,16 +117,19 @@ class SearchDialog(PicardDialog): def setupUi(self): self.verticalLayout = QtGui.QVBoxLayout(self) - self.verticalLayout.setObjectName(_("verticalLayout")) - + self.verticalLayout.setObjectName(_("vertical_layout")) self.search_box = SearchBox(self) + self.search_box.setObjectName(_("search_box")) self.verticalLayout.addWidget(self.search_box) self.center_widget = QtGui.QWidget(self) + self.center_widget.setObjectName(_("center_widget")) self.center_layout = QtGui.QVBoxLayout(self.center_widget) + self.center_layout.setObjectName(_("center_layout")) self.center_layout.setMargin(1) self.center_widget.setLayout(self.center_layout) self.verticalLayout.addWidget(self.center_widget) self.buttonBox = QtGui.QDialogButtonBox(self) + self.buttonBox.setObjectName(_("button_box")) self.load_button = QtGui.QPushButton(_("Load into Picard")) self.load_button.setEnabled(False) self.buttonBox.addButton( From ba8c904ec21787bb685c4d4c32cf1800799b7c56 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 16 Jul 2016 11:55:26 +0530 Subject: [PATCH 73/98] Don't use `setMargin()`, as it's obsolete As mentioned here -> http://doc.qt.io/qt-4.8/qlayout-obsolete.html, use `setContentsMargins()` instead. --- picard/ui/searchdialog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 41bc96aa3..69f4599f8 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -61,7 +61,7 @@ class SearchBox(QtGui.QWidget): self.layout = QtGui.QVBoxLayout(self) self.search_row_widget = QtGui.QWidget() self.search_row_layout = QtGui.QHBoxLayout(self.search_row_widget) - self.search_row_layout.setMargin(1) + self.search_row_layout.setContentsMargins(1, 1, 1, 1) self.search_row_layout.setSpacing(1) self.search_edit = ButtonLineEdit(self.search_row_widget) self.search_row_layout.addWidget(self.search_edit) @@ -75,7 +75,7 @@ class SearchBox(QtGui.QWidget): self.adv_opt_row_widget = QtGui.QWidget() self.adv_opt_row_layout = QtGui.QHBoxLayout(self.adv_opt_row_widget) self.adv_opt_row_layout.setAlignment(QtCore.Qt.AlignLeft) - self.adv_opt_row_layout.setMargin(1) + self.adv_opt_row_layout.setContentsMargins(1, 1, 1, 1) self.adv_opt_row_layout.setSpacing(1) self.use_adv_search_syntax = QtGui.QCheckBox(self.adv_opt_row_widget) self.use_adv_search_syntax.setText(_("Use advance query syntax")) @@ -88,7 +88,7 @@ class SearchBox(QtGui.QWidget): self.adv_opt_row_layout.addWidget(self.adv_syntax_help) self.adv_opt_row_widget.setLayout(self.adv_opt_row_layout) self.layout.addWidget(self.adv_opt_row_widget) - self.layout.setMargin(1) + self.layout.setContentsMargins(1, 1, 1, 1) self.layout.setSpacing(1) self.setMaximumHeight(60) @@ -125,7 +125,7 @@ class SearchDialog(PicardDialog): self.center_widget.setObjectName(_("center_widget")) self.center_layout = QtGui.QVBoxLayout(self.center_widget) self.center_layout.setObjectName(_("center_layout")) - self.center_layout.setMargin(1) + self.center_layout.setContentsMargins(1, 1, 1, 1) self.center_widget.setLayout(self.center_layout) self.verticalLayout.addWidget(self.center_widget) self.buttonBox = QtGui.QDialogButtonBox(self) @@ -160,7 +160,7 @@ class SearchDialog(PicardDialog): gif_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop) layout.addWidget(text_label) layout.addWidget(gif_label) - layout.setMargin(1) + layout.setContentsMargins(1, 1, 1, 1) self.progress_widget.setLayout(layout) self.add_widget_to_center_layout(self.progress_widget) From 74dee64892ffe53f32ea8eb211909e796fbc8657 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sun, 17 Jul 2016 23:58:36 +0530 Subject: [PATCH 74/98] Use existing methods to parse results There are existing methods to parse xml nodes of a recording. Use them to avoid duplicating code. For extracting country details, use separate functionality. Reason being, current `release_to_metadata` method uses `country` element to extract country details. This element is obsolete as there can be multiple countries, and this element returns only one. --- picard/mbxml.py | 9 ++--- picard/track.py | 4 +- picard/ui/searchdialog.py | 81 +++++++++++++++------------------------ 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index 31910e1a5..70bc31126 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -246,7 +246,7 @@ def media_formats_from_node(node): def track_to_metadata(node, track): m = track.metadata - recording_to_metadata(node.recording[0], track) + recording_to_metadata(node.recording[0], m, track) m.add_unique('musicbrainz_trackid', node.id) # overwrite with data we have on the track for name, nodes in node.children.iteritems(): @@ -265,8 +265,7 @@ def track_to_metadata(node, track): m['~length'] = format_time(m.length) -def recording_to_metadata(node, track): - m = track.metadata +def recording_to_metadata(node, m, track=None): m.length = 0 m.add_unique('musicbrainz_recordingid', node.id) for name, nodes in node.children.iteritems(): @@ -281,7 +280,7 @@ def recording_to_metadata(node, track): m['~recordingcomment'] = nodes[0].text elif name == 'artist_credit': artist_credit_to_metadata(nodes[0], m) - if 'name_credit' in nodes[0].children: + if 'name_credit' in nodes[0].children and track: for name_credit in nodes[0].name_credit: if 'artist' in name_credit.children: for artist in name_credit.artist: @@ -375,7 +374,7 @@ def release_to_metadata(node, m, album=None): m['barcode'] = nodes[0].text elif name == 'relation_list': _relations_to_metadata(nodes, m) - elif name == 'label_info_list' and nodes[0].count != '0': + elif name == 'label_info_list' and getattr(nodes[0], "count", 0) != '0': m['label'], m['catalognumber'] = label_info_from_node(nodes[0]) elif name == 'text_representation': if 'language' in nodes[0].children: diff --git a/picard/track.py b/picard/track.py index 3a96d4eb2..faaef9cc8 100644 --- a/picard/track.py +++ b/picard/track.py @@ -271,9 +271,9 @@ class NonAlbumTrack(Track): log.error(traceback.format_exc()) def _parse_recording(self, recording): - recording_to_metadata(recording, self) - self._customize_metadata() m = self.metadata + recording_to_metadata(recording, m, self) + self._customize_metadata() run_track_metadata_processors(self.album, m, None, recording) if config.setting["enable_tagger_script"]: script = config.setting["tagger_script"] diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 69f4599f8..12df1363f 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -24,8 +24,14 @@ from picard.file import File from picard.ui import PicardDialog from picard.ui.util import StandardButton, ButtonLineEdit from picard.util import format_time, icontheme -from picard.mbxml import artist_credit_from_node +from picard.mbxml import ( + artist_credit_from_node, + recording_to_metadata, + release_to_metadata, + release_group_to_metadata +) from picard.i18n import ugettext_attr +from picard.metadata import Metadata class ResultTable(QtGui.QTableWidget): @@ -291,97 +297,70 @@ class TrackSearchDialog(SearchDialog): def display_results(self): self.show_table(self.table_headers) + row = 0 for row, obj in enumerate(self.search_results): track = obj[0] table_item = QtGui.QTableWidgetItem self.table.insertRow(row) self.table.setItem(row, 0, table_item(track.get("title", ""))) - self.table.setItem(row, 1, table_item(track.get("length", ""))) + self.table.setItem(row, 1, table_item(track.get("~length", ""))) self.table.setItem(row, 2, table_item(track.get("artist", ""))) - self.table.setItem(row, 3, table_item(track.get("release", ""))) + self.table.setItem(row, 3, table_item(track.get("album", ""))) self.table.setItem(row, 4, table_item(track.get("date", ""))) self.table.setItem(row, 5, table_item(track.get("country", ""))) - self.table.setItem(row, 6, table_item(track.get("release_type", ""))) + self.table.setItem(row, 6, table_item(track.get("releasetype", ""))) def parse_tracks_from_xml(self, tracks_xml): for node in tracks_xml: - track_id = node.id - track_title = node.title[0].text - track_artist = artist_credit_from_node(node.artist_credit[0])[0] - try: - track_length = format_time(node.length[0].text) - except AttributeError: - track_length = "" if "release_list" in node.children and "release" in node.release_list[0].children: - releases = node.release_list[0].release - for release in releases: - track = dict() - track["id"] = track_id - track["title"] = track_title - track["artist"] = track_artist - track["length"] = track_length - track["release_id"] = release.id - track["release"] = release.title[0].text - if "date" in release.children: - track["date"] = release.date[0].text - if "release_event_list" in release.children: + for rel_node in node.release_list[0].release: + track = Metadata() + recording_to_metadata(node, track) + release_to_metadata(rel_node, track) + rg_node = rel_node.release_group[0] + release_group_to_metadata(rg_node, track) + if "release_event_list" in rel_node.children: country = [] - for re in release.release_event_list[0].release_event: + for re in rel_node.release_event_list[0].release_event: try: country.append( re.area[0].iso_3166_1_code_list[0].iso_3166_1_code[0].text) except AttributeError: pass track["country"] = ", ".join(country) - rg = release.release_group[0] - track["rg_id"] = rg.id - types_list = [] - if "primary_type" in rg.children: - types_list.append(ugettext_attr( - rg.primary_type[0].text, - 'release_group_primary_type')) - if "secondary_type_list" in rg.children: - for sec in rg.secondary_type_list: - types_list.append(ugettext_attr( - sec.secondary_type[0].text, - "release_group_secondary_type")) - track["release_type"] = "+".join(types_list) self.search_results.append((track, node)) else: - track = dict() - track["id"] = track_id - track["title"] = track_title - track["artist"] = track_artist - track["length"] = track_length - track["release"] = _("Standalone Recording") + track = Metadata() + recording_to_metadata(node, track) + track["album"] = _("Standalone Recording") self.search_results.append((track, node)) def load_selection(self, row=None): track, node = self.search_results[row] - if track.get("release_id"): + if track.get("musicbrainz_albumid"): # The track is not an NAT - self.tagger.get_release_group_by_id(track["rg_id"]).loaded_albums.add( - track["release_id"]) + self.tagger.get_release_group_by_id(track["musicbrainz_releasegroupid"]).loaded_albums.add( + track["musicbrainz_albumid"]) if self.file_: # Search is performed for a file # Have to move that file from its existing album to the new one if type(self.file_.parent).__name__ == "Track": album = self.file_.parent.album - self.tagger.move_file_to_track(self.file_, track["release_id"], track["id"]) + self.tagger.move_file_to_track(self.file_, track["musicbrainz_albumid"], track["musicbrainz_recordingid"]) if album._files == 0: # Remove album if it has no more files associated self.tagger.remove_album(album) else: - self.tagger.move_file_to_track(self.file_, track["release_id"], track["id"]) + self.tagger.move_file_to_track(self.file_, track["musicbrainz_albumid"], track["musicbrainz_recordingid"]) else: # No files associated. Just a normal search. - self.tagger.load_album(track["release_id"]) + self.tagger.load_album(track["musicbrainz_albumid"]) else: # The track is a NAT if self.file_: album = self.file_.parent.album - self.tagger.move_file_to_nat(track["id"]) + self.tagger.move_file_to_nat(track["musicbrainz_recordingid"]) if album._files == 0: self.tagger.remove_album(album) else: - self.tagger.load_nat(track["id"], node) + self.tagger.load_nat(track["musicbrainz_recordingid"], node) From f23a340414d8267af1d5517374dd2e805003bd96 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 19 Jul 2016 19:54:14 +0530 Subject: [PATCH 75/98] Add paranthesis around link --- picard/ui/searchdialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 12df1363f..5a198acee 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -89,8 +89,8 @@ class SearchBox(QtGui.QWidget): self.adv_syntax_help = QtGui.QLabel(self.adv_opt_row_widget) self.adv_syntax_help.setOpenExternalLinks(True) self.adv_syntax_help.setText(_( - "" - "Syntax Help")) + "(" + "Syntax Help)")) self.adv_opt_row_layout.addWidget(self.adv_syntax_help) self.adv_opt_row_widget.setLayout(self.adv_opt_row_layout) self.layout.addWidget(self.adv_opt_row_widget) From 06af62a06e49397761f3d5ea39af618e3a75a66e Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Tue, 19 Jul 2016 19:56:37 +0530 Subject: [PATCH 76/98] Save dialog state on cancel Also, use if/else logic rather than try/exception, as it makes more sense. It's know where and why exception will be thrown, so rather avoid that. --- picard/ui/searchdialog.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 5a198acee..881cedba0 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -191,17 +191,25 @@ class SearchDialog(PicardDialog): self.accept() def accept(self): - try: + if hasattr(self, "table"): sel_rows = self.table.selectionModel().selectedRows() if sel_rows: sel_row = sel_rows[0].row() self.load_selection(sel_row) self.save_state(True) - except AttributeError: + else: self.save_state(False) QtGui.QDialog.accept(self) + def reject(self): + if hasattr(self, "table"): + self.save_state(True) + else: + self.save_state(False) + + QtGui.QDialog.reject(self) + def restore_state(self): size = config.persist["searchdialog_window_size"] if size: From 80ddbee5deb6b6d832211347b1f6227c9d65627b Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 20 Jul 2016 21:56:56 +0530 Subject: [PATCH 77/98] Set widget's object name --- picard/ui/searchdialog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 881cedba0..2f1a2e284 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -156,6 +156,7 @@ class SearchDialog(PicardDialog): def show_progress(self): self.progress_widget = QtGui.QWidget(self) + self.progress_widget.setObjectName("progress_widget") layout = QtGui.QVBoxLayout(self.progress_widget) text_label = QtGui.QLabel('Fetching results...', self.progress_widget) text_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom) @@ -172,12 +173,14 @@ class SearchDialog(PicardDialog): def show_error(self, error): self.error_widget = QtGui.QLabel(_("" + error + "")) + self.error_widget.setObjectName("error_widget") self.error_widget.setAlignment(QtCore.Qt.AlignCenter) self.error_widget.setWordWrap(True) self.add_widget_to_center_layout(self.error_widget) def show_table(self, column_headers): self.table = ResultTable(self.table_headers) + self.table.setObjectName("results_table") self.table.cellDoubleClicked.connect(self.row_double_clicked) self.restore_table_header_state() self.add_widget_to_center_layout(self.table) From 56e6ad6e8dddd1afef432a10ecd7ea35e9a28d84 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 20 Jul 2016 21:58:06 +0530 Subject: [PATCH 78/98] Unset corresponding object when widget is deleted Exception can be generated after the table widget has been deleted. Say, table is displayed first. But for next search, some network error occurs. The table widget will get deleted by `add_widget_to_center_layout` as soon as progress widget is displayed. `self.table` would have the widget reference, but it's cleared from memory, resulting in an exception, like -> "wrapped object has been deleted". To avoid this, unreference `self.table` if corresponding widget is to be deleted. The other two widgets namely `self.progress_widget` and `self.error_widget` aren't (and probably wouldn't in future) accessed by any other method. So, no need to explicity unreference them. They will be reassigned a new widget when `show_progress` and `show_error` are called respectively. --- picard/ui/searchdialog.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 2f1a2e284..0489fb6cd 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -151,6 +151,8 @@ class SearchDialog(PicardDialog): def add_widget_to_center_layout(self, widget): wid = self.center_layout.itemAt(0) if wid: + if wid.widget().objectName() == "results_table": + self.table = None wid.widget().deleteLater() self.center_layout.addWidget(widget) @@ -194,7 +196,7 @@ class SearchDialog(PicardDialog): self.accept() def accept(self): - if hasattr(self, "table"): + if getattr(self, "table"): sel_rows = self.table.selectionModel().selectedRows() if sel_rows: sel_row = sel_rows[0].row() @@ -206,7 +208,7 @@ class SearchDialog(PicardDialog): QtGui.QDialog.accept(self) def reject(self): - if hasattr(self, "table"): + if getattr(self, "table"): self.save_state(True) else: self.save_state(False) From e9881ca0de9762fd866d5f26b61ee578d49695ec Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 20 Jul 2016 22:26:03 +0530 Subject: [PATCH 79/98] Add default value in case attribute isn't present Happens if table isn't displayed ever. `self.table` wouldn't be defined leading to AttributeError. --- picard/ui/searchdialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 0489fb6cd..0ad41581e 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -196,7 +196,7 @@ class SearchDialog(PicardDialog): self.accept() def accept(self): - if getattr(self, "table"): + if getattr(self, "table", None): sel_rows = self.table.selectionModel().selectedRows() if sel_rows: sel_row = sel_rows[0].row() @@ -208,7 +208,7 @@ class SearchDialog(PicardDialog): QtGui.QDialog.accept(self) def reject(self): - if getattr(self, "table"): + if getattr(self, "table", None): self.save_state(True) else: self.save_state(False) From cc4aac97b09f71d450c7973e5227a7acadcab84e Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 20 Jul 2016 22:29:27 +0530 Subject: [PATCH 80/98] Add refresh button This would allow user to re-perform the search, in case results aren't displayed, without closing the dialog. --- picard/ui/searchdialog.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 0ad41581e..807c33a96 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -61,6 +61,9 @@ class SearchBox(QtGui.QWidget): self.search_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search"), self) self.search_action.triggered.connect(self.search) + self.refresh_action = QtGui.QAction(icontheme.lookup('view-refresh'), + _(u"Refresh"), self) + self.refresh_action.triggered.connect(self.parent.retry) self.setupUi() def setupUi(self): @@ -76,6 +79,11 @@ class SearchBox(QtGui.QWidget): self.search_button.setDefaultAction(self.search_action) self.search_button.setIconSize(QtCore.QSize(22, 22)) self.search_row_layout.addWidget(self.search_button) + self.refresh_button = QtGui.QToolButton(self.search_row_widget) + self.refresh_button.setAutoRaise(True) + self.refresh_button.setDefaultAction(self.refresh_action) + self.refresh_button.setIconSize(QtCore.QSize(22, 22)) + self.search_row_layout.addWidget(self.refresh_button) self.search_row_widget.setLayout(self.search_row_layout) self.layout.addWidget(self.search_row_widget) self.adv_opt_row_widget = QtGui.QWidget() @@ -253,6 +261,7 @@ class TrackSearchDialog(SearchDialog): ] def search(self, text): + self.retry_params = (self.search, text) self.search_box.search_edit.setText(text) self.show_progress() self.tagger.xmlws.find_tracks(self.handle_reply, @@ -261,6 +270,7 @@ class TrackSearchDialog(SearchDialog): limit=25) def load_similar_tracks(self, file_): + self.retry_params = (self.load_similar_tracks, file_) self.file_ = file_ metadata = file_.orig_metadata query = { @@ -283,10 +293,15 @@ class TrackSearchDialog(SearchDialog): self.handle_reply, **query) + def retry(self): + self.retry_params[0](self.retry_params[1]) + + def handle_reply(self, document, http, error): if error: - error_msg = _("Unable to fetch results. Close the dialog and try " - "again. See debug logs for more details.") + error_msg = _("Some network error occurred. Check debug logs for more details.
" + "Click on refresh or try a different query." + ) self.show_error(error_msg) return From 6413b895cd4a39549aa4d9054d94593c71aa50b8 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 21 Jul 2016 21:32:26 +0530 Subject: [PATCH 81/98] Add non breaking space to avoid text collapse --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 807c33a96..f9a30e3c5 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -97,7 +97,7 @@ class SearchBox(QtGui.QWidget): self.adv_syntax_help = QtGui.QLabel(self.adv_opt_row_widget) self.adv_syntax_help.setOpenExternalLinks(True) self.adv_syntax_help.setText(_( - "(" + " (" "Syntax Help)")) self.adv_opt_row_layout.addWidget(self.adv_syntax_help) self.adv_opt_row_widget.setLayout(self.adv_opt_row_layout) From 7fd0b58e17fd9ef595e877acb9ee10cd9994d6d3 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 21 Jul 2016 21:41:21 +0530 Subject: [PATCH 82/98] Display retry button only when request fails --- picard/ui/searchdialog.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index f9a30e3c5..222f48149 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -61,9 +61,6 @@ class SearchBox(QtGui.QWidget): self.search_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search"), self) self.search_action.triggered.connect(self.search) - self.refresh_action = QtGui.QAction(icontheme.lookup('view-refresh'), - _(u"Refresh"), self) - self.refresh_action.triggered.connect(self.parent.retry) self.setupUi() def setupUi(self): @@ -79,11 +76,6 @@ class SearchBox(QtGui.QWidget): self.search_button.setDefaultAction(self.search_action) self.search_button.setIconSize(QtCore.QSize(22, 22)) self.search_row_layout.addWidget(self.search_button) - self.refresh_button = QtGui.QToolButton(self.search_row_widget) - self.refresh_button.setAutoRaise(True) - self.refresh_button.setDefaultAction(self.refresh_action) - self.refresh_button.setIconSize(QtCore.QSize(22, 22)) - self.search_row_layout.addWidget(self.refresh_button) self.search_row_widget.setLayout(self.search_row_layout) self.layout.addWidget(self.search_row_widget) self.adv_opt_row_widget = QtGui.QWidget() @@ -181,11 +173,25 @@ class SearchDialog(PicardDialog): self.progress_widget.setLayout(layout) self.add_widget_to_center_layout(self.progress_widget) - def show_error(self, error): - self.error_widget = QtGui.QLabel(_("" + error + "")) + def show_error(self, error, show_retry_button=False): + self.error_widget = QtGui.QWidget(self) self.error_widget.setObjectName("error_widget") - self.error_widget.setAlignment(QtCore.Qt.AlignCenter) - self.error_widget.setWordWrap(True) + layout = QtGui.QVBoxLayout(self.error_widget) + error_label = QtGui.QLabel(_("" + error + "")) + error_label.setWordWrap(True) + error_label.setAlignment(QtCore.Qt.AlignCenter) + layout.addWidget(error_label) + if show_retry_button: + retry_widget = QtGui.QWidget(self.error_widget) + retry_layout = QtGui.QHBoxLayout(retry_widget) + retry_button = QtGui.QPushButton(_("Retry"), self.error_widget) + retry_button.clicked.connect(self.retry) + retry_button.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Fixed)) + retry_layout.addWidget(retry_button) + retry_layout.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop) + retry_widget.setLayout(retry_layout) + layout.addWidget(retry_widget) + self.error_widget.setLayout(layout) self.add_widget_to_center_layout(self.error_widget) def show_table(self, column_headers): @@ -300,9 +306,9 @@ class TrackSearchDialog(SearchDialog): def handle_reply(self, document, http, error): if error: error_msg = _("Some network error occurred. Check debug logs for more details.
" - "Click on refresh or try a different query." + "Hit `Retry` or try a different query." ) - self.show_error(error_msg) + self.show_error(error_msg, show_retry_button=True) return try: From e47a245bd17dd3111bc39ab2af4fa7fb9ef68a14 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 22 Jul 2016 19:42:58 +0530 Subject: [PATCH 83/98] Save table header size on each resize The ResultTable is resized using size information from config.setting["searchdialog_header_state"]. As this value is updated only when the dialog is closed, any ongoing changes (i.e. without closing the dialog) will be ignored. This patch avoids that. --- picard/ui/searchdialog.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 222f48149..1c461f890 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -45,14 +45,12 @@ class ResultTable(QtGui.QTableWidget): QtGui.QAbstractItemView.SelectRows) self.setEditTriggers( QtGui.QAbstractItemView.NoEditTriggers) - self.horizontalHeader().setStretchLastSection(True) self.horizontalHeader().setResizeMode( QtGui.QHeaderView.Stretch) self.horizontalHeader().setResizeMode( QtGui.QHeaderView.Interactive) - class SearchBox(QtGui.QWidget): def __init__(self, parent): @@ -198,6 +196,8 @@ class SearchDialog(PicardDialog): self.table = ResultTable(self.table_headers) self.table.setObjectName("results_table") self.table.cellDoubleClicked.connect(self.row_double_clicked) + self.table.horizontalHeader().sectionResized.connect( + self.save_table_header_state) self.restore_table_header_state() self.add_widget_to_center_layout(self.table) def enable_loading_button(): @@ -244,11 +244,14 @@ class SearchDialog(PicardDialog): def save_state(self, table_loaded=True): if table_loaded: - header = self.table.horizontalHeader() - config.persist["searchdialog_header_state"] = header.saveState() + self.save_table_header_state() config.persist["searchdialog_window_size"] = self.size() self.search_box.save_checkbox_state() + def save_table_header_state(self): + state = self.table.horizontalHeader().saveState() + config.persist["searchdialog_header_state"] = state + class TrackSearchDialog(SearchDialog): From 1692b84f3928b71ade10b7504189fa645c25a286 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 22 Jul 2016 23:38:24 +0530 Subject: [PATCH 84/98] Use double quote instead of backquote --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 1c461f890..9a0c21ddb 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -309,7 +309,7 @@ class TrackSearchDialog(SearchDialog): def handle_reply(self, document, http, error): if error: error_msg = _("Some network error occurred. Check debug logs for more details.
" - "Hit `Retry` or try a different query." + "Hit \"Retry\" or try a different query." ) self.show_error(error_msg, show_retry_button=True) return From a637ff18adcd83aa58669cde0b6710e4775572f3 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 22 Jul 2016 23:43:02 +0530 Subject: [PATCH 85/98] Default value for getattr should be a string And use single quote for uniformity. --- picard/mbxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/mbxml.py b/picard/mbxml.py index 70bc31126..9e43171db 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -374,7 +374,7 @@ def release_to_metadata(node, m, album=None): m['barcode'] = nodes[0].text elif name == 'relation_list': _relations_to_metadata(nodes, m) - elif name == 'label_info_list' and getattr(nodes[0], "count", 0) != '0': + elif name == 'label_info_list' and getattr(nodes[0], 'count', '0') != '0': m['label'], m['catalognumber'] = label_info_from_node(nodes[0]) elif name == 'text_representation': if 'language' in nodes[0].children: From f53fb9c3f31536ff0bfb7516672422fd95f833e5 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 25 Jul 2016 21:49:28 +0530 Subject: [PATCH 86/98] Replace progress label text To reuse an existing translation. --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 9a0c21ddb..bffc8e331 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -158,7 +158,7 @@ class SearchDialog(PicardDialog): self.progress_widget = QtGui.QWidget(self) self.progress_widget.setObjectName("progress_widget") layout = QtGui.QVBoxLayout(self.progress_widget) - text_label = QtGui.QLabel('Fetching results...', self.progress_widget) + text_label = QtGui.QLabel(_('Loading...'), self.progress_widget) text_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignBottom) gif_label = QtGui.QLabel(self.progress_widget) movie = QtGui.QMovie(":/images/loader.gif") From 21da7e717a652ad511d8823d447e1ebaa0faa778 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 25 Jul 2016 22:04:12 +0530 Subject: [PATCH 87/98] Display actual error when network request fails --- picard/ui/searchdialog.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index bffc8e331..759e3e6f0 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -16,7 +16,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -from PyQt4 import QtGui, QtCore +from PyQt4 import QtGui, QtCore, QtNetwork from operator import itemgetter from functools import partial from picard import config @@ -175,9 +175,10 @@ class SearchDialog(PicardDialog): self.error_widget = QtGui.QWidget(self) self.error_widget.setObjectName("error_widget") layout = QtGui.QVBoxLayout(self.error_widget) - error_label = QtGui.QLabel(_("" + error + "")) + error_label = QtGui.QLabel(error) error_label.setWordWrap(True) error_label.setAlignment(QtCore.Qt.AlignCenter) + error_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) layout.addWidget(error_label) if show_retry_button: retry_widget = QtGui.QWidget(self.error_widget) @@ -308,16 +309,20 @@ class TrackSearchDialog(SearchDialog): def handle_reply(self, document, http, error): if error: - error_msg = _("Some network error occurred. Check debug logs for more details.
" - "Hit \"Retry\" or try a different query." - ) + error_msg = _("Following network request error occurred while fetching results:
" + "Network request error for %s: %s (QT code %d, HTTP code %s)
" % ( + http.request().url().toString(QtCore.QUrl.RemoveUserInfo), + http.errorString(), + error, + repr(http.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute))) + ) self.show_error(error_msg, show_retry_button=True) return try: tracks = document.metadata[0].recording_list[0].recording except (AttributeError, IndexError): - error_msg = _("No results found. Please try a different search query.") + error_msg = _("No results found. Please try a different search query.") self.show_error(error_msg) return From e9b0eacafb46963c10c7fea1717a602129742bef Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 25 Jul 2016 22:33:41 +0530 Subject: [PATCH 88/98] Fix missing parent argument for some widgets --- picard/ui/searchdialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 759e3e6f0..9056e1493 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -63,7 +63,7 @@ class SearchBox(QtGui.QWidget): def setupUi(self): self.layout = QtGui.QVBoxLayout(self) - self.search_row_widget = QtGui.QWidget() + self.search_row_widget = QtGui.QWidget(self) self.search_row_layout = QtGui.QHBoxLayout(self.search_row_widget) self.search_row_layout.setContentsMargins(1, 1, 1, 1) self.search_row_layout.setSpacing(1) @@ -76,7 +76,7 @@ class SearchBox(QtGui.QWidget): self.search_row_layout.addWidget(self.search_button) self.search_row_widget.setLayout(self.search_row_layout) self.layout.addWidget(self.search_row_widget) - self.adv_opt_row_widget = QtGui.QWidget() + self.adv_opt_row_widget = QtGui.QWidget(self) self.adv_opt_row_layout = QtGui.QHBoxLayout(self.adv_opt_row_widget) self.adv_opt_row_layout.setAlignment(QtCore.Qt.AlignLeft) self.adv_opt_row_layout.setContentsMargins(1, 1, 1, 1) From 7b4e213a302ca9fd7381ab1be9849ca77c584584 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 25 Jul 2016 23:33:46 +0530 Subject: [PATCH 89/98] Move code to generate error message into methods * This makes the code structure a bit cleaner * Also these methods are likely to be reused in other dialog classes --- picard/ui/searchdialog.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 9056e1493..0c8f3ad27 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -210,6 +210,20 @@ class SearchDialog(PicardDialog): self.load_selection(row) self.accept() + def network_error(self, reply, error): + error_msg = _("Following error occurred while fetching results:
" + "Network request error for %s: %s (QT code %d, HTTP code %s)
" % ( + reply.request().url().toString(QtCore.QUrl.RemoveUserInfo), + reply.errorString(), + error, + repr(reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute))) + ) + self.show_error(error_msg, show_retry_button=True) + + def no_results_found(self): + error_msg = _("No results found. Please try a different search query.") + self.show_error(error_msg) + def accept(self): if getattr(self, "table", None): sel_rows = self.table.selectionModel().selectedRows() @@ -309,21 +323,13 @@ class TrackSearchDialog(SearchDialog): def handle_reply(self, document, http, error): if error: - error_msg = _("Following network request error occurred while fetching results:
" - "Network request error for %s: %s (QT code %d, HTTP code %s)
" % ( - http.request().url().toString(QtCore.QUrl.RemoveUserInfo), - http.errorString(), - error, - repr(http.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute))) - ) - self.show_error(error_msg, show_retry_button=True) + self.network_error(http, error) return try: tracks = document.metadata[0].recording_list[0].recording except (AttributeError, IndexError): - error_msg = _("No results found. Please try a different search query.") - self.show_error(error_msg) + self.no_results_found() return if self.file_: From 9abd7ccaa16234d4918f07114c076604b31acc66 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Wed, 27 Jul 2016 14:39:03 +0530 Subject: [PATCH 90/98] Remove unnecessary initialization --- picard/ui/searchdialog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 0c8f3ad27..74a16a4d8 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -345,7 +345,6 @@ class TrackSearchDialog(SearchDialog): def display_results(self): self.show_table(self.table_headers) - row = 0 for row, obj in enumerate(self.search_results): track = obj[0] table_item = QtGui.QTableWidgetItem From dd5ac9bc97c172481c787634c814ab2975274f2b Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Thu, 28 Jul 2016 14:58:17 +0530 Subject: [PATCH 91/98] Default value of `row` is no longer required From commit 8efcf98 onwards. --- picard/ui/searchdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 74a16a4d8..63e63a7af 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -382,7 +382,7 @@ class TrackSearchDialog(SearchDialog): track["album"] = _("Standalone Recording") self.search_results.append((track, node)) - def load_selection(self, row=None): + def load_selection(self, row): track, node = self.search_results[row] if track.get("musicbrainz_albumid"): # The track is not an NAT From 09a746105a262080ecae7620c1552c8804e9673e Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 1 Aug 2016 21:52:30 +0530 Subject: [PATCH 92/98] Rename actions to more specific one --- picard/ui/itemviews.py | 4 ++-- picard/ui/mainwindow.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index bacc084fb..93e388ab2 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -257,7 +257,7 @@ class BaseTreeView(QtGui.QTreeWidget): if obj.num_linked_files == 1: menu.addAction(self.window.play_file_action) menu.addAction(self.window.open_folder_action) - menu.addAction(self.window.more_results_action) + menu.addAction(self.window.tracks_search_action) plugin_actions.extend(_file_actions) menu.addAction(self.window.browser_lookup_action) menu.addSeparator() @@ -286,7 +286,7 @@ class BaseTreeView(QtGui.QTreeWidget): menu.addSeparator() menu.addAction(self.window.autotag_action) menu.addAction(self.window.analyze_action) - menu.addAction(self.window.more_results_action) + menu.addAction(self.window.tracks_search_action) plugin_actions = list(_file_actions) elif isinstance(obj, Album): if can_view_info: diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 8583a7932..c9e9d7bbe 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -382,9 +382,9 @@ class MainWindow(QtGui.QMainWindow): self.browser_lookup_action.setEnabled(False) self.browser_lookup_action.triggered.connect(self.browser_lookup) - self.more_results_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search similar tracks..."), self) - self.more_results_action.setStatusTip(_(u"View similar tracks and optionally choose a different release")) - self.more_results_action.triggered.connect(self.show_more_results) + self.tracks_search_action = QtGui.QAction(icontheme.lookup('system-search'), _(u"Search similar tracks..."), self) + self.tracks_search_action.setStatusTip(_(u"View similar tracks and optionally choose a different release")) + self.tracks_search_action.triggered.connect(self.show_more_tracks) self.show_file_browser_action = QtGui.QAction(_(u"File &Browser"), self) self.show_file_browser_action.setCheckable(True) @@ -802,7 +802,7 @@ class MainWindow(QtGui.QMainWindow): QtGui.QMessageBox.Yes) return ret == QtGui.QMessageBox.Yes - def show_more_results(self): + def show_more_tracks(self): obj = self.selected_objects[0] if isinstance(obj, Track): obj = obj.linked_files[0] From d1f516208a17bf93584ee7ed846ba5b78b04e73c Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 1 Aug 2016 22:05:06 +0530 Subject: [PATCH 93/98] Add docstrings and comments --- picard/ui/searchdialog.py | 56 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 63e63a7af..93b65c864 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -147,6 +147,12 @@ class SearchDialog(PicardDialog): self.verticalLayout.addWidget(self.buttonBox) def add_widget_to_center_layout(self, widget): + """Updates child widget of center_widget. + + Child widgets represent dialog's current state, like progress, + error, and displaying fetched results. + """ + wid = self.center_layout.itemAt(0) if wid: if wid.widget().objectName() == "results_table": @@ -155,6 +161,8 @@ class SearchDialog(PicardDialog): self.center_layout.addWidget(widget) def show_progress(self): + """Displays feedback while results are being fetched from server.""" + self.progress_widget = QtGui.QWidget(self) self.progress_widget.setObjectName("progress_widget") layout = QtGui.QVBoxLayout(self.progress_widget) @@ -172,6 +180,13 @@ class SearchDialog(PicardDialog): self.add_widget_to_center_layout(self.progress_widget) def show_error(self, error, show_retry_button=False): + """Displays error inside the dialog. + + Args: + error -- Error string + show_retry_button -- Whether to display retry button or not + """ + self.error_widget = QtGui.QWidget(self) self.error_widget.setObjectName("error_widget") layout = QtGui.QVBoxLayout(self.error_widget) @@ -194,6 +209,8 @@ class SearchDialog(PicardDialog): self.add_widget_to_center_layout(self.error_widget) def show_table(self, column_headers): + """Displays results table inside the dialog.""" + self.table = ResultTable(self.table_headers) self.table.setObjectName("results_table") self.table.cellDoubleClicked.connect(self.row_double_clicked) @@ -207,6 +224,8 @@ class SearchDialog(PicardDialog): enable_loading_button) def row_double_clicked(self, row): + """Handle function for double click event inside the table.""" + self.load_selection(row) self.accept() @@ -258,6 +277,12 @@ class SearchDialog(PicardDialog): header.setResizeMode(QtGui.QHeaderView.Interactive) def save_state(self, table_loaded=True): + """Saves dialog state i.e. window size, checkbox state, and table + header size. + + Args: + table_loaded -- Whether table widget is loaded or not + """ if table_loaded: self.save_table_header_state() config.persist["searchdialog_window_size"] = self.size() @@ -285,6 +310,7 @@ class TrackSearchDialog(SearchDialog): ] def search(self, text): + """Performs search using query provided by the user.""" self.retry_params = (self.search, text) self.search_box.search_edit.setText(text) self.show_progress() @@ -294,6 +320,8 @@ class TrackSearchDialog(SearchDialog): limit=25) def load_similar_tracks(self, file_): + """Performs search by using existing metadata information + from the file.""" self.retry_params = (self.load_similar_tracks, file_) self.file_ = file_ metadata = file_.orig_metadata @@ -307,9 +335,13 @@ class TrackSearchDialog(SearchDialog): 'isrc': metadata['isrc'], } if config.setting["use_adv_search_syntax"]: + # Display the query in advance syntax format. query_str = ' '.join(['%s:(%s)' % (item, value) for item, value in query.iteritems() if value]) else: + # Display only the track title query_str = query["track"] + # `query_str` is used only for presenting purpose. Actual query consists of all filters and follows + # advanced query syntax. query["limit"] = 25 self.search_box.search_edit.setText(query_str) self.show_progress() @@ -318,8 +350,15 @@ class TrackSearchDialog(SearchDialog): **query) def retry(self): - self.retry_params[0](self.retry_params[1]) + """Retries the search using information from `retry_params`. + `retry_params` is a tuple having search information. + retry_params[0] -- Method to be used to perform search + -- Can be `self.search()` or `self.load_similar_tracks()` + retry_params[1] -- Search query information + -- Can be a text string, or a File object + """ + self.retry_params[0](self.retry_params[1]) def handle_reply(self, document, http, error): if error: @@ -333,6 +372,7 @@ class TrackSearchDialog(SearchDialog): return if self.file_: + # Sort the results by comparing them to original metadata tags tmp = sorted((self.file_.orig_metadata.compare_to_track( track, File.comparison_weights) for track in tracks), reverse=True, @@ -358,6 +398,11 @@ class TrackSearchDialog(SearchDialog): self.table.setItem(row, 6, table_item(track.get("releasetype", ""))) def parse_tracks_from_xml(self, tracks_xml): + """Extracts track information from XmlNode objects and stores that into Metadata objects. + + Args: + tracks_xml -- list of XmlNode objects + """ for node in tracks_xml: if "release_list" in node.children and "release" in node.release_list[0].children: for rel_node in node.release_list[0].release: @@ -367,6 +412,9 @@ class TrackSearchDialog(SearchDialog): rg_node = rel_node.release_group[0] release_group_to_metadata(rg_node, track) if "release_event_list" in rel_node.children: + # Extract contries list from `release_event_list` element + # Don't use `country` element as it contains information of a single release + # event and is basically for backward compatibility. country = [] for re in rel_node.release_event_list[0].release_event: try: @@ -377,12 +425,18 @@ class TrackSearchDialog(SearchDialog): track["country"] = ", ".join(country) self.search_results.append((track, node)) else: + # This handles the case when no release is associated with a track + # i.e. the track is a NAT track = Metadata() recording_to_metadata(node, track) track["album"] = _("Standalone Recording") self.search_results.append((track, node)) def load_selection(self, row): + """Loads album corresponding to selected track. + If the search is performed for a file, also associates the file to + corresponding track in the album. + """ track, node = self.search_results[row] if track.get("musicbrainz_albumid"): # The track is not an NAT From 7f3c6afbaf294642e956bc9e3e502a737add051d Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Mon, 1 Aug 2016 22:53:55 +0530 Subject: [PATCH 94/98] Remove unnecessary import `artist_credit_from_node` is not required as `recording_to_metadata` uses it internally. --- picard/ui/searchdialog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 93b65c864..6b651e808 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -25,7 +25,6 @@ from picard.ui import PicardDialog from picard.ui.util import StandardButton, ButtonLineEdit from picard.util import format_time, icontheme from picard.mbxml import ( - artist_credit_from_node, recording_to_metadata, release_to_metadata, release_group_to_metadata From 62bd5126622b6ad05dd35a34fcd933131afcf509 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 6 Aug 2016 23:16:33 +0530 Subject: [PATCH 95/98] Improve code semantics and style Some noticeable points: * `save_state` can check whether table is loaded or not. No need to check it in `accept` and `reject`. Also `table_loaded` isn't required anymore. * Keep file formats list in alphabatic order, in `makeqrc.py`. * Import `Track` and use `isinstance` to check whether object belongs to it. --- picard/ui/searchdialog.py | 41 +++++++++++++++++---------------------- resources/makeqrc.py | 2 +- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 6b651e808..8001bc491 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -31,6 +31,7 @@ from picard.mbxml import ( ) from picard.i18n import ugettext_attr from picard.metadata import Metadata +from picard.track import Track class ResultTable(QtGui.QTableWidget): @@ -81,7 +82,7 @@ class SearchBox(QtGui.QWidget): self.adv_opt_row_layout.setContentsMargins(1, 1, 1, 1) self.adv_opt_row_layout.setSpacing(1) self.use_adv_search_syntax = QtGui.QCheckBox(self.adv_opt_row_widget) - self.use_adv_search_syntax.setText(_("Use advance query syntax")) + self.use_adv_search_syntax.setText(_("Use advanced query syntax")) self.adv_opt_row_layout.addWidget(self.use_adv_search_syntax) self.adv_syntax_help = QtGui.QLabel(self.adv_opt_row_widget) self.adv_syntax_help.setOpenExternalLinks(True) @@ -115,6 +116,7 @@ class SearchDialog(PicardDialog): def __init__(self, parent=None): PicardDialog.__init__(self, parent) self.search_results = [] + self.table = None self.setupUi() self.restore_state() @@ -229,8 +231,8 @@ class SearchDialog(PicardDialog): self.accept() def network_error(self, reply, error): - error_msg = _("Following error occurred while fetching results:
" - "Network request error for %s: %s (QT code %d, HTTP code %s)
" % ( + error_msg = _("Following error occurred while fetching results:

" + "Network request error for %s:
%s (QT code %d, HTTP code %s)
" % ( reply.request().url().toString(QtCore.QUrl.RemoveUserInfo), reply.errorString(), error, @@ -243,23 +245,16 @@ class SearchDialog(PicardDialog): self.show_error(error_msg) def accept(self): - if getattr(self, "table", None): + if self.table: sel_rows = self.table.selectionModel().selectedRows() if sel_rows: sel_row = sel_rows[0].row() self.load_selection(sel_row) - self.save_state(True) - else: - self.save_state(False) - + self.save_state() QtGui.QDialog.accept(self) def reject(self): - if getattr(self, "table", None): - self.save_state(True) - else: - self.save_state(False) - + self.save_state() QtGui.QDialog.reject(self) def restore_state(self): @@ -275,14 +270,12 @@ class SearchDialog(PicardDialog): header.restoreState(state) header.setResizeMode(QtGui.QHeaderView.Interactive) - def save_state(self, table_loaded=True): + def save_state(self): """Saves dialog state i.e. window size, checkbox state, and table header size. - - Args: - table_loaded -- Whether table widget is loaded or not """ - if table_loaded: + + if self.table: self.save_table_header_state() config.persist["searchdialog_window_size"] = self.size() self.search_box.save_checkbox_state() @@ -371,12 +364,14 @@ class TrackSearchDialog(SearchDialog): return if self.file_: - # Sort the results by comparing them to original metadata tags - tmp = sorted((self.file_.orig_metadata.compare_to_track( - track, File.comparison_weights) for track in tracks), + sorted_results = sorted( + (self.file_.orig_metadata.compare_to_track( + track, + File.comparison_weights) + for track in tracks), reverse=True, key=itemgetter(0)) - tracks = [item[3] for item in tmp] + tracks = [item[3] for item in sorted_results] del self.search_results[:] # Clear existing data self.parse_tracks_from_xml(tracks) @@ -444,7 +439,7 @@ class TrackSearchDialog(SearchDialog): if self.file_: # Search is performed for a file # Have to move that file from its existing album to the new one - if type(self.file_.parent).__name__ == "Track": + if isinstance(self.file_.parent, Track): album = self.file_.parent.album self.tagger.move_file_to_track(self.file_, track["musicbrainz_albumid"], track["musicbrainz_recordingid"]) if album._files == 0: diff --git a/resources/makeqrc.py b/resources/makeqrc.py index 63ec68911..9337a4cd3 100755 --- a/resources/makeqrc.py +++ b/resources/makeqrc.py @@ -37,7 +37,7 @@ def main(): topdir = os.path.abspath(os.path.join(scriptdir, "..")) resourcesdir = os.path.join(topdir, "resources") qrcfile = os.path.join(resourcesdir, "picard.qrc") - images = [i for i in find_files(resourcesdir, 'images', ['*.png', '*.gif'])] + images = [i for i in find_files(resourcesdir, 'images', ['*.gif', '*.png'])] newimages = 0 for filename in images: filepath = os.path.join(resourcesdir, filename) From c6b9273b43542f9bbca615d1c5ee25de8e113b67 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 6 Aug 2016 23:24:33 +0530 Subject: [PATCH 96/98] Use namedtuples instead of simple ones --- picard/ui/searchdialog.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 8001bc491..d792f3d02 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -19,6 +19,7 @@ from PyQt4 import QtGui, QtCore, QtNetwork from operator import itemgetter from functools import partial +from collections import namedtuple from picard import config from picard.file import File from picard.ui import PicardDialog @@ -106,6 +107,9 @@ class SearchBox(QtGui.QWidget): config.setting["use_adv_search_syntax"] = self.use_adv_search_syntax.isChecked() +Retry = namedtuple("Retry", ["function", "query"]) + + class SearchDialog(PicardDialog): options = [ @@ -303,7 +307,8 @@ class TrackSearchDialog(SearchDialog): def search(self, text): """Performs search using query provided by the user.""" - self.retry_params = (self.search, text) + + self.retry_params = Retry(self.search, text) self.search_box.search_edit.setText(text) self.show_progress() self.tagger.xmlws.find_tracks(self.handle_reply, @@ -314,7 +319,8 @@ class TrackSearchDialog(SearchDialog): def load_similar_tracks(self, file_): """Performs search by using existing metadata information from the file.""" - self.retry_params = (self.load_similar_tracks, file_) + + self.retry_params = Retry(self.load_similar_tracks, file_) self.file_ = file_ metadata = file_.orig_metadata query = { @@ -342,15 +348,8 @@ class TrackSearchDialog(SearchDialog): **query) def retry(self): - """Retries the search using information from `retry_params`. - - `retry_params` is a tuple having search information. - retry_params[0] -- Method to be used to perform search - -- Can be `self.search()` or `self.load_similar_tracks()` - retry_params[1] -- Search query information - -- Can be a text string, or a File object - """ - self.retry_params[0](self.retry_params[1]) + """Retries the search using information from `retry_params`.""" + self.retry_params.function(self.retry_params.query) def handle_reply(self, document, http, error): if error: From d9ebd73a9500e1036eed4990b2f83d3f9d755478 Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Sat, 6 Aug 2016 23:26:22 +0530 Subject: [PATCH 97/98] Escape lucene query from filter values --- picard/ui/searchdialog.py | 4 +++- picard/webservice.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index d792f3d02..9d0b16168 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -32,6 +32,7 @@ from picard.mbxml import ( ) from picard.i18n import ugettext_attr from picard.metadata import Metadata +from picard.webservice import escape_lucene_query from picard.track import Track @@ -334,7 +335,8 @@ class TrackSearchDialog(SearchDialog): } if config.setting["use_adv_search_syntax"]: # Display the query in advance syntax format. - query_str = ' '.join(['%s:(%s)' % (item, value) for item, value in query.iteritems() if value]) + query_str = ' '.join(['%s:(%s)' % (item, escape_lucene_query(value)) + for item, value in query.iteritems() if value]) else: # Display only the track title query_str = query["track"] diff --git a/picard/webservice.py b/picard/webservice.py index b4e554e0f..d174ae193 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -63,7 +63,7 @@ CLIENT_STRING = str(QUrl.toPercentEncoding('%s %s-%s' % (PICARD_ORG_NAME, PICARD_VERSION_STR))) -def _escape_lucene_query(text): +def escape_lucene_query(text): return re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\/])', r'\\\1', text) @@ -468,12 +468,12 @@ class XmlWebService(QtCore.QObject): if config.setting["use_adv_search_syntax"]: query = kwargs["query"] else: - query = _escape_lucene_query(kwargs["query"]).strip().lower() + query = escape_lucene_query(kwargs["query"]).strip().lower() filters.append(("dismax", 'true')) else: query = [] for name, value in kwargs.items(): - value = _escape_lucene_query(value).strip().lower() + value = escape_lucene_query(value).strip().lower() if value: query.append('%s:(%s)' % (name, value)) query = ' '.join(query) From d33ba59d079a368e1d14599d99172e1da23ffc9f Mon Sep 17 00:00:00 2001 From: Rahul Raturi Date: Fri, 12 Aug 2016 01:58:00 +0530 Subject: [PATCH 98/98] Fix multiple widgets appearing in center layout This happens with displaying error. New widgets may appear without existing being cleared. To avoid this: 1. Pass parent widget to `error_label` so it gets removed with parent. 2. Use `takeAt` rather than `itemAt` as it's more suitable for removing widgets according to Qt docs. For reference: http://doc.qt.io/qt-4.8/qlayout.html#itemAt and http://doc.qt.io/qt-4.8/qlayout.html#takeAt --- picard/ui/searchdialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/picard/ui/searchdialog.py b/picard/ui/searchdialog.py index 9d0b16168..c1d837d6b 100644 --- a/picard/ui/searchdialog.py +++ b/picard/ui/searchdialog.py @@ -159,7 +159,7 @@ class SearchDialog(PicardDialog): error, and displaying fetched results. """ - wid = self.center_layout.itemAt(0) + wid = self.center_layout.takeAt(0) if wid: if wid.widget().objectName() == "results_table": self.table = None @@ -196,7 +196,7 @@ class SearchDialog(PicardDialog): self.error_widget = QtGui.QWidget(self) self.error_widget.setObjectName("error_widget") layout = QtGui.QVBoxLayout(self.error_widget) - error_label = QtGui.QLabel(error) + error_label = QtGui.QLabel(error, self.error_widget) error_label.setWordWrap(True) error_label.setAlignment(QtCore.Qt.AlignCenter) error_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)