Improve cover providers score system

This commit is contained in:
Jonas Kvinge 2019-04-17 22:18:03 +02:00
parent ca928bdacb
commit 63d5018ad6
14 changed files with 190 additions and 66 deletions

View File

@ -48,12 +48,12 @@ AlbumCoverFetcher::AlbumCoverFetcher(CoverProviders *cover_providers, QObject *p
quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album, bool fetchall) {
CoverSearchRequest request;
request.id = next_id_++;
request.artist = artist;
request.album = album;
request.album.remove(Song::kAlbumRemoveDisc);
request.album.remove(Song::kAlbumRemoveMisc);
request.search = false;
request.id = next_id_++;
request.fetchall = fetchall;
AddRequest(request);
@ -64,12 +64,12 @@ quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString
quint64 AlbumCoverFetcher::SearchForCovers(const QString &artist, const QString &album) {
CoverSearchRequest request;
request.id = next_id_++;
request.artist = artist;
request.album = album;
request.album.remove(Song::kAlbumRemoveDisc);
request.album.remove(Song::kAlbumRemoveMisc);
request.search = true;
request.id = next_id_++;
request.fetchall = false;
AddRequest(request);

View File

@ -60,16 +60,20 @@ struct CoverSearchRequest {
};
// This structure represents a single result of some album's cover search request.
// It contains an URL that leads to a found cover plus its description (usually the "artist - album" string).
struct CoverSearchResult {
// Used for grouping in the user interface. This is set automatically - don't set it manually in your cover provider.
// Used for grouping in the user interface.
QString provider;
// Description of this result (we suggest using the "artist - album" format)
QString description;
// Artist and album returned by the provider
QString artist;
QString album;
// An URL of a cover image described by this CoverSearchResult
// An URL of a cover image
QUrl image_url;
// Total score for this result
float score;
};
Q_DECLARE_METATYPE(CoverSearchResult);

View File

@ -43,12 +43,15 @@
#include "coverprovider.h"
#include "coverproviders.h"
using std::min;
using std::max;
using std::stable_sort;
using std::sqrt;
const int AlbumCoverFetcherSearch::kSearchTimeoutMs = 25000;
const int AlbumCoverFetcherSearch::kImageLoadTimeoutMs = 3000;
const int AlbumCoverFetcherSearch::kTargetSize = 500;
const float AlbumCoverFetcherSearch::kGoodScore = 1.85;
const float AlbumCoverFetcherSearch::kGoodScore = 4;
AlbumCoverFetcherSearch::AlbumCoverFetcherSearch(
const CoverSearchRequest &request, QNetworkAccessManager *network, QObject *parent)
@ -107,13 +110,19 @@ static bool CompareProviders(const CoverSearchResult &a, const CoverSearchResult
void AlbumCoverFetcherSearch::ProviderSearchFinished(int id, const QList<CoverSearchResult> &results) {
if (!pending_requests_.contains(id)) return;
CoverProvider *provider = pending_requests_.take(id);
CoverSearchResults results_copy(results);
// Set categories on the results
for (int i = 0; i < results_copy.count(); ++i) {
results_copy[i].provider = provider->name();
results_copy[i].score = provider->quality();
if (results_copy[i].artist == request_.artist) {
results_copy[i].score += 0.5;
}
if (results_copy[i].album == request_.album) { results_copy[i].score += 0.5; }
if (results_copy[i].artist != request_.artist && results_copy[i].album != request_.album) {
results_copy[i].score -= 1.0;
}
}
// Add results from the current provider to our pool
@ -126,6 +135,7 @@ void AlbumCoverFetcherSearch::ProviderSearchFinished(int id, const QList<CoverSe
}
AllProvidersFinished();
}
void AlbumCoverFetcherSearch::AllProvidersFinished() {
@ -171,7 +181,7 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
RedirectFollower *image_reply = new RedirectFollower(network_->get(QNetworkRequest(result.image_url)));
NewClosure(image_reply, SIGNAL(finished()), this, SLOT(ProviderCoverFetchFinished(RedirectFollower*)), image_reply);
pending_image_loads_[image_reply] = result.provider;
pending_image_loads_[image_reply] = result;
image_load_timeout_->AddReply(image_reply);
statistics_.network_requests_made_++;
@ -187,7 +197,9 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
void AlbumCoverFetcherSearch::ProviderCoverFetchFinished(RedirectFollower *reply) {
reply->deleteLater();
const QString provider = pending_image_loads_.take(reply);
if (!pending_image_loads_.contains(reply)) return;
CoverSearchResult result = pending_image_loads_.take(reply);
statistics_.bytes_transferred_ += reply->bytesAvailable();
@ -196,18 +208,20 @@ void AlbumCoverFetcherSearch::ProviderCoverFetchFinished(RedirectFollower *reply
}
if (reply->error() != QNetworkReply::NoError) {
qLog(Info) << "Error requesting" << reply->url() << reply->errorString();
qLog(Error) << "Error requesting" << reply->url() << reply->errorString();
}
else {
QImage image;
if (!image.loadFromData(reply->readAll())) {
qLog(Info) << "Error decoding image data from" << reply->url();
if (image.loadFromData(reply->readAll())) {
result.score += ScoreImage(image);
candidate_images_.insertMulti(result.score, CandidateImage(result, image));
qLog(Debug) << reply->url() << "from" << result.provider << "scored" << result.score;
}
else {
const float score = ScoreImage(image);
candidate_images_.insertMulti(score, CandidateImage(provider, image));
qLog(Debug) << reply->url() << "scored" << score;
qLog(Error) << "Error decoding image data from" << reply->url();
}
}
@ -241,7 +255,7 @@ float AlbumCoverFetcherSearch::ScoreImage(const QImage &image) const {
const float size_score = std::sqrt(float(image.width() * image.height())) / kTargetSize;
// A 1:1 image scores 1.0, anything else scores less
const float aspect_score = 1.0 - float(image.height() - image.width()) / std::max(image.height(), image.width());
const float aspect_score = 1.0 - float(std::max(image.width(), image.height()) - std::min(image.width(), image.height())) / std::max(image.height(), image.width());
return size_score + aspect_score;
@ -255,7 +269,9 @@ void AlbumCoverFetcherSearch::SendBestImage() {
const CandidateImage best_image = candidate_images_.values().back();
image = best_image.second;
statistics_.chosen_images_by_provider_[best_image.first]++;
qLog(Info) << "Using " << best_image.first.image_url << "from" << best_image.first.provider << "with score" << best_image.first.score;
statistics_.chosen_images_by_provider_[best_image.first.provider]++;
statistics_.chosen_images_++;
statistics_.chosen_width_ += image.width();
statistics_.chosen_height_ += image.height();

View File

@ -92,11 +92,11 @@ class AlbumCoverFetcherSearch : public QObject {
CoverSearchResults results_;
QMap<int, CoverProvider*> pending_requests_;
QMap<RedirectFollower*, QString> pending_image_loads_;
QMap<RedirectFollower*, CoverSearchResult> pending_image_loads_;
NetworkTimeouts* image_load_timeout_;
// QMap is sorted by key (score). Values are (provider_name, image)
typedef QPair<QString, QImage> CandidateImage;
// QMap is sorted by key (score). Values are (result, image)
typedef QPair<CoverSearchResult, QImage> CandidateImage;
QMap<float, CandidateImage> candidate_images_;
QNetworkAccessManager *network_;

View File

@ -81,6 +81,7 @@
#include "ui_albumcovermanager.h"
using std::unique_ptr;
using std::stable_sort;
const char *AlbumCoverManager::kSettingsGroup = "CoverManager";

View File

@ -216,7 +216,7 @@ void AlbumCoverSearcher::SearchFinished(quint64 id, const CoverSearchResults &re
QStandardItem *item = new QStandardItem;
item->setIcon(no_cover_icon_);
item->setText(result.description);
item->setText(result.artist + " - " + result.album);
item->setData(result.image_url, Role_ImageURL);
item->setData(id, Role_ImageRequestId);
item->setData(false, Role_ImageFetchFinished);

View File

@ -26,5 +26,5 @@
#include "core/application.h"
#include "coverprovider.h"
CoverProvider::CoverProvider(const QString &name, const bool &fetchall, Application *app, QObject *parent)
: QObject(parent), app_(app), name_(name), fetchall_(fetchall) {}
CoverProvider::CoverProvider(const QString &name, const float &quality, const bool &fetchall, Application *app, QObject *parent)
: QObject(parent), app_(app), name_(name), quality_(quality), fetchall_(fetchall) {}

View File

@ -38,10 +38,11 @@ class CoverProvider : public QObject {
Q_OBJECT
public:
explicit CoverProvider(const QString &name, const bool &fetchall, Application *app, QObject *parent);
explicit CoverProvider(const QString &name, const float &quality, const bool &fetchall, Application *app, QObject *parent);
// A name (very short description) of this provider, like "last.fm".
QString name() const { return name_; }
bool quality() const { return quality_; }
bool fetchall() const { return fetchall_; }
// Starts searching for covers matching the given query text.
@ -57,6 +58,7 @@ class CoverProvider : public QObject {
private:
Application *app_;
QString name_;
float quality_;
bool fetchall_;
};

View File

@ -48,7 +48,7 @@
const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com";
const int DeezerCoverProvider::kLimit = 10;
DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", true, app, parent), network_(new NetworkAccessManager(this)) {}
DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", 2.0, true, app, parent), network_(new NetworkAccessManager(this)) {}
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
@ -269,8 +269,12 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, int id) {
}
QUrl url(cover);
album.remove(Song::kAlbumRemoveDisc);
album.remove(Song::kAlbumRemoveMisc);
CoverSearchResult cover_result;
cover_result.description = artist + " " + album;
cover_result.artist = artist;
cover_result.album = album;
cover_result.image_url = url;
results << cover_result;

View File

@ -54,7 +54,7 @@ const char *DiscogsCoverProvider::kUrlReleases = "https://api.discogs.com/releas
const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk=";
const char *DiscogsCoverProvider::kSecretKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI=";
DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", false, app, parent), network_(new NetworkAccessManager(this)) {}
DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", 0.0, false, app, parent), network_(new NetworkAccessManager(this)) {}
bool DiscogsCoverProvider::StartSearch(const QString &artist, const QString &album, int s_id) {
@ -76,7 +76,7 @@ void DiscogsCoverProvider::CancelSearch(int id) {
}
bool DiscogsCoverProvider::StartRelease(DiscogsCoverSearchContext *s_ctx, int r_id, QString resource_url) {
bool DiscogsCoverProvider::StartRelease(DiscogsCoverSearchContext *s_ctx, int r_id, QString &description, QString &resource_url) {
DiscogsCoverReleaseContext *r_ctx = new DiscogsCoverReleaseContext;
@ -225,7 +225,7 @@ QJsonObject DiscogsCoverProvider::ExtractJsonObj(const QByteArray &data) {
Error("Reply from server missing Json data.", data);
return QJsonObject();
}
if (json_doc.isNull() || json_doc.isEmpty()) {
if (json_doc.isEmpty()) {
Error("Received empty Json document.", json_doc);
return QJsonObject();
}
@ -307,7 +307,7 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, int s_id) {
QString title = json_obj["title"].toString();
QString resource_url = json_obj["resource_url"].toString();
if (resource_url.isEmpty()) continue;
StartRelease(s_ctx, r_id, resource_url);
StartRelease(s_ctx, r_id, title, resource_url);
i++;
}
@ -337,41 +337,96 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, int s_id, in
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
EndSearch(s_ctx);
return;
}
QJsonValue json_value = ExtractData(data, "images", true);
if (!json_value.isArray()) {
EndSearch(s_ctx, r_ctx);
return;
}
QJsonArray json_images = json_value.toArray();
if (json_images.isEmpty()) {
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
EndSearch(s_ctx, r_ctx);
return;
}
if (!json_obj.contains("artists") || !json_obj.contains("title") || !json_obj.contains("images")) {
Error("Json reply is missing artists, title or images.");
EndSearch(s_ctx, r_ctx);
return;
}
QJsonValue json_artists = json_obj["artists"];
if (!json_artists.isArray()) {
Error("Json artists is not a array.", json_artists);
EndSearch(s_ctx, r_ctx);
return;
}
QJsonArray json_array_artists = json_artists.toArray();
int i = 0;
QString artist;
for (const QJsonValue &value : json_array_artists) {
if (!value.isObject()) {
Error("Invalid Json reply, value is not a object.", value);
continue;
}
QJsonObject json_obj_artist = value.toObject();
if (!json_obj_artist.contains("name") ) {
Error("Invalid Json reply, artists is missing name.", json_obj_artist);
continue;
}
artist = json_obj_artist["name"].toString();
++i;
if (artist == s_ctx->artist) break;
}
if (artist.isEmpty()) {
EndSearch(s_ctx, r_ctx);
return;
}
if (i > 1 && artist != s_ctx->artist) artist = "Various artists";
QString album = json_obj["title"].toString();
if (artist != s_ctx->artist && album != s_ctx->album) {
EndSearch(s_ctx, r_ctx);
return;
}
QJsonValue json_images = json_obj["images"];
if (!json_images.isArray()) {
Error("Json images is not an array.");
EndSearch(s_ctx, r_ctx);
return;
}
QJsonArray json_array_images = json_images.toArray();
if (json_array_images.isEmpty()) {
Error("Json array is empty.");
EndSearch(s_ctx, r_ctx);
return;
}
int i = 0;
for (const QJsonValue &value : json_images) {
i = 0;
for (const QJsonValue &value : json_array_images) {
if (!value.isObject()) {
Error("Invalid Json reply, value is not an object.", value);
continue;
}
QJsonObject json_obj = value.toObject();
if (!json_obj.contains("type") || !json_obj.contains("resource_url")) {
Error("Invalid Json reply, value is missing ID or resource_url.", json_obj);
if (!json_obj.contains("type") || !json_obj.contains("resource_url") || !json_obj.contains("width") || !json_obj.contains("height") ) {
Error("Invalid Json reply, images value is missing type, resource_url, width or height.", json_obj);
continue;
}
CoverSearchResult cover_result;
cover_result.description = s_ctx->title;
QString type = json_obj["type"].toString();
i++;
if (type != "primary") {
continue;
}
int width = json_obj["width"].toInt();
int height = json_obj["height"].toInt();
if (width < 300 || height < 300) continue;
const float aspect_score = 1.0 - float(std::max(width, height) - std::min(width, height)) / std::max(height, width);
if (aspect_score < 0.85) continue;
CoverSearchResult cover_result;
cover_result.artist = artist;
cover_result.album = album;
cover_result.image_url = QUrl(json_obj["resource_url"].toString());
if (cover_result.image_url.isEmpty()) continue;
s_ctx->results.append(cover_result);
@ -400,7 +455,7 @@ void DiscogsCoverProvider::EndSearch(DiscogsCoverSearchContext *s_ctx) {
}
void DiscogsCoverProvider::EndSearch(DiscogsCoverReleaseContext* r_ctx) {
void DiscogsCoverProvider::EndSearch(DiscogsCoverReleaseContext *r_ctx) {
delete requests_release_.take(r_ctx->id);
}

View File

@ -47,7 +47,6 @@ struct DiscogsCoverSearchContext {
// The search query
QString artist;
QString album;
QString title;
int r_count;
CoverSearchResults results;
@ -88,7 +87,7 @@ class DiscogsCoverProvider : public CoverProvider {
QHash<int, DiscogsCoverSearchContext*> requests_search_;
QHash<int, DiscogsCoverReleaseContext*> requests_release_;
bool StartRelease(DiscogsCoverSearchContext *s_ctx, int r_id, QString resource_url);
bool StartRelease(DiscogsCoverSearchContext *s_ctx, int r_id, QString &description, QString &resource_url);
void SendSearchRequest(DiscogsCoverSearchContext *s_ctx);
void SendReleaseRequest(DiscogsCoverSearchContext *s_ctx, DiscogsCoverReleaseContext *r_ctx);

View File

@ -48,7 +48,7 @@ const char *LastFmCoverProvider::kUrl = "https://ws.audioscrobbler.com/2.0/";
const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e";
const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8";
LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", true, app, parent), network_(new NetworkAccessManager(this)) {}
LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", 1.0, true, app, parent), network_(new NetworkAccessManager(this)) {}
bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
@ -156,8 +156,8 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, int id) {
continue;
}
QString artist = json_obj["artist"].toString();
QString name = json_obj["name"].toString();
QString album = json_obj["name"].toString();
QJsonValue json_image = json_obj["image"];
if (!json_image.isArray()) {
Error("Invalid Json reply, album image is not an array.", json_image);
@ -187,7 +187,8 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, int id) {
if (url.isEmpty()) continue;
CoverSearchResult cover_result;
cover_result.description = artist + " " + name;
cover_result.artist = artist;
cover_result.album = album;
cover_result.image_url = url;
results << cover_result;
}

View File

@ -49,7 +49,7 @@ const char *MusicbrainzCoverProvider::kReleaseSearchUrl = "https://musicbrainz.o
const char *MusicbrainzCoverProvider::kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front";
const int MusicbrainzCoverProvider::kLimit = 8;
MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", true, app, parent), network_(new NetworkAccessManager(this)) {}
MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", 1.5, true, app, parent), network_(new NetworkAccessManager(this)) {}
bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
@ -117,22 +117,60 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, int searc
}
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);
if (!json_obj.contains("id") || !json_obj.contains("artist-credit") || !json_obj.contains("title")) {
Error("Invalid Json reply, album is missing id, artist-credit or title.", json_obj);
continue;
}
QJsonValue json_artists = json_obj["artist-credit"];
if (!json_artists.isArray()) {
Error("Json artist-credit is not an array.", json_artists);
continue;
}
QJsonArray json_array_artists = json_artists.toArray();
int i = 0;
QString artist;
for (const QJsonValue &json_value_artist : json_array_artists) {
if (!json_value_artist.isObject()) {
Error("Invalid Json reply, artist is not an object.", json_value_artist);
continue;
}
QJsonObject json_obj_artist = json_value_artist.toObject();
if (!json_obj_artist.contains("artist") ) {
Error("Invalid Json reply, artist is missing.", json_obj_artist);
continue;
}
QJsonValue json_value_artist2 = json_obj_artist["artist"];
if (!json_value_artist2.isObject()) {
Error("Invalid Json reply, artist is not an object.", json_value_artist2);
continue;
}
QJsonObject json_obj_artist2 = json_value_artist2.toObject();
if (!json_obj_artist2.contains("name") ) {
Error("Invalid Json reply, artist is missing name.", json_value_artist2);
continue;
}
artist = json_obj_artist2["name"].toString();
++i;
}
if (i > 1) artist = "Various artists";
QString id = json_obj["id"].toString();
QString title = json_obj["title"].toString();
CoverSearchResult result;
QString album = json_obj["title"].toString();
CoverSearchResult cover_result;
QUrl url(QString(kAlbumCoverUrl).arg(id));
result.description = title;
result.image_url = url;
results.append(result);
cover_result.artist = artist;
cover_result.album = album;
cover_result.image_url = url;
results.append(cover_result);
}
emit SearchFinished(search_id, results);

View File

@ -54,7 +54,7 @@ const char *TidalCoverProvider::kApiTokenB64 = "UDVYYmVvNUxGdkVTZUR5Ng==";
const int TidalCoverProvider::kLimit = 10;
TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) :
CoverProvider("Tidal", true, app, parent),
CoverProvider("Tidal", 2.0, true, app, parent),
service_(app->internet_services()->Service<TidalService>()),
network_(new NetworkAccessManager(this)) {
@ -161,7 +161,7 @@ QJsonObject TidalCoverProvider::ExtractJsonObj(QByteArray &data, QString &error)
return QJsonObject();
}
if (json_doc.isNull() || json_doc.isEmpty()) {
if (json_doc.isEmpty()) {
error = Error("Received empty Json document.", data);
return QJsonObject();
}
@ -256,11 +256,15 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, int id) {
}
QString artist = json_artist["name"].toString();
album.remove(Song::kAlbumRemoveDisc);
album.remove(Song::kAlbumRemoveMisc);
cover = cover.replace("-", "/");
QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg("1280x1280"));
CoverSearchResult cover_result;
cover_result.description = artist + " " + album;
cover_result.artist = artist;
cover_result.album = album;
cover_result.image_url = cover_url;
results << cover_result;