Get more results from MusicBrainz, and sort them by relevance.

We were stopping at the first one in AcoustidClient::RequestFinished.
To make things worse, it seems that results we received weren't sorted, so we sometimes ignored the most relevant one.
This commit is contained in:
Arnaud Bienner 2014-08-09 01:48:35 +02:00
parent 47108a9a68
commit 93b4b2caac
6 changed files with 121 additions and 53 deletions

View File

@ -19,6 +19,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QNetworkReply> #include <QNetworkReply>
#include <QStringList>
#include <qjson/parser.h> #include <qjson/parser.h>
@ -45,7 +46,7 @@ void AcoustidClient::Start(int id, const QString& fingerprint,
QList<Param> parameters; QList<Param> parameters;
parameters << Param("format", "json") << Param("client", kClientId) parameters << Param("format", "json") << Param("client", kClientId)
<< Param("duration", QString::number(duration_msec / kMsecPerSec)) << Param("duration", QString::number(duration_msec / kMsecPerSec))
<< Param("meta", "recordingids") << Param("meta", "recordingids+sources")
<< Param("fingerprint", fingerprint); << Param("fingerprint", fingerprint);
QUrl url(kUrl); QUrl url(kUrl);
@ -67,13 +68,29 @@ void AcoustidClient::CancelAll() {
requests_.clear(); requests_.clear();
} }
void AcoustidClient::RequestFinished(QNetworkReply* reply, int id) { namespace {
// Struct used when extracting results in RequestFinished
struct IdSource {
IdSource(const QString& id, int source)
: id_(id), nb_sources_(source) {}
bool operator<(const IdSource& other) const {
// We want the items with more sources to be at the beginning of the list
return nb_sources_ > other.nb_sources_;
}
QString id_;
int nb_sources_;
};
}
void AcoustidClient::RequestFinished(QNetworkReply* reply, int request_id) {
reply->deleteLater(); reply->deleteLater();
requests_.remove(id); requests_.remove(request_id);
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
200) { 200) {
emit Finished(id, QString()); emit Finished(request_id, QStringList());
return; return;
} }
@ -81,16 +98,26 @@ void AcoustidClient::RequestFinished(QNetworkReply* reply, int id) {
bool ok = false; bool ok = false;
QVariantMap result = parser.parse(reply, &ok).toMap(); QVariantMap result = parser.parse(reply, &ok).toMap();
if (!ok) { if (!ok) {
emit Finished(id, QString()); emit Finished(request_id, QStringList());
return; return;
} }
QString status = result["status"].toString(); QString status = result["status"].toString();
if (status != "ok") { if (status != "ok") {
emit Finished(id, QString()); emit Finished(request_id, QStringList());
return; return;
} }
// Get the results:
// -in a first step, gather ids and their corresponding number of sources
// -then sort results by number of sources (the results are originally
// unsorted but results with more sources are likely to be more accurate)
// -keep only the ids, as sources where useful only to sort the results
QVariantList results = result["results"].toList(); QVariantList results = result["results"].toList();
// List of <id, nb of sources> pairs
QList<IdSource> id_source_list;
for (const QVariant& v : results) { for (const QVariant& v : results) {
QVariantMap r = v.toMap(); QVariantMap r = v.toMap();
if (r.contains("recordings")) { if (r.contains("recordings")) {
@ -98,12 +125,18 @@ void AcoustidClient::RequestFinished(QNetworkReply* reply, int id) {
for (const QVariant& recording : recordings) { for (const QVariant& recording : recordings) {
QVariantMap o = recording.toMap(); QVariantMap o = recording.toMap();
if (o.contains("id")) { if (o.contains("id")) {
emit Finished(id, o["id"].toString()); id_source_list << IdSource(o["id"].toString(), o["sources"].toInt());
return;
} }
} }
} }
} }
emit Finished(id, QString()); qStableSort(id_source_list);
QList<QString> id_list;
for (const IdSource& is : id_source_list) {
id_list << is.id_;
}
emit Finished(request_id, id_list);
} }

View File

