From ceb0f5ead48b950c2ba15627a01a60067a8bfd10 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sat, 15 Dec 2018 15:12:18 +0100 Subject: [PATCH] Add new musicbrainz album cover provider --- dist/debian/copyright | 4 + src/covermanager/musicbrainzcoverprovider.cpp | 193 ++++++++++++------ src/covermanager/musicbrainzcoverprovider.h | 24 ++- 3 files changed, 154 insertions(+), 67 deletions(-) diff --git a/dist/debian/copyright b/dist/debian/copyright index f6eb721fc..6d26505e7 100644 --- a/dist/debian/copyright +++ b/dist/debian/copyright @@ -46,12 +46,16 @@ Files: src/core/main.h src/settings/tidalsettingspage.h src/covermanager/lastfmcoverprovider.cpp src/covermanager/lastfmcoverprovider.h + src/covermanager/musicbrainzcoverprovider.cpp + src/covermanager/musicbrainzcoverprovider.h Copyright: 2012-2014, 2017-2018, Jonas Kvinge License: GPL-3+ Files: src/core/main.cpp src/core/mainwindow.cpp src/core/mainwindow.h + src/core/application.cpp + src/core/application.h src/core/player.cpp src/core/player.h src/core/song.cpp diff --git a/src/covermanager/musicbrainzcoverprovider.cpp b/src/covermanager/musicbrainzcoverprovider.cpp index 0e6f5daad..7af7978c3 100644 --- a/src/covermanager/musicbrainzcoverprovider.cpp +++ b/src/covermanager/musicbrainzcoverprovider.cpp @@ -1,7 +1,6 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2012, David Sansome + * Copyright 2018, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,16 +23,19 @@ #include #include -#include #include #include +#include #include #include #include #include #include #include -#include +#include +#include +#include +#include #include "core/closure.h" #include "core/network.h" @@ -42,91 +44,168 @@ #include "coverprovider.h" #include "musicbrainzcoverprovider.h" -using std::count_if; -using std::mem_fun; - const char *MusicbrainzCoverProvider::kReleaseSearchUrl = "https://musicbrainz.org/ws/2/release/"; const char *MusicbrainzCoverProvider::kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front"; +const int MusicbrainzCoverProvider::kLimit = 8; MusicbrainzCoverProvider::MusicbrainzCoverProvider(QObject *parent): CoverProvider("MusicBrainz", true, parent), network_(new NetworkAccessManager(this)) {} bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, int id) { QString query = QString("release:\"%1\" AND artist:\"%2\"").arg(album.trimmed().replace('"', "\\\"")).arg(artist.trimmed().replace('"', "\\\"")); + QUrlQuery url_query; url_query.addQueryItem("query", query); - url_query.addQueryItem("limit", "6"); + url_query.addQueryItem("limit", QString::number(kLimit)); + url_query.addQueryItem("fmt", "json"); + QUrl url(kReleaseSearchUrl); url.setQuery(url_query); QNetworkRequest request(url); QNetworkReply *reply = network_->get(request); - NewClosure(reply, SIGNAL(finished()), this, SLOT(ReleaseSearchFinished(QNetworkReply *, int)), reply, id); + NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply *, int)), reply, id); - cover_names_[id] = QString("%1 - %2").arg(artist, album); return true; } -void MusicbrainzCoverProvider::ReleaseSearchFinished(QNetworkReply *reply, int id) { +void MusicbrainzCoverProvider::CancelSearch(int id) {} + +void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, int search_id) { reply->deleteLater(); - QList releases; + CoverSearchResults results; - QByteArray data(reply->readAll()); - //qLog(Debug) << data; - QXmlStreamReader reader(data); - while (!reader.atEnd()) { - QXmlStreamReader::TokenType type = reader.readNext(); - if (type == QXmlStreamReader::StartElement && reader.name() == "release") { - QStringRef release_id = reader.attributes().value("id"); - if (!release_id.isEmpty()) { - releases.append(release_id.toString()); - } + QByteArray data = GetReplyData(reply); + if (data.isEmpty()) { + emit SearchFinished(search_id, results); + return; + } + + QJsonObject json_obj = ExtractJsonObj(data); + if (json_obj.isEmpty()) { + emit SearchFinished(search_id, results); + return; + } + + if (!json_obj.contains("releases")) { + if (json_obj.contains("error")) { + QString error = json_obj["error"].toString(); + Error(error); } - } - - for (const QString &release_id : releases) { - QUrl url(QString(kAlbumCoverUrl).arg(release_id)); - QNetworkReply *reply = network_->head(QNetworkRequest(url)); - image_checks_.insert(id, reply); - NewClosure(reply, SIGNAL(finished()), this, SLOT(ImageCheckFinished(int)), id); - } - -} - -void MusicbrainzCoverProvider::ImageCheckFinished(int id) { - - QList replies = image_checks_.values(id); - - int finished_count = std::count_if(replies.constBegin(), replies.constEnd(), mem_fun(&QNetworkReply::isFinished)); - if (finished_count == replies.size()) { - QString cover_name = cover_names_.take(id); - QList results; - for (QNetworkReply *reply : replies) { - reply->deleteLater(); - if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() < 400) { - CoverSearchResult result; - result.description = cover_name; - result.image_url = reply->url(); - results.append(result); - } + else { + Error(QString("Json reply is missing releases."), json_obj); } - image_checks_.remove(id); - emit SearchFinished(id, results); + emit SearchFinished(search_id, results); + return; } + QJsonValue json_value = json_obj["releases"]; + + if (!json_value.isArray()) { + Error("Json releases is not an array.", json_value); + emit SearchFinished(search_id, results); + return; + } + QJsonArray json_releases = json_value.toArray(); + + if (json_releases.isEmpty()) { + Error("Json releases array is empty.", json_value); + emit SearchFinished(search_id, results); + return; + } + + for (const QJsonValue &value : json_releases) { + if (!value.isObject()) { + Error("Invalid Json reply, album value is not an object.", value); + continue; + } + QJsonObject json_obj = value.toObject(); + if (!json_obj.contains("id") || !json_obj.contains("title")) { + Error("Invalid Json reply, album is missing id or title.", json_obj); + continue; + } + QString id = json_obj["id"].toString(); + QString title = json_obj["title"].toString(); + CoverSearchResult result; + QUrl url(QString(kAlbumCoverUrl).arg(id)); + result.description = title; + result.image_url = url; + results.append(result); + } + emit SearchFinished(search_id, results); } -void MusicbrainzCoverProvider::CancelSearch(int id) { +QByteArray MusicbrainzCoverProvider::GetReplyData(QNetworkReply *reply) { - QList replies = image_checks_.values(id); + QByteArray data; - for (QNetworkReply *reply : replies) { - reply->abort(); - reply->deleteLater(); + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + data = reply->readAll(); } - image_checks_.remove(id); + else { + if (reply->error() < 200) { + // This is a network error, there is nothing more to do. + QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + Error(failure_reason); + } + else { + // See if there is Json data containing "error" - then use that instead. + data = reply->readAll(); + QJsonParseError error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + QString failure_reason; + if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QJsonObject json_obj = json_doc.object(); + if (json_obj.contains("error")) { + failure_reason = json_obj["error"].toString(); + } + else { + failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + } + else { + failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + Error(failure_reason); + } + return QByteArray(); + } + + return data; } + +QJsonObject MusicbrainzCoverProvider::ExtractJsonObj(const QByteArray &data) { + + QJsonParseError error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + + if (error.error != QJsonParseError::NoError) { + Error("Reply from server is missing Json data.", data); + return QJsonObject(); + } + if (json_doc.isNull() || json_doc.isEmpty()) { + Error("Received empty Json document.", json_doc); + return QJsonObject(); + } + if (!json_doc.isObject()) { + Error("Json document is not an object.", json_doc); + return QJsonObject(); + } + QJsonObject json_obj = json_doc.object(); + if (json_obj.isEmpty()) { + Error("Received empty Json object.", json_doc); + return QJsonObject(); + } + + return json_obj; + +} + +void MusicbrainzCoverProvider::Error(QString error, QVariant debug) { + qLog(Error) << "Musicbrainz:" << error; + if (debug.isValid()) qLog(Debug) << debug; +} diff --git a/src/covermanager/musicbrainzcoverprovider.h b/src/covermanager/musicbrainzcoverprovider.h index aa23fe95f..65b4a4b75 100644 --- a/src/covermanager/musicbrainzcoverprovider.h +++ b/src/covermanager/musicbrainzcoverprovider.h @@ -1,7 +1,6 @@ /* * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2012, David Sansome + * Copyright 2018, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,32 +25,37 @@ #include #include -#include -#include +#include +#include #include #include #include +#include #include "coverprovider.h" +#include "albumcoverfetcher.h" class MusicbrainzCoverProvider : public CoverProvider { Q_OBJECT public: explicit MusicbrainzCoverProvider(QObject *parent = nullptr); - virtual bool StartSearch(const QString &artist, const QString &album, int id); - virtual void CancelSearch(int id); + bool StartSearch(const QString &artist, const QString &album, int id); + void CancelSearch(int id); private slots: - void ReleaseSearchFinished(QNetworkReply *reply, int id); - void ImageCheckFinished(int id); + void HandleSearchReply(QNetworkReply *reply, int search_id); + + private: + QByteArray GetReplyData(QNetworkReply *reply); + QJsonObject ExtractJsonObj(const QByteArray &data); + void Error(QString error, QVariant debug = QVariant()); private: static const char *kReleaseSearchUrl; static const char *kAlbumCoverUrl; + static const int kLimit; QNetworkAccessManager *network_; - QMultiMap image_checks_; - QMap cover_names_; };