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