@ -56,7 +56,7 @@ class AcoustidClient : public QObject {
void CancelAll(); void CancelAll();
signals: signals:
void Finished(int id, const QString& mbid); void Finished(int id, const QStringList& mbid_list);
private slots: private slots:
void RequestFinished(QNetworkReply* reply, int id); void RequestFinished(QNetworkReply* reply, int id);

View File

@ -40,22 +40,26 @@ MusicBrainzClient::MusicBrainzClient(QObject* parent,
network_(network ? network : new NetworkAccessManager(this)), network_(network ? network : new NetworkAccessManager(this)),
timeouts_(new NetworkTimeouts(kDefaultTimeout, this)) {} timeouts_(new NetworkTimeouts(kDefaultTimeout, this)) {}
void MusicBrainzClient::Start(int id, const QString& mbid) { void MusicBrainzClient::Start(int id, const QStringList& mbid_list) {
typedef QPair<QString, QString> Param; typedef QPair<QString, QString> Param;
QList<Param> parameters; int request_number = 0;
parameters << Param("inc", "artists+releases+media"); for (const QString& mbid : mbid_list) {
QList<Param> parameters;
parameters << Param("inc", "artists+releases+media");
QUrl url(kTrackUrl + mbid); QUrl url(kTrackUrl + mbid);
url.setQueryItems(parameters); url.setQueryItems(parameters);
QNetworkRequest req(url); QNetworkRequest req(url);
QNetworkReply* reply = network_->get(req); QNetworkReply* reply = network_->get(req);
NewClosure(reply, SIGNAL(finished()), this, NewClosure(reply, SIGNAL(finished()), this,
SLOT(RequestFinished(QNetworkReply*, int)), reply, id); SLOT(RequestFinished(QNetworkReply*, int, int)),
requests_[id] = reply; reply, id, request_number++);
requests_.insert(id, reply);
timeouts_->AddReply(reply); timeouts_->AddReply(reply);
}
} }
void MusicBrainzClient::StartDiscIdRequest(const QString& discid) { void MusicBrainzClient::StartDiscIdRequest(const QString& discid) {
@ -141,31 +145,46 @@ void MusicBrainzClient::DiscIdRequestFinished(const QString& discid,
emit Finished(artist, album, UniqueResults(ret)); emit Finished(artist, album, UniqueResults(ret));
} }
void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id) { void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id, int request_number) {
reply->deleteLater(); reply->deleteLater();
requests_.remove(id);
ResultList ret;
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != const int nb_removed = requests_.remove(id, reply);
200) { if (nb_removed != 1) {
emit Finished(id, ret); qLog(Error) << "Error: unknown reply received:" << nb_removed <<
return; "requests removed, while only one was supposed to be removed";
} }
QXmlStreamReader reader(reply);
while (!reader.atEnd()) { if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() ==
if (reader.readNext() == QXmlStreamReader::StartElement && 200) {
reader.name() == "recording") { QXmlStreamReader reader(reply);
ResultList tracks = ParseTrack(&reader); ResultList res;
for (const Result& track : tracks) { while (!reader.atEnd()) {
if (!track.title_.isEmpty()) { if (reader.readNext() == QXmlStreamReader::StartElement &&
ret << track; reader.name() == "recording") {
ResultList tracks = ParseTrack(&reader);
for (const Result& track : tracks) {
if (!track.title_.isEmpty()) {
res << track;
}
} }
} }
} }
qSort(res);
pending_results_[id] << PendingResults(request_number, res);
} }
emit Finished(id, UniqueResults(ret)); // No more pending requests for this id: emit the results we have.
if (!requests_.contains(id)) {
// Merge the results we have
ResultList ret;
QList<PendingResults> result_list_list = pending_results_.take(id);
qSort(result_list_list);
for (const PendingResults& result_list : result_list_list) {
ret << result_list.results_;
}
emit Finished(id, UniqueResults(ret));
}
} }
bool MusicBrainzClient::MediumHasDiscid(const QString& discid, bool MusicBrainzClient::MediumHasDiscid(const QString& discid,
@ -326,9 +345,8 @@ MusicBrainzClient::Release MusicBrainzClient::ParseRelease(
return ret; return ret;
} }
MusicBrainzClient::ResultList MusicBrainzClient::UniqueResults( MusicBrainzClient::ResultList& MusicBrainzClient::UniqueResults(ResultList& results) {
const ResultList& results) { qStableSort(results);
ResultList ret = QSet<Result>::fromList(results).toList(); results.erase(std::unique(results.begin(), results.end()), results.end());
qSort(ret); return results;
return ret;
} }

View File

@ -19,7 +19,7 @@
#define MUSICBRAINZCLIENT_H #define MUSICBRAINZCLIENT_H
#include <QHash> #include <QHash>
#include <QMap> #include <QMultiMap>
#include <QObject> #include <QObject>
#include <QXmlStreamReader> #include <QXmlStreamReader>
@ -78,7 +78,7 @@ class MusicBrainzClient : public QObject {
// Starts a request and returns immediately. Finished() will be emitted // Starts a request and returns immediately. Finished() will be emitted
// later with the same ID. // later with the same ID.
void Start(int id, const QString& mbid); void Start(int id, const QStringList& mbid);
void StartDiscIdRequest(const QString& discid); void StartDiscIdRequest(const QString& discid);
// Cancels the request with the given ID. Finished() will never be emitted // Cancels the request with the given ID. Finished() will never be emitted
@ -97,7 +97,9 @@ signals:
const MusicBrainzClient::ResultList& result); const MusicBrainzClient::ResultList& result);
private slots: private slots:
void RequestFinished(QNetworkReply* reply, int id); // id identifies the track, and request_number means it's the
// 'request_number'th request for this track
void RequestFinished(QNetworkReply* reply, int id, int request_number);
void DiscIdRequestFinished(const QString& discid, QNetworkReply* reply); void DiscIdRequestFinished(const QString& discid, QNetworkReply* reply);
private: private:
@ -117,13 +119,26 @@ signals:
int year_; int year_;
}; };
struct PendingResults {
PendingResults(int sort_id, const ResultList& results)
: sort_id_(sort_id), results_(results) {}
bool operator<(const PendingResults& other) const {
return sort_id_ < other.sort_id_;
}
int sort_id_;
ResultList results_;
};
static bool MediumHasDiscid(const QString& discid, QXmlStreamReader* reader); static bool MediumHasDiscid(const QString& discid, QXmlStreamReader* reader);
static ResultList ParseMedium(QXmlStreamReader* reader); static ResultList ParseMedium(QXmlStreamReader* reader);
static Result ParseTrackFromDisc(QXmlStreamReader* reader); static Result ParseTrackFromDisc(QXmlStreamReader* reader);
static ResultList ParseTrack(QXmlStreamReader* reader); static ResultList ParseTrack(QXmlStreamReader* reader);
static void ParseArtist(QXmlStreamReader* reader, QString* artist); static void ParseArtist(QXmlStreamReader* reader, QString* artist);
static Release ParseRelease(QXmlStreamReader* reader); static Release ParseRelease(QXmlStreamReader* reader);
static ResultList UniqueResults(const ResultList& results); // Remove duplicate from the list. Returns a reference to the input parameter
static ResultList& UniqueResults(ResultList& results);
private: private:
static const char* kTrackUrl; static const char* kTrackUrl;
@ -133,7 +148,9 @@ signals:
QNetworkAccessManager* network_; QNetworkAccessManager* network_;
NetworkTimeouts* timeouts_; NetworkTimeouts* timeouts_;
QMap<int, QNetworkReply*> requests_; QMultiMap<int, QNetworkReply*> requests_;
// Results we received so far, kept here until all the replies are finished
QMap<int, QList<PendingResults>> pending_results_;
}; };
inline uint qHash(const MusicBrainzClient::Result& result) { inline uint qHash(const MusicBrainzClient::Result& result) {

View File

@ -32,8 +32,8 @@ TagFetcher::TagFetcher(QObject* parent)
fingerprint_watcher_(nullptr), fingerprint_watcher_(nullptr),
acoustid_client_(new AcoustidClient(this)), acoustid_client_(new AcoustidClient(this)),
musicbrainz_client_(new MusicBrainzClient(this)) { musicbrainz_client_(new MusicBrainzClient(this)) {
connect(acoustid_client_, SIGNAL(Finished(int, QString)), connect(acoustid_client_, SIGNAL(Finished(int, QStringList)),
SLOT(PuidFound(int, QString))); SLOT(PuidsFound(int, QStringList)));
connect(musicbrainz_client_, connect(musicbrainz_client_,
SIGNAL(Finished(int, MusicBrainzClient::ResultList)), SIGNAL(Finished(int, MusicBrainzClient::ResultList)),
SLOT(TagsFetched(int, MusicBrainzClient::ResultList))); SLOT(TagsFetched(int, MusicBrainzClient::ResultList)));
@ -92,20 +92,20 @@ void TagFetcher::FingerprintFound(int index) {
song.length_nanosec() / kNsecPerMsec); song.length_nanosec() / kNsecPerMsec);
} }
void TagFetcher::PuidFound(int index, const QString& puid) { void TagFetcher::PuidsFound(int index, const QStringList& puid_list) {
if (index >= songs_.count()) { if (index >= songs_.count()) {
return; return;
} }
const Song& song = songs_[index]; const Song& song = songs_[index];
if (puid.isEmpty()) { if (puid_list.isEmpty()) {
emit ResultAvailable(song, SongList()); emit ResultAvailable(song, SongList());
return; return;
} }
emit Progress(song, tr("Downloading metadata")); emit Progress(song, tr("Downloading metadata"));
musicbrainz_client_->Start(index, puid); musicbrainz_client_->Start(index, puid_list);
} }
void TagFetcher::TagsFetched(int index, void TagFetcher::TagsFetched(int index,

View File

@ -47,7 +47,7 @@ signals:
private slots: private slots:
void FingerprintFound(int index); void FingerprintFound(int index);
void PuidFound(int index, const QString& puid); void PuidsFound(int index, const QStringList& puid_list);
void TagsFetched(int index, const MusicBrainzClient::ResultList& result); void TagsFetched(int index, const MusicBrainzClient::ResultList& result);
private: private: