Base initial score on album cover sizes retrieved from API

This commit is contained in:
Jonas Kvinge 2020-08-09 20:10:53 +02:00
parent 15ddf6ff20
commit 465369d79e
10 changed files with 132 additions and 94 deletions

View File

@ -61,7 +61,7 @@ struct CoverSearchRequest {
// This structure represents a single result of some album's cover search request. // This structure represents a single result of some album's cover search request.
struct CoverSearchResult { struct CoverSearchResult {
explicit CoverSearchResult() : score(0.0) {} explicit CoverSearchResult() : score_provider(0.0), score_match(0.0), score_quality(0.0), number(0) {}
// Used for grouping in the user interface. // Used for grouping in the user interface.
QString provider; QString provider;
@ -73,8 +73,23 @@ struct CoverSearchResult {
// An URL of a cover image // An URL of a cover image
QUrl image_url; QUrl image_url;
// Image size
QSize image_size;
// Score for this provider
float score_provider;
// Score for match
float score_match;
// Score for image quality
float score_quality;
// The result number
int number;
// Total score for this result // Total score for this result
float score; float score() const { return score_provider + score_match + score_quality; }
}; };
Q_DECLARE_METATYPE(CoverSearchResult) Q_DECLARE_METATYPE(CoverSearchResult)

View File

@ -135,7 +135,7 @@ void AlbumCoverFetcherSearch::ProviderSearchResults(CoverProvider *provider, con
for (int i = 0 ; i < results_copy.count() ; ++i) { for (int i = 0 ; i < results_copy.count() ; ++i) {
results_copy[i].provider = provider->name(); results_copy[i].provider = provider->name();
results_copy[i].score = provider->quality(); results_copy[i].score_provider = provider->quality();
QString request_artist = request_.artist.toLower(); QString request_artist = request_.artist.toLower();
QString request_album = request_.album.toLower(); QString request_album = request_.album.toLower();
@ -143,17 +143,17 @@ void AlbumCoverFetcherSearch::ProviderSearchResults(CoverProvider *provider, con
QString result_album = results_copy[i].album.toLower(); QString result_album = results_copy[i].album.toLower();
if (result_artist == request_artist) { if (result_artist == request_artist) {
results_copy[i].score += 0.5; results_copy[i].score_match += 0.5;
} }
if (result_album == request_album) { if (result_album == request_album) {
results_copy[i].score += 0.5; results_copy[i].score_match += 0.5;
} }
if (result_artist != request_artist && result_album != request_album) { if (result_artist != request_artist && result_album != request_album) {
results_copy[i].score -= 1.5; results_copy[i].score_match -= 1.5;
} }
if (request_album.isEmpty() && result_artist != request_artist) { if (request_album.isEmpty() && result_artist != request_artist) {
results_copy[i].score -= 1; results_copy[i].score_match -= 1;
} }
// Decrease score if the search was based on artist and song title, and the resulting album is a compilation or live album. // Decrease score if the search was based on artist and song title, and the resulting album is a compilation or live album.
@ -168,9 +168,12 @@ void AlbumCoverFetcherSearch::ProviderSearchResults(CoverProvider *provider, con
result_album.contains("live") || result_album.contains("live") ||
result_album.contains("concert") result_album.contains("concert")
)) { )) {
results_copy[i].score -= 1; results_copy[i].score_match -= 1;
} }
// Set the initial image quality score besed on the size returned by the API, this is recalculated when the image is received.
results_copy[i].score_quality += ScoreImage(results_copy[i].image_size);
} }
// Add results from the current provider to our pool // Add results from the current provider to our pool
@ -231,7 +234,7 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
++i; ++i;
CoverSearchResult result = results_.takeFirst(); CoverSearchResult result = results_.takeFirst();
qLog(Debug) << "Loading" << result.image_url << "from" << result.provider << "with current score" << result.score; qLog(Debug) << "Loading" << result.image_url << "from" << result.provider << "with current score" << result.score();
QNetworkRequest req(result.image_url); QNetworkRequest req(result.image_url);
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
@ -278,9 +281,13 @@ void AlbumCoverFetcherSearch::ProviderCoverFetchFinished(QNetworkReply *reply) {
if (QImageReader::supportedMimeTypes().contains(mimetype.toUtf8())) { if (QImageReader::supportedMimeTypes().contains(mimetype.toUtf8())) {
QImage image; QImage image;
if (image.loadFromData(reply->readAll())) { if (image.loadFromData(reply->readAll())) {
result.score += ScoreImage(image); if (result.image_size != QSize(0,0) && result.image_size != image.size()) {
candidate_images_.insert(result.score, CandidateImage(result, image)); qLog(Debug) << "API size for image" << result.image_size << "for" << reply->url() << "from" << result.provider << "did not match retrieved size" << image.size();
qLog(Debug) << reply->url() << "from" << result.provider << "scored" << result.score; }
result.image_size = image.size();
result.score_quality = ScoreImage(image.size());
candidate_images_.insert(result.score(), CandidateImage(result, image));
qLog(Debug) << reply->url() << "from" << result.provider << "scored" << result.score();
} }
else { else {
qLog(Error) << "Error decoding image data from" << reply->url(); qLog(Error) << "Error decoding image data from" << reply->url();
@ -310,18 +317,15 @@ void AlbumCoverFetcherSearch::ProviderCoverFetchFinished(QNetworkReply *reply) {
} }
float AlbumCoverFetcherSearch::ScoreImage(const QImage &image) const { float AlbumCoverFetcherSearch::ScoreImage(const QSize size) const {
// Invalid images score nothing if (size.width() == 0 || size.height() == 0) return 0.0;
if (image.isNull()) {
return 0.0;
}
// A 500x500px image scores 1.0, bigger scores higher // A 500x500px image scores 1.0, bigger scores higher
const float size_score = std::sqrt(float(image.width() * image.height())) / kTargetSize; const float size_score = std::sqrt(float(size.width() * size.height())) / kTargetSize;
// A 1:1 image scores 1.0, anything else scores less // A 1:1 image scores 1.0, anything else scores less
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()); const float aspect_score = 1.0 - float(std::max(size.width(), size.height()) - std::min(size.width(), size.height())) / std::max(size.height(), size.width());
return size_score + aspect_score; return size_score + aspect_score;
@ -337,7 +341,7 @@ void AlbumCoverFetcherSearch::SendBestImage() {
cover_url = best_image.first.image_url; cover_url = best_image.first.image_url;
image = best_image.second; image = best_image.second;
qLog(Info) << "Using" << best_image.first.image_url << "from" << best_image.first.provider << "with score" << best_image.first.score; 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_by_provider_[best_image.first.provider]++;
statistics_.chosen_images_++; statistics_.chosen_images_++;
@ -375,5 +379,9 @@ bool AlbumCoverFetcherSearch::ProviderCompareOrder(CoverProvider *a, CoverProvid
} }
bool AlbumCoverFetcherSearch::CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b) { bool AlbumCoverFetcherSearch::CoverSearchResultCompareScore(const CoverSearchResult &a, const CoverSearchResult &b) {
return a.score > b.score; return a.score() > b.score();
}
bool AlbumCoverFetcherSearch::CoverSearchResultCompareNumber(const CoverSearchResult &a, const CoverSearchResult &b) {
return a.number < b.number;
} }

View File

@ -59,6 +59,8 @@ class AlbumCoverFetcherSearch : public QObject {
CoverSearchStatistics statistics() const { return statistics_; } CoverSearchStatistics statistics() const { return statistics_; }
static bool CoverSearchResultCompareNumber(const CoverSearchResult &a, const CoverSearchResult &b);
signals: signals:
// It's the end of search (when there was no fetch-me-a-cover request). // It's the end of search (when there was no fetch-me-a-cover request).
void SearchFinished(const quint64, const CoverSearchResults &results); void SearchFinished(const quint64, const CoverSearchResults &results);
@ -77,7 +79,7 @@ class AlbumCoverFetcherSearch : public QObject {
void AllProvidersFinished(); void AllProvidersFinished();
void FetchMoreImages(); void FetchMoreImages();
float ScoreImage(const QImage &image) const; float ScoreImage(const QSize size) const;
void SendBestImage(); void SendBestImage();
static bool ProviderCompareOrder(CoverProvider *a, CoverProvider *b); static bool ProviderCompareOrder(CoverProvider *a, CoverProvider *b);

View File

@ -26,6 +26,7 @@
#include <QPair> #include <QPair>
#include <QSet> #include <QSet>
#include <QList> #include <QList>
#include <QMap>
#include <QVariant> #include <QVariant>
#include <QByteArray> #include <QByteArray>
#include <QString> #include <QString>
@ -45,6 +46,7 @@
#include "core/logging.h" #include "core/logging.h"
#include "core/song.h" #include "core/song.h"
#include "albumcoverfetcher.h" #include "albumcoverfetcher.h"
#include "albumcoverfetchersearch.h"
#include "jsoncoverprovider.h" #include "jsoncoverprovider.h"
#include "deezercoverprovider.h" #include "deezercoverprovider.h"
@ -197,26 +199,26 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
disconnect(reply, nullptr, this, nullptr); disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
CoverSearchResults results;
QByteArray data = GetReplyData(reply); QByteArray data = GetReplyData(reply);
if (data.isEmpty()) { if (data.isEmpty()) {
emit SearchFinished(id, results); emit SearchFinished(id, CoverSearchResults());
return; return;
} }
QJsonValue value_data = ExtractData(data); QJsonValue value_data = ExtractData(data);
if (!value_data.isArray()) { if (!value_data.isArray()) {
emit SearchFinished(id, results); emit SearchFinished(id, CoverSearchResults());
return; return;
} }
QJsonArray array_data = value_data.toArray(); QJsonArray array_data = value_data.toArray();
if (array_data.isEmpty()) { if (array_data.isEmpty()) {
emit SearchFinished(id, results); emit SearchFinished(id, CoverSearchResults());
return; return;
} }
QMap<QUrl, CoverSearchResult> results;
int i = 0;
for (const QJsonValue &json_value : array_data) { for (const QJsonValue &json_value : array_data) {
if (!json_value.isObject()) { if (!json_value.isObject()) {
@ -270,36 +272,46 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
} }
QString album = obj_album["title"].toString(); QString album = obj_album["title"].toString();
QString cover; album = album.remove(Song::kAlbumRemoveDisc);
if (obj_album.contains("cover_xl")) { album = album.remove(Song::kAlbumRemoveMisc);
cover = obj_album["cover_xl"].toString();
}
else if (obj_album.contains("cover_big")) {
cover = obj_album["cover_big"].toString();
}
else if (obj_album.contains("cover_medium")) {
cover = obj_album["cover_medium"].toString();
}
else if (obj_album.contains("cover_small")) {
cover = obj_album["cover_small"].toString();
}
else {
Error("Invalid Json reply, data array value album object is missing cover.", obj_album);
continue;
}
QUrl url(cover);
album.remove(Song::kAlbumRemoveDisc);
album.remove(Song::kAlbumRemoveMisc);
CoverSearchResult cover_result; CoverSearchResult cover_result;
cover_result.artist = artist; cover_result.artist = artist;
cover_result.album = album; cover_result.album = album;
cover_result.image_url = url;
results << cover_result; bool have_cover = false;
QList<QPair<QString, QSize>> cover_sizes = QList<QPair<QString, QSize>>() << qMakePair(QString("cover_xl"), QSize(1000, 1000))
<< qMakePair(QString("cover_big"), QSize(500, 500));
for (const QPair<QString, QSize> &cover_size : cover_sizes) {
if (!obj_album.contains(cover_size.first)) continue;
QString cover = obj_album[cover_size.first].toString();
if (!have_cover) {
have_cover = true;
++i;
}
QUrl url(cover);
if (!results.contains(url)) {
cover_result.image_url = url;
cover_result.image_size = cover_size.second;
cover_result.number = i;
results.insert(url, cover_result);
}
}
if (!have_cover) {
Error("Invalid Json reply, data array value album object is missing cover.", obj_album);
}
} }
emit SearchFinished(id, results);
if (results.isEmpty()) {
emit SearchFinished(id, CoverSearchResults());
}
else {
CoverSearchResults cover_results = results.values();
std::stable_sort(cover_results.begin(), cover_results.end(), AlbumCoverFetcherSearch::CoverSearchResultCompareNumber);
emit SearchFinished(id, cover_results);
}
} }

View File

@ -275,6 +275,7 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
cover_result.artist = artist; cover_result.artist = artist;
cover_result.album = album; cover_result.album = album;
cover_result.image_url = url; cover_result.image_url = url;
cover_result.image_size = QSize(size, size);
results << cover_result; results << cover_result;
} }
emit SearchFinished(id, results); emit SearchFinished(id, results);

View File

@ -50,10 +50,10 @@ class LastFmCoverProvider : public JsonCoverProvider {
private: private:
enum LastFmImageSize { enum LastFmImageSize {
Unknown, Unknown,
Small, Small = 34,
Medium, Medium = 64,
Large, Large = 174,
ExtraLarge ExtraLarge = 300
}; };
QByteArray GetReplyData(QNetworkReply *reply); QByteArray GetReplyData(QNetworkReply *reply);

View File

@ -196,36 +196,27 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
return; return;
} }
QString cover;
if (obj_album.contains("coverart800x800")) {
cover = obj_album["coverart800x800"].toString();
}
else if (obj_album.contains("coverart500x500")) {
cover = obj_album["coverart500x500"].toString();
}
else if (obj_album.contains("coverart350x350")) {
cover = obj_album["coverart350x350"].toString();
}
if (cover.isEmpty()) {
emit SearchFinished(id, results);
return;
}
QUrl cover_url(cover);
if (!cover_url.isValid()) {
Error("Received cover url is not valid.", cover);
emit SearchFinished(id, results);
return;
}
CoverSearchResult result; CoverSearchResult result;
result.artist = obj_album["artistName"].toString(); result.artist = obj_album["artistName"].toString();
result.album = obj_album["name"].toString(); result.album = obj_album["name"].toString();
result.image_url = cover_url;
if (artist.toLower() == result.artist.toLower() || album.toLower() == result.album.toLower()) { if (artist.toLower() != result.artist.toLower() && album.toLower() != result.album.toLower()) {
results.append(result); emit SearchFinished(id, results);
return;
}
QList<QPair<QString, QSize>> cover_sizes = QList<QPair<QString, QSize>>() << qMakePair(QString("coverart800x800"), QSize(800, 800))
<< qMakePair(QString("coverart500x500"), QSize(500, 500))
<< qMakePair(QString("coverart350x350"), QSize(300, 300));
for (const QPair<QString, QSize> &cover_size : cover_sizes) {
if (!obj_album.contains(cover_size.first)) continue;
QUrl cover_url(obj_album[cover_size.first].toString());
if (cover_url.isValid()) {
result.image_url = cover_url;
result.image_size = cover_size.second;
results << result;
}
} }
emit SearchFinished(id, results); emit SearchFinished(id, results);

View File

@ -493,13 +493,14 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
} }
QUrl cover_url(obj_image["large"].toString()); QUrl cover_url(obj_image["large"].toString());
album.remove(Song::kAlbumRemoveDisc); album = album.remove(Song::kAlbumRemoveDisc);
album.remove(Song::kAlbumRemoveMisc); album = album.remove(Song::kAlbumRemoveMisc);
CoverSearchResult cover_result; CoverSearchResult cover_result;
cover_result.artist = artist; cover_result.artist = artist;
cover_result.album = album; cover_result.album = album;
cover_result.image_url = cover_url; cover_result.image_url = cover_url;
cover_result.image_size = QSize(600, 600);
results << cover_result; results << cover_result;
} }

