1 /* This file is part of Clementine.
2    Copyright 2010, David Sansome <me@davidsansome.com>
3 
4    Clementine is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    Clementine is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
16 */
17 
18 #include "musicbrainzclient.h"
19 
20 #include <algorithm>
21 
22 #include <QCoreApplication>
23 #include <QNetworkReply>
24 #include <QSet>
25 #include <QXmlStreamReader>
26 #include <QUrlQuery>
27 
28 #include "core/closure.h"
29 #include "core/logging.h"
30 #include "core/network.h"
31 #include "core/utilities.h"
32 
33 const char* MusicBrainzClient::kTrackUrl =
34     "https://musicbrainz.org/ws/2/recording/";
35 const char* MusicBrainzClient::kDiscUrl =
36     "https://musicbrainz.org/ws/2/discid/";
37 const char* MusicBrainzClient::kDateRegex = "^[12]\\d{3}";
38 const int MusicBrainzClient::kDefaultTimeout = 5000;  // msec
39 const int MusicBrainzClient::kMaxRequestPerTrack = 3;
40 
MusicBrainzClient(QObject * parent,QNetworkAccessManager * network)41 MusicBrainzClient::MusicBrainzClient(QObject* parent,
42                                      QNetworkAccessManager* network)
43     : QObject(parent),
44       network_(network ? network : new NetworkAccessManager(this)),
45       timeouts_(new NetworkTimeouts(kDefaultTimeout, this)) {}
46 
Start(int id,const QStringList & mbid_list)47 void MusicBrainzClient::Start(int id, const QStringList& mbid_list) {
48   typedef QPair<QString, QString> Param;
49 
50   int request_number = 0;
51   for (const QString& mbid : mbid_list) {
52     QList<Param> parameters;
53     parameters << Param("inc", "artists+releases+media");
54 
55     QUrl url(kTrackUrl + mbid);
56     QUrlQuery url_query;
57     url_query.setQueryItems(parameters);
58     url.setQuery(url_query);
59     QNetworkRequest req(url);
60 
61     QNetworkReply* reply = network_->get(req);
62     NewClosure(reply, SIGNAL(finished()), this,
63                SLOT(RequestFinished(QNetworkReply*, int, int)), reply, id,
64                request_number++);
65     requests_.insert(id, reply);
66 
67     timeouts_->AddReply(reply);
68 
69     if (request_number >= kMaxRequestPerTrack) {
70       break;
71     }
72   }
73 }
74 
StartDiscIdRequest(const QString & discid)75 void MusicBrainzClient::StartDiscIdRequest(const QString& discid) {
76   typedef QPair<QString, QString> Param;
77 
78   QList<Param> parameters;
79   parameters << Param("inc", "artists+recordings");
80 
81   QUrl url(kDiscUrl + discid);
82   QUrlQuery url_query;
83   url_query.setQueryItems(parameters);
84   url.setQuery(url_query);
85   QNetworkRequest req(url);
86 
87   QNetworkReply* reply = network_->get(req);
88   NewClosure(reply, SIGNAL(finished()), this,
89              SLOT(DiscIdRequestFinished(const QString&, QNetworkReply*)),
90              discid, reply);
91 
92   timeouts_->AddReply(reply);
93 }
94 
Cancel(int id)95 void MusicBrainzClient::Cancel(int id) { delete requests_.take(id); }
96 
CancelAll()97 void MusicBrainzClient::CancelAll() {
98   qDeleteAll(requests_.values());
99   requests_.clear();
100 }
101 
DiscIdRequestFinished(const QString & discid,QNetworkReply * reply)102 void MusicBrainzClient::DiscIdRequestFinished(const QString& discid,
103                                               QNetworkReply* reply) {
104   reply->deleteLater();
105 
106   ResultList ret;
107   QString artist;
108   QString album;
109   int year = 0;
110 
111   if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
112       200) {
113     qLog(Error) << "Error:"
114                 << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)
115                        .toInt() << "http status code received";
116     qLog(Error) << reply->readAll();
117     emit Finished(artist, album, ret);
118     return;
119   }
120 
121   // Parse xml result:
122   // -get title
123   // -get artist
124   // -get year
125   // -get all the tracks' tags
126   // Note: If there are multiple releases for the discid, the first
127   // release is chosen.
128   QXmlStreamReader reader(reply);
129   while (!reader.atEnd()) {
130     QXmlStreamReader::TokenType type = reader.readNext();
131     if (type == QXmlStreamReader::StartElement) {
132       QStringRef name = reader.name();
133       if (name == "title") {
134         album = reader.readElementText();
135       } else if (name == "date") {
136         QRegExp regex(kDateRegex);
137         if (regex.indexIn(reader.readElementText()) == 0) {
138           year = regex.cap(0).toInt();
139         }
140       } else if (name == "artist-credit") {
141         ParseArtist(&reader, &artist);
142       } else if (name == "medium-list") {
143         break;
144       }
145     }
146   }
147 
148   while (!reader.atEnd()) {
149     QXmlStreamReader::TokenType token = reader.readNext();
150     if (token == QXmlStreamReader::StartElement && reader.name() == "medium") {
151       // Get the medium with a matching discid.
152       if (MediumHasDiscid(discid, &reader)) {
153         ResultList tracks = ParseMedium(&reader);
154         for (const Result& track : tracks) {
155           if (!track.title_.isEmpty()) {
156             ret << track;
157           }
158         }
159       } else {
160         Utilities::ConsumeCurrentElement(&reader);
161       }
162     } else if (token == QXmlStreamReader::EndElement &&
163                reader.name() == "medium-list") {
164       break;
165     }
166   }
167 
168   // If we parsed a year, copy it to the tracks.
169   if (year > 0) {
170     for (ResultList::iterator it = ret.begin(); it != ret.end(); ++it) {
171       it->year_ = year;
172     }
173   }
174 
175   emit Finished(artist, album, UniqueResults(ret, SortResults));
176 }
177 
RequestFinished(QNetworkReply * reply,int id,int request_number)178 void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id,
179                                         int request_number) {
180   reply->deleteLater();
181 
182   const int nb_removed = requests_.remove(id, reply);
183   if (nb_removed != 1) {
184     qLog(Error)
185         << "Error: unknown reply received:" << nb_removed
186         << "requests removed, while only one was supposed to be removed";
187   }
188 
189   if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() ==
190       200) {
191     QXmlStreamReader reader(reply);
192     ResultList res;
193     while (!reader.atEnd()) {
194       if (reader.readNext() == QXmlStreamReader::StartElement &&
195           reader.name() == "recording") {
196         ResultList tracks = ParseTrack(&reader);
197         for (const Result& track : tracks) {
198           if (!track.title_.isEmpty()) {
199             res << track;
200           }
201         }
202       }
203     }
204     pending_results_[id] << PendingResults(request_number, res);
205   } else {
206     qLog(Error) << "Error:"
207                 << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)
208                        .toInt() << "http status code received";
209     qLog(Error) << reply->readAll();
210   }
211 
212   // No more pending requests for this id: emit the results we have.
213   if (!requests_.contains(id)) {
214     // Merge the results we have
215     ResultList ret;
216     QList<PendingResults> result_list_list = pending_results_.take(id);
217     std::sort(result_list_list.begin(), result_list_list.end());
218     for (const PendingResults& result_list : result_list_list) {
219       ret << result_list.results_;
220     }
221     emit Finished(id, UniqueResults(ret, KeepOriginalOrder));
222   }
223 }
224 
MediumHasDiscid(const QString & discid,QXmlStreamReader * reader)225 bool MusicBrainzClient::MediumHasDiscid(const QString& discid,
226                                         QXmlStreamReader* reader) {
227   while (!reader->atEnd()) {
228     QXmlStreamReader::TokenType type = reader->readNext();
229 
230     if (type == QXmlStreamReader::StartElement && reader->name() == "disc" &&
231         reader->attributes().value("id").toString() == discid) {
232       return true;
233     } else if (type == QXmlStreamReader::EndElement &&
234                reader->name() == "disc-list") {
235       return false;
236     }
237   }
238   qLog(Debug) << "Reached end of xml stream without encountering </disc-list>";
239   return false;
240 }
241 
ParseMedium(QXmlStreamReader * reader)242 MusicBrainzClient::ResultList MusicBrainzClient::ParseMedium(
243     QXmlStreamReader* reader) {
244   ResultList ret;
245   while (!reader->atEnd()) {
246     QXmlStreamReader::TokenType type = reader->readNext();
247 
248     if (type == QXmlStreamReader::StartElement) {
249       if (reader->name() == "track") {
250         Result result;
251         result = ParseTrackFromDisc(reader);
252         ret << result;
253       }
254     }
255 
256     if (type == QXmlStreamReader::EndElement &&
257         reader->name() == "track-list") {
258       break;
259     }
260   }
261 
262   return ret;
263 }
264 
ParseTrackFromDisc(QXmlStreamReader * reader)265 MusicBrainzClient::Result MusicBrainzClient::ParseTrackFromDisc(
266     QXmlStreamReader* reader) {
267   Result result;
268 
269   while (!reader->atEnd()) {
270     QXmlStreamReader::TokenType type = reader->readNext();
271 
272     if (type == QXmlStreamReader::StartElement) {
273       QStringRef name = reader->name();
274       if (name == "position") {
275         result.track_ = reader->readElementText().toInt();
276       } else if (name == "length") {
277         result.duration_msec_ = reader->readElementText().toInt();
278       } else if (name == "title") {
279         result.title_ = reader->readElementText();
280       }
281     }
282 
283     if (type == QXmlStreamReader::EndElement && reader->name() == "track") {
284       break;
285     }
286   }
287 
288   return result;
289 }
290 
ParseTrack(QXmlStreamReader * reader)291 MusicBrainzClient::ResultList MusicBrainzClient::ParseTrack(
292     QXmlStreamReader* reader) {
293   Result result;
294   QList<Release> releases;
295 
296   while (!reader->atEnd()) {
297     QXmlStreamReader::TokenType type = reader->readNext();
298 
299     if (type == QXmlStreamReader::StartElement) {
300       QStringRef name = reader->name();
301 
302       if (name == "title") {
303         result.title_ = reader->readElementText();
304       } else if (name == "length") {
305         result.duration_msec_ = reader->readElementText().toInt();
306       } else if (name == "artist-credit") {
307         ParseArtist(reader, &result.artist_);
308       } else if (name == "release") {
309         releases << ParseRelease(reader);
310       }
311     }
312 
313     if (type == QXmlStreamReader::EndElement && reader->name() == "recording") {
314       break;
315     }
316   }
317 
318   ResultList ret;
319   if (releases.isEmpty()) {
320     ret << result;
321   } else {
322     std::stable_sort(releases.begin(), releases.end());
323     for (const Release& release : releases) {
324       ret << release.CopyAndMergeInto(result);
325     }
326   }
327   return ret;
328 }
329 
330 // Parse the artist. Multiple artists are joined together with the
331 // joinphrase from musicbrainz.
ParseArtist(QXmlStreamReader * reader,QString * artist)332 void MusicBrainzClient::ParseArtist(QXmlStreamReader* reader, QString* artist) {
333   QString join_phrase;
334   while (!reader->atEnd()) {
335     QXmlStreamReader::TokenType type = reader->readNext();
336 
337     if (type == QXmlStreamReader::StartElement &&
338         reader->name() == "name-credit") {
339       join_phrase = reader->attributes().value("joinphrase").toString();
340     }
341 
342     if (type == QXmlStreamReader::StartElement && reader->name() == "name") {
343       *artist += reader->readElementText() + join_phrase;
344     }
345 
346     if (type == QXmlStreamReader::EndElement &&
347         reader->name() == "artist-credit") {
348       return;
349     }
350   }
351 }
352 
ParseRelease(QXmlStreamReader * reader)353 MusicBrainzClient::Release MusicBrainzClient::ParseRelease(
354     QXmlStreamReader* reader) {
355   Release ret;
356 
357   while (!reader->atEnd()) {
358     QXmlStreamReader::TokenType type = reader->readNext();
359 
360     if (type == QXmlStreamReader::StartElement) {
361       QStringRef name = reader->name();
362       if (name == "title") {
363         ret.album_ = reader->readElementText();
364       } else if (name == "status") {
365         ret.SetStatusFromString(reader->readElementText());
366       } else if (name == "date") {
367         QRegExp regex(kDateRegex);
368         if (regex.indexIn(reader->readElementText()) == 0) {
369           ret.year_ = regex.cap(0).toInt();
370         }
371       } else if (name == "track-list") {
372         ret.track_ =
373             reader->attributes().value("offset").toString().toInt() + 1;
374         Utilities::ConsumeCurrentElement(reader);
375       }
376     }
377 
378     if (type == QXmlStreamReader::EndElement && reader->name() == "release") {
379       break;
380     }
381   }
382 
383   return ret;
384 }
385 
UniqueResults(const ResultList & results,UniqueResultsSortOption opt)386 MusicBrainzClient::ResultList MusicBrainzClient::UniqueResults(
387     const ResultList& results, UniqueResultsSortOption opt) {
388   ResultList ret;
389   if (opt == SortResults) {
390     ret = QSet<Result>::fromList(results).toList();
391     std::sort(ret.begin(), ret.end());
392   } else {  // KeepOriginalOrder
393     // Qt doesn't provide a ordered set (QSet "stores values in an unspecified
394     // order" according to Qt documentation).
395     // We might use std::set instead, but it's probably faster to use ResultList
396     // directly to avoid converting from one structure to another.
397     for (const Result& res : results) {
398       if (!ret.contains(res)) {
399         ret << res;
400       }
401     }
402   }
403   return ret;
404 }
405