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