View File

@ -526,6 +526,7 @@ void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id,
CoverSearchResult result; CoverSearchResult result;
result.album = album; result.album = album;
result.image_url = url; result.image_url = url;
result.image_size = QSize(width, height);
if (!artists.isEmpty()) result.artist = artists.first(); if (!artists.isEmpty()) result.artist = artists.first();
results << result; results << result;
} }

View File

@ -177,37 +177,37 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
disconnect(reply, nullptr, this, nullptr); disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
CoverSearchResults results;
QByteArray data = GetReplyData(reply); QByteArray data = GetReplyData(reply);
if (data.isEmpty()) { if (data.isEmpty()) {
emit SearchFinished(id, results); emit SearchFinished(id, CoverSearchResults());
return; return;
} }
QJsonObject json_obj = ExtractJsonObj(data); QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) { if (json_obj.isEmpty()) {
emit SearchFinished(id, results); emit SearchFinished(id, CoverSearchResults());
return; return;
} }
if (!json_obj.contains("items")) { if (!json_obj.contains("items")) {
Error("Json object is missing items.", json_obj); Error("Json object is missing items.", json_obj);
emit SearchFinished(id, results); emit SearchFinished(id, CoverSearchResults());
return; return;
} }
QJsonValue value_items = json_obj["items"]; QJsonValue value_items = json_obj["items"];
if (!value_items.isArray()) { if (!value_items.isArray()) {
emit SearchFinished(id, results); emit SearchFinished(id, CoverSearchResults());
return; return;
} }
QJsonArray array_items = value_items.toArray(); QJsonArray array_items = value_items.toArray();
if (array_items.isEmpty()) { if (array_items.isEmpty()) {
emit SearchFinished(id, results); emit SearchFinished(id, CoverSearchResults());
return; return;
} }
CoverSearchResults results;
int i = 0;
for (const QJsonValue &value_item : array_items) { for (const QJsonValue &value_item : array_items) {
if (!value_item.isObject()) { if (!value_item.isObject()) {
@ -256,15 +256,22 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
album = album.remove(Song::kAlbumRemoveDisc); album = album.remove(Song::kAlbumRemoveDisc);
album = album.remove(Song::kAlbumRemoveMisc); album = album.remove(Song::kAlbumRemoveMisc);
cover = cover.replace("-", "/"); cover = cover.replace("-", "/");
QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg("1280x1280"));
CoverSearchResult cover_result; CoverSearchResult cover_result;
cover_result.artist = artist; cover_result.artist = artist;
cover_result.album = album; cover_result.album = album;
cover_result.image_url = cover_url; cover_result.number = ++i;
results << cover_result;
QList<QPair<QString, QSize>> cover_sizes = QList<QPair<QString, QSize>>() << qMakePair(QString("1280x1280"), QSize(1280, 1280))
<< qMakePair(QString("750x750"), QSize(750, 750))
<< qMakePair(QString("640x640"), QSize(640, 640));
for (const QPair<QString, QSize> &cover_size : cover_sizes) {
QUrl cover_url(QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg(cover_size.first));
cover_result.image_url = cover_url;
cover_result.image_size = cover_size.second;
results << cover_result;
}
} }
emit SearchFinished(id, results); emit SearchFinished(id, results);