1# -*- coding: utf-8 -*- 2# 3# Picard, the next-generation MusicBrainz tagger 4# 5# Copyright (C) 2016 Rahul Raturi 6# Copyright (C) 2018 Antonio Larrosa 7# Copyright (C) 2018-2019 Laurent Monin 8# Copyright (C) 2018-2020 Philipp Wolfer 9# 10# This program is free software; you can redistribute it and/or 11# modify it under the terms of the GNU General Public License 12# as published by the Free Software Foundation; either version 2 13# of the License, or (at your option) any later version. 14# 15# This program is distributed in the hope that it will be useful, 16# but WITHOUT ANY WARRANTY; without even the implied warranty of 17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18# GNU General Public License for more details. 19# 20# You should have received a copy of the GNU General Public License 21# along with this program; if not, write to the Free Software 22# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 23 24 25from PyQt5 import QtCore 26 27from picard.config import Option 28from picard.const import QUERY_LIMIT 29from picard.file import File 30from picard.mbjson import ( 31 countries_from_node, 32 recording_to_metadata, 33 release_group_to_metadata, 34 release_to_metadata, 35) 36from picard.metadata import Metadata 37from picard.track import Track 38from picard.util import sort_by_similarity 39from picard.webservice.api_helpers import escape_lucene_query 40 41from picard.ui.searchdialog import ( 42 Retry, 43 SearchDialog, 44) 45 46 47class TrackSearchDialog(SearchDialog): 48 49 dialog_header_state = "tracksearchdialog_header_state" 50 51 options = [ 52 Option("persist", dialog_header_state, QtCore.QByteArray()) 53 ] 54 55 def __init__(self, parent): 56 super().__init__( 57 parent, 58 accept_button_title=_("Load into Picard"), 59 search_type="track") 60 self.file_ = None 61 self.setWindowTitle(_("Track Search Results")) 62 self.columns = [ 63 ('name', _("Name")), 64 ('length', _("Length")), 65 ('artist', _("Artist")), 66 ('release', _("Release")), 67 ('date', _("Date")), 68 ('country', _("Country")), 69 ('type', _("Type")), 70 ('score', _("Score")), 71 ] 72 73 def search(self, text): 74 """Perform search using query provided by the user.""" 75 self.retry_params = Retry(self.search, text) 76 self.search_box_text(text) 77 self.show_progress() 78 self.tagger.mb_api.find_tracks(self.handle_reply, 79 query=text, 80 search=True, 81 advanced_search=self.use_advanced_search, 82 limit=QUERY_LIMIT) 83 84 def load_similar_tracks(self, file_): 85 """Perform search using existing metadata information 86 from the file as query.""" 87 self.retry_params = Retry(self.load_similar_tracks, file_) 88 self.file_ = file_ 89 metadata = file_.orig_metadata 90 query = { 91 'track': metadata['title'], 92 'artist': metadata['artist'], 93 'release': metadata['album'], 94 'tnum': metadata['tracknumber'], 95 'tracks': metadata['totaltracks'], 96 'qdur': str(metadata.length // 2000), 97 'isrc': metadata['isrc'], 98 } 99 100 # Generate query to be displayed to the user (in search box). 101 # If advanced query syntax setting is enabled by user, display query in 102 # advanced syntax style. Otherwise display only track title. 103 if self.use_advanced_search: 104 query_str = ' '.join(['%s:(%s)' % (item, escape_lucene_query(value)) 105 for item, value in query.items() if value]) 106 else: 107 query_str = query["track"] 108 109 query["limit"] = QUERY_LIMIT 110 self.search_box_text(query_str) 111 self.show_progress() 112 self.tagger.mb_api.find_tracks( 113 self.handle_reply, 114 **query) 115 116 def retry(self): 117 self.retry_params.function(self.retry_params.query) 118 119 def handle_reply(self, document, http, error): 120 if error: 121 self.network_error(http, error) 122 return 123 124 try: 125 tracks = document['recordings'] 126 except (KeyError, TypeError): 127 self.no_results_found() 128 return 129 130 if self.file_: 131 metadata = self.file_.orig_metadata 132 133 def candidates(): 134 for track in tracks: 135 yield metadata.compare_to_track(track, File.comparison_weights) 136 137 tracks = [result.track for result in sort_by_similarity(candidates)] 138 139 del self.search_results[:] # Clear existing data 140 self.parse_tracks(tracks) 141 self.display_results() 142 143 def display_results(self): 144 self.prepare_table() 145 for row, obj in enumerate(self.search_results): 146 track = obj[0] 147 self.table.insertRow(row) 148 self.set_table_item(row, 'name', track, "title") 149 self.set_table_item(row, 'length', track, "~length", sortkey=track.length) 150 self.set_table_item(row, 'artist', track, "artist") 151 self.set_table_item(row, 'release', track, "album") 152 self.set_table_item(row, 'date', track, "date") 153 self.set_table_item(row, 'country', track, "country") 154 self.set_table_item(row, 'type', track, "releasetype") 155 self.set_table_item(row, 'score', track, "score") 156 self.show_table(sort_column='score') 157 158 def parse_tracks(self, tracks): 159 for node in tracks: 160 if "releases" in node: 161 for rel_node in node['releases']: 162 track = Metadata() 163 recording_to_metadata(node, track) 164 track['score'] = node['score'] 165 release_to_metadata(rel_node, track) 166 rg_node = rel_node['release-group'] 167 release_group_to_metadata(rg_node, track) 168 countries = countries_from_node(rel_node) 169 if countries: 170 track["country"] = ", ".join(countries) 171 self.search_results.append((track, node)) 172 else: 173 # This handles the case when no release is associated with a track 174 # i.e. the track is an NAT 175 track = Metadata() 176 recording_to_metadata(node, track) 177 track['score'] = node['score'] 178 track["album"] = _("Standalone Recording") 179 self.search_results.append((track, node)) 180 181 def accept_event(self, rows): 182 for row in rows: 183 self.load_selection(row) 184 185 def load_selection(self, row): 186 """Load the album corresponding to the selected track. 187 If the search is performed for a file, also associate the file to 188 corresponding track in the album. 189 """ 190 191 track, node = self.search_results[row] 192 if track.get("musicbrainz_albumid"): 193 # The track is not an NAT 194 self.tagger.get_release_group_by_id(track["musicbrainz_releasegroupid"]).loaded_albums.add( 195 track["musicbrainz_albumid"]) 196 if self.file_: 197 # Search is performed for a file. 198 # Have to move that file from its existing album to the new one. 199 if isinstance(self.file_.parent, Track): 200 album = self.file_.parent.album 201 self.tagger.move_file_to_track(self.file_, track["musicbrainz_albumid"], track["musicbrainz_recordingid"]) 202 if album.get_num_total_files() == 0: 203 # Remove album if it has no more files associated 204 self.tagger.remove_album(album) 205 else: 206 self.tagger.move_file_to_track(self.file_, track["musicbrainz_albumid"], track["musicbrainz_recordingid"]) 207 else: 208 # No files associated. Just a normal search. 209 self.tagger.load_album(track["musicbrainz_albumid"]) 210 else: 211 if self.file_ and getattr(self.file_.parent, 'album', None): 212 album = self.file_.parent.album 213 self.tagger.move_file_to_nat(self.file_, track["musicbrainz_recordingid"], node) 214 if album.get_num_total_files() == 0: 215 self.tagger.remove_album(album) 216 else: 217 self.tagger.load_nat(track["musicbrainz_recordingid"], node) 218 self.tagger.move_file_to_nat(self.file_, track["musicbrainz_recordingid"], node) 219