Improve album cover searching and cover manager, use HttpStatusCodeAttribute and QSslError for services

- Improve album cover manager
- Change art_automatic and art_manual to QUrl
- Refresh collection album covers when new album covers are fetched
- Fix automatic album cover searching for local files outside of the collection
- Make all Json services check HttpStatusCodeAttribute
- Show detailed SSL errors for Subsonic, Tidal and Qobuz
This commit is contained in:
Jonas Kvinge
2019-07-07 21:14:24 +02:00
parent c92a7967ea
commit 65780e1672
101 changed files with 1531 additions and 1239 deletions

View File

@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -53,12 +54,14 @@
#include "collection/collectionbackend.h"
#include "settings/collectionsettingspage.h"
#include "organise/organiseformat.h"
#include "internet/internetservices.h"
#include "internet/internetservice.h"
#include "albumcoverchoicecontroller.h"
#include "albumcoverfetcher.h"
#include "albumcoverloader.h"
#include "albumcoversearcher.h"
#include "coverfromurldialog.h"
#include "currentartloader.h"
#include "currentalbumcoverloader.h"
const char *AlbumCoverChoiceController::kLoadImageFileFilter = QT_TR_NOOP("Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
const char *AlbumCoverChoiceController::kSaveImageFileFilter = QT_TR_NOOP("Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
@@ -100,6 +103,18 @@ AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent) :
AlbumCoverChoiceController::~AlbumCoverChoiceController() {}
void AlbumCoverChoiceController::Init(Application *app) {
app_ = app;
cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), this);
cover_searcher_ = new AlbumCoverSearcher(QIcon(":/pictures/cdcase.png"), app, this);
cover_searcher_->Init(cover_fetcher_);
connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(const quint64, const QUrl&, const QImage&, CoverSearchStatistics)), this, SLOT(AlbumCoverFetched(const quint64, const QUrl&, const QImage&, CoverSearchStatistics)));
}
void AlbumCoverChoiceController::ReloadSettings() {
QSettings s;
@@ -114,37 +129,28 @@ void AlbumCoverChoiceController::ReloadSettings() {
}
void AlbumCoverChoiceController::SetApplication(Application *app) {
app_ = app;
cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), this);
cover_searcher_ = new AlbumCoverSearcher(QIcon(":/pictures/cdcase.png"), app, this);
cover_searcher_->Init(cover_fetcher_);
connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)), this, SLOT(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)));
}
QList<QAction*> AlbumCoverChoiceController::GetAllActions() {
return QList<QAction*>() << cover_from_file_ << cover_to_file_ << separator_ << cover_from_url_ << search_for_cover_ << unset_cover_ << show_cover_;
}
QString AlbumCoverChoiceController::LoadCoverFromFile(Song *song) {
QUrl AlbumCoverChoiceController::LoadCoverFromFile(Song *song) {
QString cover = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
QString cover_file = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
if (cover.isNull()) return QString();
if (cover_file.isNull()) return QUrl();
// Can we load the image?
QImage image(cover);
QImage image(cover_file);
if (!image.isNull()) {
SaveCover(song, cover);
return cover;
if (image.isNull()) {
return QUrl();
}
else {
return QString();
QUrl cover_url;
cover_url.setScheme("file");
cover_url.setPath(cover_file);
SaveCoverToSong(song, cover_url);
return cover_url;
}
}
@@ -178,38 +184,43 @@ QString AlbumCoverChoiceController::GetInitialPathForFileDialog(const Song &song
// Art automatic is first to show user which cover the album may be using now;
// The song is using it if there's no manual path but we cannot use manual path here because it can contain cached paths
if (!song.art_automatic().isEmpty() && !song.has_embedded_cover()) {
return song.art_automatic();
if (!song.art_automatic().isEmpty() && !song.art_automatic().path().isEmpty() && !song.has_embedded_cover()) {
if (song.art_automatic().scheme().isEmpty() && QFile::exists(QFileInfo(song.art_automatic().path()).path())) {
return song.art_automatic().path();
}
else if (song.art_automatic().scheme() == "file" && QFile::exists(QFileInfo(song.art_automatic().toLocalFile()).path())) {
return song.art_automatic().toLocalFile();
}
// If no automatic art, start in the song's folder
}
else if (!song.url().isEmpty() && song.url().toLocalFile().contains('/')) {
return song.url().toLocalFile().section('/', 0, -2) + filename;
// Fallback - start in home
}
else {
return QDir::home().absolutePath() + filename;
}
return QDir::home().absolutePath() + filename;
}
QString AlbumCoverChoiceController::LoadCoverFromURL(Song *song) {
QUrl AlbumCoverChoiceController::LoadCoverFromURL(Song *song) {
if (!cover_from_url_dialog_) { cover_from_url_dialog_ = new CoverFromURLDialog(this); }
QImage image = cover_from_url_dialog_->Exec();
if (!image.isNull()) {
QString cover = SaveCoverToFileAutomatic(song, image);
if (cover.isEmpty()) return QString();
SaveCover(song, cover);
return cover;
if (image.isNull()) {
return QUrl();
}
else {
QUrl cover_url = SaveCoverToFileAutomatic(song, QUrl(), image, true);
if (cover_url.isEmpty()) return QUrl();
SaveCoverToSong(song, cover_url);
return cover_url;
}
else { return QString(); }
}
QString AlbumCoverChoiceController::SearchForCover(Song *song) {
QUrl AlbumCoverChoiceController::SearchForCover(Song *song) {
QString album = song->effective_album();
album.remove(Song::kAlbumRemoveDisc);
@@ -218,23 +229,26 @@ QString AlbumCoverChoiceController::SearchForCover(Song *song) {
// Get something sensible to stick in the search box
QImage image = cover_searcher_->Exec(song->effective_albumartist(), album);
if (!image.isNull()) {
QString cover = SaveCoverToFileAutomatic(song, image);
if (cover.isEmpty()) return QString();
SaveCover(song, cover);
return cover;
if (image.isNull()) {
return QUrl();
}
else {
QUrl cover_url = SaveCoverToFileAutomatic(song, QUrl(), image, true);
if (cover_url.isEmpty()) return QUrl();
SaveCoverToSong(song, cover_url);
return cover_url;
}
else { return QString(); }
}
QString AlbumCoverChoiceController::UnsetCover(Song *song) {
QUrl AlbumCoverChoiceController::UnsetCover(Song *song) {
QString cover = Song::kManuallyUnsetCover;
SaveCover(song, cover);
QUrl cover_url;
cover_url.setScheme("file");
cover_url.setPath(Song::kManuallyUnsetCover);
SaveCoverToSong(song, cover_url);
return cover;
return cover_url;
}
@@ -307,7 +321,7 @@ void AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) {
}
void AlbumCoverChoiceController::AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics) {
void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics) {
Song song;
if (cover_fetching_tasks_.contains(id)) {
@@ -315,93 +329,76 @@ void AlbumCoverChoiceController::AlbumCoverFetched(quint64 id, const QImage &ima
}
if (!image.isNull()) {
QString cover = SaveCoverToFileAutomatic(&song, image);
if (cover.isEmpty()) return;
SaveCover(&song, cover);
QUrl new_cover_url = SaveCoverToFileAutomatic(&song, cover_url, image, false);
if (!new_cover_url.isEmpty()) SaveCoverToSong(&song, new_cover_url);
}
emit AutomaticCoverSearchDone();
}
void AlbumCoverChoiceController::SaveCover(Song *song, const QString &cover) {
void AlbumCoverChoiceController::SaveCoverToSong(Song *song, const QUrl &cover_url) {
if (song->is_valid() && song->id() != -1) {
song->set_art_manual(cover);
app_->collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover);
if (!song->is_valid()) return;
if (song->url() == app_->current_art_loader()->last_song().url()) {
app_->current_art_loader()->LoadArt(*song);
song->set_art_manual(cover_url);
if (song->id() != -1) { // Update the backends.
switch (song->source()) {
case Song::Source_Collection:
app_->collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url);
break;
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_Unknown:
break;
case Song::Source_Tidal:
case Song::Source_Qobuz:
case Song::Source_Subsonic:
InternetService *service = app_->internet_services()->ServiceBySource(song->source());
if (!service) break;
if (service->artists_collection_backend())
service->artists_collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url);
if (service->albums_collection_backend())
service->albums_collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url);
if (service->songs_collection_backend())
service->songs_collection_backend()->UpdateManualAlbumArtAsync(song->artist(), song->albumartist(), song->album(), cover_url);
break;
}
}
if (song->url() == app_->current_albumcover_loader()->last_song().url()) {
app_->current_albumcover_loader()->LoadAlbumCover(*song);
}
}
QString AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, const QImage &image) {
QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, const QUrl &cover_url, const QImage &image, const bool overwrite) {
QString albumartist(song->effective_albumartist());
QString artist(song->artist());
QString album(song->effective_album());
album.remove(Song::kAlbumRemoveDisc);
return SaveCoverToFileAutomatic(albumartist, artist, album, song->url().adjusted(QUrl::RemoveFilename).path(), image);
return SaveCoverToFileAutomatic(song->source(), song->effective_albumartist(), song->effective_album(), song->album_id(), song->url().adjusted(QUrl::RemoveFilename).path(), cover_url, image, overwrite);
}
QString AlbumCoverChoiceController::SaveCoverToFileAutomatic(const QString &albumartist, const QString &artist, const QString &album, const QString &album_dir, const QImage &image) {
QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QImage &image, const bool overwrite) {
QString album_new(album);
album_new.remove(Song::kAlbumRemoveDisc);
QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, cover_url);
if (filepath.isEmpty()) return QUrl();
QString path;
QString filename;
if (cover_album_dir_) {
path = album_dir;
}
else {
path = AlbumCoverLoader::ImageCacheDir();
QUrl new_cover_url;
new_cover_url.setScheme("file");
new_cover_url.setPath(filepath);
// Don't overwrite when saving in album dir if the filename is set to pattern unless the "overwrite" is set.
if (source == Song::Source_Collection && QFile::exists(filepath) && !cover_overwrite_ && !overwrite && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern) {
return new_cover_url;
}
if (path.right(1) == QDir::separator()) {
path.chop(1);
}
if (!image.save(filepath, "JPG") && !QFile::exists(filepath)) return QUrl();
QDir dir;
if (!dir.mkpath(path)) {
qLog(Error) << "Unable to create directory" << path;
return QString();
}
if (cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) {
filename = CreateCoverFilename(albumartist, artist, album_new) + ".jpg";
filename.remove(OrganiseFormat::kValidFatCharacters);
if (cover_lowercase_) filename = filename.toLower();
if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-");
}
else {
filename = Utilities::Sha1CoverHash(albumartist, album_new).toHex() + ".jpg";
}
QString filepath(path + "/" + filename);
// Don't overwrite when saving in album dir if the filename is set to pattern unless the "cover_overwrite" is set.
if (QFile::exists(filepath) && !cover_overwrite_ && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern) {
return filepath;
}
image.save(filepath, "JPG");
return filepath;
}
QString AlbumCoverChoiceController::CreateCoverFilename(const QString &albumartist, const QString &artist, const QString &album) {
QString filename(cover_pattern_);
filename.replace("%albumartist", albumartist);
filename.replace("%artist", artist);
filename.replace("%album", album);
return filename;
return new_cover_url;
}
@@ -426,7 +423,7 @@ bool AlbumCoverChoiceController::CanAcceptDrag(const QDragEnterEvent *e) {
}
QString AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
for (const QUrl &url : e->mimeData()->urls()) {
@@ -434,21 +431,21 @@ QString AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
const QString suffix = QFileInfo(filename).suffix().toLower();
if (IsKnownImageExtension(suffix)) {
SaveCover(song, filename);
return filename;
SaveCoverToSong(song, url);
return url;
}
}
if (e->mimeData()->hasImage()) {
QImage image = qvariant_cast<QImage>(e->mimeData()->imageData());
if (!image.isNull()) {
QString cover_path = SaveCoverToFileAutomatic(song, image);
if (cover_path.isEmpty()) return QString();
SaveCover(song, cover_path);
return cover_path;
QUrl cover_url = SaveCoverToFileAutomatic(song, QUrl(), image, true);
if (cover_url.isEmpty()) return QUrl();
SaveCoverToSong(song, cover_url);
return cover_url;
}
}
return QString();
return QUrl();
}

View File

@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -58,7 +59,7 @@ class AlbumCoverChoiceController : public QWidget {
AlbumCoverChoiceController(QWidget *parent = nullptr);
~AlbumCoverChoiceController();
void SetApplication(Application *app);
void Init(Application *app);
void ReloadSettings();
// Getters for all QActions implemented by this controller.
@@ -85,7 +86,7 @@ class AlbumCoverChoiceController : public QWidget {
// Lets the user choose a cover from disk. If no cover will be chosen or the chosen cover will not be a proper image, this returns an empty string.
// Otherwise, the path to the chosen cover will be returned.
QString LoadCoverFromFile(Song *song);
QUrl LoadCoverFromFile(Song *song);
// Shows a dialog that allows user to save the given image on disk.
// The image is supposed to be the cover of the given song's album.
@@ -93,14 +94,14 @@ class AlbumCoverChoiceController : public QWidget {
// Downloads the cover from an URL given by user.
// This returns the downloaded image or null image if something went wrong for example when user cancelled the dialog.
QString LoadCoverFromURL(Song *song);
QUrl LoadCoverFromURL(Song *song);
// Lets the user choose a cover among all that have been found on last.fm.
// Returns the chosen cover or null cover if user didn't choose anything.
QString SearchForCover(Song *song);
QUrl SearchForCover(Song *song);
// Returns a path which indicates that the cover has been unset manually.
QString UnsetCover(Song *song);
QUrl UnsetCover(Song *song);
// Shows the cover of given song in it's original size.
void ShowCover(const Song &song);
@@ -111,15 +112,14 @@ class AlbumCoverChoiceController : public QWidget {
void SearchCoverAutomatically(const Song &song);
// Saves the chosen cover as manual cover path of this song in collection.
void SaveCover(Song *song, const QString &cover);
void SaveCoverToSong(Song *song, const QUrl &cover_url);
// Saves the cover that the user picked through a drag and drop operation.
QString SaveCover(Song *song, const QDropEvent *e);
QUrl SaveCover(Song *song, const QDropEvent *e);
// Saves the given image in album directory or cache as a cover for 'album artist' - 'album'. The method returns path of the image.
QString SaveCoverToFileAutomatic(const QString &albumartist, const QString &artist, const QString &album, const QString &album_dir, const QImage &image);
QString SaveCoverToFileAutomatic(const Song *song, const QImage &image);
QString CreateCoverFilename(const QString &albumartist, const QString &artist, const QString &album);
QUrl SaveCoverToFileAutomatic(const Song *song, const QUrl &cover_url, const QImage &image, const bool overwrite = false);
QUrl SaveCoverToFileAutomatic(const Song::Source source, const QString &artist, const QString &album, const QString &album_id, const QString &album_dir, const QUrl &cover_url, const QImage &image, const bool overwrite = false);
static bool CanAcceptDrag(const QDragEnterEvent *e);
@@ -127,7 +127,7 @@ signals:
void AutomaticCoverSearchDone();
private slots:
void AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics);
void AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics);
private:
QString GetInitialPathForFileDialog(const Song &song, const QString &filename);

View File

@@ -114,15 +114,15 @@ void AlbumCoverFetcher::StartRequests() {
AlbumCoverFetcherSearch *search = new AlbumCoverFetcherSearch(request, network_, this);
active_requests_.insert(request.id, search);
connect(search, SIGNAL(SearchFinished(quint64, CoverSearchResults)), SLOT(SingleSearchFinished(quint64, CoverSearchResults)));
connect(search, SIGNAL(AlbumCoverFetched(quint64, const QImage&)), SLOT(SingleCoverFetched(quint64, const QImage&)));
connect(search, SIGNAL(SearchFinished(const quint64, const CoverSearchResults)), SLOT(SingleSearchFinished(const quint64, const CoverSearchResults)));
connect(search, SIGNAL(AlbumCoverFetched(const quint64, const QUrl&, const QImage&)), SLOT(SingleCoverFetched(const quint64, const QUrl&, const QImage&)));
search->Start(cover_providers_);
}
}
void AlbumCoverFetcher::SingleSearchFinished(quint64 request_id, CoverSearchResults results) {
void AlbumCoverFetcher::SingleSearchFinished(const quint64 request_id, const CoverSearchResults results) {
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
if (!search) return;
@@ -132,13 +132,13 @@ void AlbumCoverFetcher::SingleSearchFinished(quint64 request_id, CoverSearchResu
}
void AlbumCoverFetcher::SingleCoverFetched(quint64 request_id, const QImage &image) {
void AlbumCoverFetcher::SingleCoverFetched(const quint64 request_id, const QUrl &cover_url, const QImage &image) {
AlbumCoverFetcherSearch *search = active_requests_.take(request_id);
if (!search) return;
search->deleteLater();
emit AlbumCoverFetched(request_id, image, search->statistics());
emit AlbumCoverFetched(request_id, cover_url, image, search->statistics());
}

View File

@@ -36,7 +36,6 @@
#include <QString>
#include <QUrl>
#include <QImage>
#include <QVector>
#include <QNetworkAccessManager>
class CoverProviders;
@@ -45,6 +44,8 @@ struct CoverSearchStatistics;
// This class represents a single search-for-cover request. It identifies and describes the request.
struct CoverSearchRequest {
CoverSearchRequest() : id(-1), search(false), fetchall(false) {}
// An unique (for one AlbumCoverFetcher) request identifier
quint64 id;
@@ -61,6 +62,8 @@ struct CoverSearchRequest {
// This structure represents a single result of some album's cover search request.
struct CoverSearchResult {
CoverSearchResult() : score(0.0) {}
// Used for grouping in the user interface.
QString provider;
@@ -92,17 +95,17 @@ class AlbumCoverFetcher : public QObject {
static const int kMaxConcurrentRequests;
quint64 SearchForCovers(const QString &artist, const QString &album);
quint64 FetchAlbumCover(const QString &artist, const QString &album, bool fetchall);
quint64 FetchAlbumCover(const QString &artist, const QString &album, const bool fetchall);
void Clear();
signals:
void AlbumCoverFetched(quint64, const QImage &cover, const CoverSearchStatistics &statistics);
void SearchFinished(quint64, const CoverSearchResults &results, const CoverSearchStatistics &statistics);
void AlbumCoverFetched(const quint64 request_id, const QUrl &cover_url, const QImage &cover, const CoverSearchStatistics &statistics);
void SearchFinished(const quint64 request_id, const CoverSearchResults &results, const CoverSearchStatistics &statistics);
private slots:
void SingleSearchFinished(quint64, CoverSearchResults results);
void SingleCoverFetched(quint64, const QImage &cover);
void SingleSearchFinished(const quint64, const CoverSearchResults results);
void SingleCoverFetched(const quint64, const QUrl &cover_url, const QImage &cover);
void StartRequests();
private:

View File

@@ -68,7 +68,7 @@ AlbumCoverFetcherSearch::AlbumCoverFetcherSearch(
void AlbumCoverFetcherSearch::TerminateSearch() {
for (int id : pending_requests_.keys()) {
for (quint64 id : pending_requests_.keys()) {
pending_requests_.take(id)->CancelSearch(id);
}
@@ -86,7 +86,7 @@ void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
continue;
}
connect(provider, SIGNAL(SearchFinished(int, QList<CoverSearchResult>)), SLOT(ProviderSearchFinished(int, QList<CoverSearchResult>)));
connect(provider, SIGNAL(SearchFinished(int, CoverSearchResults)), SLOT(ProviderSearchFinished(int, CoverSearchResults)));
const int id = cover_providers->NextId();
const bool success = provider->StartSearch(request_.artist, request_.album, id);
@@ -107,7 +107,7 @@ static bool CompareProviders(const CoverSearchResult &a, const CoverSearchResult
return a.provider < b.provider;
}
void AlbumCoverFetcherSearch::ProviderSearchFinished(int id, const QList<CoverSearchResult> &results) {
void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverSearchResults &results) {
if (!pending_requests_.contains(id)) return;
CoverProvider *provider = pending_requests_.take(id);
@@ -155,7 +155,7 @@ void AlbumCoverFetcherSearch::AllProvidersFinished() {
// No results?
if (results_.isEmpty()) {
statistics_.missing_images_++;
emit AlbumCoverFetched(request_.id, QImage());
emit AlbumCoverFetched(request_.id, QUrl(), QImage());
return;
}
@@ -265,10 +265,12 @@ float AlbumCoverFetcherSearch::ScoreImage(const QImage &image) const {
void AlbumCoverFetcherSearch::SendBestImage() {
QUrl cover_url;
QImage image;
if (!candidate_images_.isEmpty()) {
const CandidateImage best_image = candidate_images_.values().back();
cover_url = best_image.first.image_url;
image = best_image.second;
qLog(Info) << "Using " << best_image.first.image_url << "from" << best_image.first.provider << "with score" << best_image.first.score;
@@ -282,7 +284,7 @@ void AlbumCoverFetcherSearch::SendBestImage() {
statistics_.missing_images_++;
}
emit AlbumCoverFetched(request_.id, image);
emit AlbumCoverFetched(request_.id, cover_url, image);
}

View File

@@ -60,13 +60,13 @@ class AlbumCoverFetcherSearch : public QObject {
signals:
// It's the end of search (when there was no fetch-me-a-cover request).
void SearchFinished(quint64, const CoverSearchResults &results);
void SearchFinished(const quint64, const CoverSearchResults &results);
// It's the end of search and we've fetched a cover.
void AlbumCoverFetched(quint64, const QImage &cover);
void AlbumCoverFetched(const quint64, const QUrl &cover_url, const QImage &cover);
private slots:
void ProviderSearchFinished(int id, const QList<CoverSearchResult> &results);
void ProviderSearchFinished(const int id, const CoverSearchResults &results);
void ProviderCoverFetchFinished(RedirectFollower *reply);
void TerminateSearch();
@@ -106,4 +106,3 @@ class AlbumCoverFetcherSearch : public QObject {
};
#endif // ALBUMCOVERFETCHERSEARCH_H

View File

@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,6 +23,7 @@
#include <QtGlobal>
#include <QObject>
#include <QDir>
#include <QQueue>
#include <QMutex>
#include <QStandardPaths>
@@ -37,11 +39,15 @@
#include <QPainter>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSettings>
#include "core/closure.h"
#include "core/network.h"
#include "core/song.h"
#include "core/tagreaderclient.h"
#include "core/utilities.h"
#include "settings/collectionsettingspage.h"
#include "organise/organiseformat.h"
#include "albumcoverloader.h"
#include "albumcoverloaderoptions.h"
@@ -49,13 +55,147 @@ AlbumCoverLoader::AlbumCoverLoader(QObject *parent)
: QObject(parent),
stop_requested_(false),
next_id_(1),
network_(new NetworkAccessManager(this)){}
network_(new NetworkAccessManager(this)),
cover_album_dir_(false),
cover_filename_(CollectionSettingsPage::SaveCover_Hash),
cover_overwrite_(false),
cover_lowercase_(true),
cover_replace_spaces_(true)
{
ReloadSettings();
QString AlbumCoverLoader::ImageCacheDir() {
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/albumcovers";
}
void AlbumCoverLoader::CancelTask(quint64 id) {
void AlbumCoverLoader::ReloadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
cover_album_dir_ = s.value("cover_album_dir", false).toBool();
cover_filename_ = CollectionSettingsPage::SaveCover(s.value("cover_filename", CollectionSettingsPage::SaveCover_Hash).toInt());
cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString();
cover_overwrite_ = s.value("cover_overwrite", false).toBool();
cover_lowercase_ = s.value("cover_lowercase", false).toBool();
cover_replace_spaces_ = s.value("cover_replace_spaces", false).toBool();
s.endGroup();
}
QString AlbumCoverLoader::ImageCacheDir(const Song::Source source) {
switch (source) {
case Song::Source_LocalFile:
case Song::Source_Collection:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_Unknown:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/albumcovers";
case Song::Source_Tidal:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/tidalalbumcovers";
case Song::Source_Qobuz:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/qobuzalbumcovers";
case Song::Source_Subsonic:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/subsonicalbumcovers";
}
return QString();
}
QString AlbumCoverLoader::CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url) {
album.remove(Song::kAlbumRemoveDisc);
QString path;
if (source == Song::Source_Collection && cover_album_dir_ && !album_dir.isEmpty()) {
path = album_dir;
}
else {
path = AlbumCoverLoader::ImageCacheDir(source);
}
if (path.right(1) == QDir::separator()) {
path.chop(1);
}
QDir dir;
if (!dir.mkpath(path)) {
qLog(Error) << "Unable to create directory" << path;
return QString();
}
QString filename;
if (source == Song::Source_Collection && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) {
filename = CreateCoverFilename(artist, album) + ".jpg";
filename.remove(OrganiseFormat::kValidFatCharacters);
if (cover_lowercase_) filename = filename.toLower();
if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-");
}
else {
switch (source) {
case Song::Source_Collection:
case Song::Source_LocalFile:
case Song::Source_CDDA:
case Song::Source_Device:
case Song::Source_Stream:
case Song::Source_Unknown:
filename = Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg";
break;
case Song::Source_Tidal:
filename = album_id + "-" + cover_url.fileName();
break;
case Song::Source_Qobuz:
case Song::Source_Subsonic:
filename = AlbumCoverFileName(artist, album);
break;
}
}
if (filename.isEmpty()) return QString();
QString filepath(path + "/" + filename);
return filepath;
}
QString AlbumCoverLoader::AlbumCoverFileName(QString artist, QString album) {
artist.remove('/');
album.remove('/');
QString filename = artist + "-" + album + ".jpg";
filename = filename.toLower();
filename.replace(' ', '-');
filename.replace("--", "-");
filename.replace(230, "ae");
filename.replace(198, "AE");
filename.replace(246, 'o');
filename.replace(248, 'o');
filename.replace(214, 'O');
filename.replace(216, 'O');
filename.replace(228, 'a');
filename.replace(229, 'a');
filename.replace(196, 'A');
filename.replace(197, 'A');
filename.remove(OrganiseFormat::kValidFatCharacters);
return filename;
}
QString AlbumCoverLoader::CreateCoverFilename(const QString &artist, const QString &album) {
QString filename(cover_pattern_);
filename.replace("%albumartist", artist);
filename.replace("%artist", artist);
filename.replace("%album", album);
return filename;
}
void AlbumCoverLoader::CancelTask(const quint64 id) {
QMutexLocker l(&mutex_);
for (QQueue<Task>::iterator it = tasks_.begin(); it != tasks_.end(); ++it) {
@@ -83,7 +223,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions& options,
return LoadImageAsync(options, song.art_automatic(), song.art_manual(), song.url().toLocalFile(), song.image());
}
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QString &art_automatic, const QString &art_manual, const QString &song_filename, const QImage &embedded_image) {
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QString &song_filename, const QImage &embedded_image) {
Task task;
task.options = options;
@@ -102,6 +242,7 @@ quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options,
metaObject()->invokeMethod(this, "ProcessTasks", Qt::QueuedConnection);
return task.id;
}
void AlbumCoverLoader::ProcessTasks() {
@@ -129,12 +270,13 @@ void AlbumCoverLoader::ProcessTask(Task *task) {
if (result.loaded_success) {
QImage scaled = ScaleAndPad(task->options, result.image);
emit ImageLoaded(task->id, scaled);
emit ImageLoaded(task->id, scaled, result.image);
emit ImageLoaded(task->id, result.cover_url, scaled);
emit ImageLoaded(task->id, result.cover_url, scaled, result.image);
return;
}
NextState(task);
}
void AlbumCoverLoader::NextState(Task *task) {
@@ -146,8 +288,8 @@ void AlbumCoverLoader::NextState(Task *task) {
}
else {
// Give up
emit ImageLoaded(task->id, task->options.default_output_image_);
emit ImageLoaded(task->id, task->options.default_output_image_, task->options.default_output_image_);
emit ImageLoaded(task->id, QUrl(), task->options.default_output_image_);
emit ImageLoaded(task->id, QUrl(), task->options.default_output_image_, task->options.default_output_image_);
}
}
@@ -156,47 +298,56 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(const Task &task)
// An image embedded in the song itself takes priority
if (!task.embedded_image.isNull())
return TryLoadResult(false, true, ScaleAndPad(task.options, task.embedded_image));
return TryLoadResult(false, true, QUrl(), ScaleAndPad(task.options, task.embedded_image));
QUrl cover_url;
QString filename;
switch (task.state) {
case State_TryingAuto: filename = task.art_automatic; break;
case State_TryingManual: filename = task.art_manual; break;
case State_TryingAuto: cover_url = task.art_automatic; break;
case State_TryingManual: cover_url = task.art_manual; break;
}
if (filename == Song::kManuallyUnsetCover)
return TryLoadResult(false, true, task.options.default_output_image_);
if (cover_url.path() == Song::kManuallyUnsetCover)
return TryLoadResult(false, true, QUrl(), task.options.default_output_image_);
if (filename == Song::kEmbeddedCover && !task.song_filename.isEmpty()) {
else if (cover_url.path() == Song::kEmbeddedCover && !task.song_filename.isEmpty()) {
const QImage taglib_image = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task.song_filename);
if (!taglib_image.isNull())
return TryLoadResult(false, true, ScaleAndPad(task.options, taglib_image));
return TryLoadResult(false, true, QUrl(), ScaleAndPad(task.options, taglib_image));
}
if (filename.toLower().startsWith("http://") || filename.toLower().startsWith("https://")) {
QUrl url(filename);
QNetworkReply *reply = network_->get(QNetworkRequest(url));
NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*)), reply);
remote_tasks_.insert(reply, task);
return TryLoadResult(true, false, QImage());
if (cover_url.path().isEmpty()) {
return TryLoadResult(false, false, cover_url, task.options.default_output_image_);
}
else if (filename.isEmpty()) {
// Avoid "QFSFileEngine::open: No file name specified" messages if we know that the filename is empty
return TryLoadResult(false, false, task.options.default_output_image_);
else {
if (cover_url.scheme() == "file") {
QImage image(cover_url.toLocalFile());
return TryLoadResult(false, !image.isNull(), cover_url, image.isNull() ? task.options.default_output_image_ : image);
}
else if (cover_url.scheme().isEmpty()) { // Assume a local file with no scheme.
QImage image(cover_url.path());
return TryLoadResult(false, !image.isNull(), cover_url, image.isNull() ? task.options.default_output_image_ : image);
}
else if (!cover_url.scheme().isEmpty()) { // Assume remote URL
QNetworkReply *reply = network_->get(QNetworkRequest(cover_url));
NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, const QUrl&)), reply, cover_url);
remote_tasks_.insert(reply, task);
return TryLoadResult(true, false, cover_url, QImage());
}
}
QImage image(filename);
return TryLoadResult(false, !image.isNull(), image.isNull() ? task.options.default_output_image_ : image);
return TryLoadResult(false, false, cover_url, task.options.default_output_image_);
}
void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply) {
void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url) {
reply->deleteLater();
if (!remote_tasks_.contains(reply)) return;
Task task = remote_tasks_.take(reply);
// Handle redirects.
@@ -208,7 +359,7 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply) {
QNetworkRequest request = reply->request();
request.setUrl(redirect.toUrl());
QNetworkReply *redirected_reply = network_->get(request);
NewClosure(redirected_reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*)), redirected_reply);
NewClosure(redirected_reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, const QUrl&)), redirected_reply, redirect.toUrl());
remote_tasks_.insert(redirected_reply, task);
return;
@@ -219,8 +370,8 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply) {
QImage image;
if (image.load(reply, 0)) {
QImage scaled = ScaleAndPad(task.options, image);
emit ImageLoaded(task.id, scaled);
emit ImageLoaded(task.id, scaled, image);
emit ImageLoaded(task.id, cover_url, scaled);
emit ImageLoaded(task.id, cover_url, scaled, image);
return;
}
}
@@ -256,16 +407,30 @@ QImage AlbumCoverLoader::ScaleAndPad(const AlbumCoverLoaderOptions &options, con
}
QPixmap AlbumCoverLoader::TryLoadPixmap(const QString &automatic, const QString &manual, const QString &filename) {
QPixmap AlbumCoverLoader::TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QString &filename) {
QPixmap ret;
if (manual == Song::kManuallyUnsetCover) return ret;
if (!manual.isEmpty()) ret.load(manual);
if (manual.path() == Song::kManuallyUnsetCover) return ret;
if (!manual.path().isEmpty()) {
if (manual.scheme().isEmpty()) {
ret.load(manual.path());
}
else if (manual.scheme() == "file") {
ret.load(manual.toLocalFile());
}
}
if (ret.isNull()) {
if (automatic == Song::kEmbeddedCover && !filename.isNull())
if (automatic.path() == Song::kEmbeddedCover && !filename.isEmpty()) {
ret = QPixmap::fromImage(TagReaderClient::Instance()->LoadEmbeddedArtBlocking(filename));
else if (!automatic.isEmpty())
ret.load(automatic);
}
else if (!automatic.path().isEmpty()) {
if (automatic.scheme().isEmpty()) {
ret.load(automatic.path());
}
else if (manual.scheme() == "file") {
ret.load(automatic.toLocalFile());
}
}
}
return ret;

View File

@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,6 +38,7 @@
#include <QNetworkReply>
#include "core/song.h"
#include "settings/collectionsettingspage.h"
#include "albumcoverloaderoptions.h"
class Song;
@@ -48,26 +50,31 @@ class AlbumCoverLoader : public QObject {
public:
explicit AlbumCoverLoader(QObject *parent = nullptr);
void ReloadSettings();
void Stop() { stop_requested_ = true; }
static QString ImageCacheDir();
static QString ImageCacheDir(const Song::Source source);
QString CreateCoverFilename(const QString &artist, const QString &album);
QString CoverFilePath(const Song::Source source, const QString &artist, QString album, const QString &album_id, const QString &album_dir, const QUrl &cover_url);
QString AlbumCoverFileName(QString artist, QString album);
quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const Song &song);
virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QString &art_automatic, const QString &art_manual, const QString &song_filename = QString(), const QImage &embedded_image = QImage());
virtual quint64 LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &art_manual, const QString &song_filename = QString(), const QImage &embedded_image = QImage());
void CancelTask(quint64 id);
void CancelTask(const quint64 id);
void CancelTasks(const QSet<quint64> &ids);
static QPixmap TryLoadPixmap(const QString &automatic, const QString &manual, const QString &filename = QString());
static QPixmap TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QString &filename = QString());
static QImage ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image);
signals:
void ImageLoaded(quint64 id, const QImage &image);
void ImageLoaded(quint64 id, const QImage &scaled, const QImage &original);
void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image);
void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &scaled, const QImage &original);
protected slots:
void ProcessTasks();
void RemoteFetchFinished(QNetworkReply *reply);
void RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url);
protected:
enum State {
@@ -81,8 +88,8 @@ signals:
AlbumCoverLoaderOptions options;
quint64 id;
QString art_automatic;
QString art_manual;
QUrl art_automatic;
QUrl art_manual;
QString song_filename;
QImage embedded_image;
State state;
@@ -90,10 +97,12 @@ signals:
};
struct TryLoadResult {
TryLoadResult(bool async, bool success, const QImage &i) : started_async(async), loaded_success(success), image(i) {}
TryLoadResult(bool async, bool success, const QUrl &_cover_url, const QImage &_image) : started_async(async), loaded_success(success), cover_url(_cover_url), image(_image) {}
bool started_async;
bool loaded_success;
QUrl cover_url;
QImage image;
};
@@ -111,6 +120,14 @@ signals:
NetworkAccessManager *network_;
static const int kMaxRedirects = 3;
bool cover_album_dir_;
CollectionSettingsPage::SaveCover cover_filename_;
QString cover_pattern_;
bool cover_overwrite_;
bool cover_lowercase_;
bool cover_replace_spaces_;
};
#endif // ALBUMCOVERLOADER_H

View File

@@ -39,4 +39,3 @@ struct AlbumCoverLoaderOptions {
};
#endif // ALBUMCOVERLOADEROPTIONS_H

View File

@@ -117,7 +117,7 @@ AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collec
ui_->action_add_to_playlist->setIcon(IconLoader::Load("media-play" ));
ui_->action_load->setIcon(IconLoader::Load("media-play" ));
album_cover_choice_controller_->SetApplication(app_);
album_cover_choice_controller_->Init(app_);
cover_searcher_ = new AlbumCoverSearcher(no_cover_item_icon_, app_, this);
cover_export_ = new AlbumCoverExport(this);
@@ -146,7 +146,7 @@ AlbumCoverManager::~AlbumCoverManager() {
}
void AlbumCoverManager::ReloadSettings() {
album_cover_choice_controller_->ReloadSettings();
app_->album_cover_loader()->ReloadSettings();
}
CollectionBackend *AlbumCoverManager::backend() const {
@@ -198,7 +198,7 @@ void AlbumCoverManager::Init() {
connect(ui_->view, SIGNAL(clicked()), ui_->view, SLOT(showMenu()));
connect(ui_->button_fetch, SIGNAL(clicked()), SLOT(FetchAlbumCovers()));
connect(ui_->export_covers, SIGNAL(clicked()), SLOT(ExportCovers()));
connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)), SLOT(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)));
connect(cover_fetcher_, SIGNAL(AlbumCoverFetched(const quint64, const QUrl&, const QImage&, const CoverSearchStatistics&)), SLOT(AlbumCoverFetched(const quint64, const QUrl&, const QImage&, const CoverSearchStatistics&)));
connect(ui_->action_fetch, SIGNAL(triggered()), SLOT(FetchSingleCover()));
connect(ui_->albums, SIGNAL(doubleClicked(QModelIndex)), SLOT(AlbumDoubleClicked(QModelIndex)));
connect(ui_->action_add_to_playlist, SIGNAL(triggered()), SLOT(AddSelectedToPlaylist()));
@@ -214,7 +214,7 @@ void AlbumCoverManager::Init() {
ui_->splitter->setSizes(QList<int>() << 200 << width() - 200);
}
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(CoverImageLoaded(quint64, QImage)));
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(const quint64, const QUrl&, const QImage&)), SLOT(CoverImageLoaded(const quint64, const QUrl&, const QImage&)));
cover_searcher_->Init(cover_fetcher_);
@@ -357,7 +357,7 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) {
}
void AlbumCoverManager::CoverImageLoaded(quint64 id, const QImage &image) {
void AlbumCoverManager::CoverImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) {
if (!cover_loading_tasks_.contains(id)) return;
@@ -455,14 +455,14 @@ void AlbumCoverManager::FetchAlbumCovers() {
}
void AlbumCoverManager::AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics) {
void AlbumCoverManager::AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics) {
if (!cover_fetching_tasks_.contains(id))
return;
QListWidgetItem *item = cover_fetching_tasks_.take(id);
if (!image.isNull()) {
SaveAndSetCover(item, image);
SaveAndSetCover(item, cover_url, image);
}
if (cover_fetching_tasks_.isEmpty()) {
@@ -537,7 +537,7 @@ Song AlbumCoverManager::GetFirstSelectedAsSong() {
Song AlbumCoverManager::ItemAsSong(QListWidgetItem *item) {
Song result;
Song result(Song::Source_Collection);
QString title = item->data(Role_AlbumName).toString();
QString artist_name = EffectiveAlbumArtistName(*item);
@@ -554,8 +554,8 @@ Song AlbumCoverManager::ItemAsSong(QListWidgetItem *item) {
result.set_url(item->data(Role_FirstUrl).toUrl());
result.set_art_automatic(item->data(Role_PathAutomatic).toString());
result.set_art_manual(item->data(Role_PathManual).toString());
result.set_art_automatic(item->data(Role_PathAutomatic).toUrl());
result.set_art_manual(item->data(Role_PathManual).toUrl());
// force validity
result.set_valid(true);
@@ -587,10 +587,10 @@ void AlbumCoverManager::FetchSingleCover() {
}
void AlbumCoverManager::UpdateCoverInList(QListWidgetItem *item, const QString &cover) {
void AlbumCoverManager::UpdateCoverInList(QListWidgetItem *item, const QUrl &cover_url) {
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QString(), cover);
item->setData(Role_PathManual, cover);
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QUrl(), cover_url);
item->setData(Role_PathManual, cover_url);
cover_loading_tasks_[id] = item;
}
@@ -602,10 +602,10 @@ void AlbumCoverManager::LoadCoverFromFile() {
QListWidgetItem *item = context_menu_items_[0];
QString cover = album_cover_choice_controller_->LoadCoverFromFile(&song);
QUrl cover_url = album_cover_choice_controller_->LoadCoverFromFile(&song);
if (!cover.isEmpty()) {
UpdateCoverInList(item, cover);
if (!cover_url.isEmpty()) {
UpdateCoverInList(item, cover_url);
}
}
@@ -622,11 +622,23 @@ void AlbumCoverManager::SaveCoverToFile() {
image = no_cover_image_;
}
else {
if (!song.art_manual().isEmpty() && QFile::exists(song.art_manual())) {
image = QImage(song.art_manual());
if (!song.art_manual().isEmpty() && !song.art_manual().path().isEmpty() &&
(
(song.art_manual().scheme().isEmpty() && QFile::exists(song.art_manual().path()))
||
(song.art_manual().scheme() == "file" && QFile::exists(song.art_manual().toLocalFile()))
)
) {
image = QImage(song.art_manual().toLocalFile());
}
else if(!song.art_automatic().isEmpty() && QFile::exists(song.art_automatic())) {
image = QImage(song.art_automatic());
if (!song.art_automatic().isEmpty() && !song.art_automatic().path().isEmpty() &&
(
(song.art_automatic().scheme().isEmpty() && QFile::exists(song.art_automatic().path()))
||
(song.art_automatic().scheme() == "file" && QFile::exists(song.art_automatic().toLocalFile()))
)
) {
image = QImage(song.art_automatic().toLocalFile());
}
else {
image = no_cover_image_;
@@ -644,10 +656,10 @@ void AlbumCoverManager::LoadCoverFromURL() {
QListWidgetItem *item = context_menu_items_[0];
QString cover = album_cover_choice_controller_->LoadCoverFromURL(&song);
QUrl cover_url = album_cover_choice_controller_->LoadCoverFromURL(&song);
if (!cover.isEmpty()) {
UpdateCoverInList(item, cover);
if (!cover_url.isEmpty()) {
UpdateCoverInList(item, cover_url);
}
}
@@ -659,18 +671,18 @@ void AlbumCoverManager::SearchForCover() {
QListWidgetItem *item = context_menu_items_[0];
QString cover = album_cover_choice_controller_->SearchForCover(&song);
if (cover.isEmpty()) return;
QUrl cover_url = album_cover_choice_controller_->SearchForCover(&song);
if (cover_url.isEmpty()) return;
// Force the found cover on all of the selected items
for (QListWidgetItem *current : context_menu_items_) {
// Don't save the first one twice
if (current != item) {
Song current_song = ItemAsSong(current);
album_cover_choice_controller_->SaveCover(&current_song, cover);
album_cover_choice_controller_->SaveCoverToSong(&current_song, cover_url);
}
UpdateCoverInList(current, cover);
UpdateCoverInList(current, cover_url);
}
}
@@ -682,17 +694,17 @@ void AlbumCoverManager::UnsetCover() {
QListWidgetItem *item = context_menu_items_[0];
QString cover = album_cover_choice_controller_->UnsetCover(&song);
QUrl cover_url = album_cover_choice_controller_->UnsetCover(&song);
// Force the 'none' cover on all of the selected items
for (QListWidgetItem *current : context_menu_items_) {
current->setIcon(no_cover_item_icon_);
current->setData(Role_PathManual, cover);
current->setData(Role_PathManual, cover_url);
// Don't save the first one twice
if (current != item) {
Song current_song = ItemAsSong(current);
album_cover_choice_controller_->SaveCover(&current_song, cover);
album_cover_choice_controller_->SaveCoverToSong(&current_song, cover_url);
}
}
@@ -776,22 +788,22 @@ void AlbumCoverManager::LoadSelectedToPlaylist() {
}
void AlbumCoverManager::SaveAndSetCover(QListWidgetItem *item, const QImage &image) {
void AlbumCoverManager::SaveAndSetCover(QListWidgetItem *item, const QUrl &cover_url, const QImage &image) {
const QString artist = item->data(Role_ArtistName).toString();
const QString albumartist = item->data(Role_AlbumArtistName).toString();
const QString album = item->data(Role_AlbumName).toString();
const QUrl url = item->data(Role_FirstUrl).toUrl();
QString path = album_cover_choice_controller_->SaveCoverToFileAutomatic((!albumartist.isEmpty() ? albumartist : artist), artist, album, url.adjusted(QUrl::RemoveFilename).path(), image);
if (path.isEmpty()) return;
QUrl new_cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(Song::Source_Collection, (!albumartist.isEmpty() ? albumartist : artist), album, QString(), url.adjusted(QUrl::RemoveFilename).path(), cover_url, image, false);
if (new_cover_url.isEmpty()) return;
// Save the image in the database
collection_backend_->UpdateManualAlbumArtAsync(artist, albumartist, album, path);
collection_backend_->UpdateManualAlbumArtAsync(artist, albumartist, album, new_cover_url);
// Update the icon in our list
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QString(), path);
item->setData(Role_PathManual, path);
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QUrl(), new_cover_url);
item->setData(Role_PathManual, new_cover_url);
cover_loading_tasks_[id] = item;
}

View File

@@ -93,11 +93,11 @@ class AlbumCoverManager : public QMainWindow {
private slots:
void ArtistChanged(QListWidgetItem *current);
void CoverImageLoaded(quint64 id, const QImage &image);
void CoverImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image);
void UpdateFilter();
void FetchAlbumCovers();
void ExportCovers();
void AlbumCoverFetched(quint64 id, const QImage &image, const CoverSearchStatistics &statistics);
void AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics);
void CancelRequests();
// On the context menu
@@ -115,7 +115,7 @@ class AlbumCoverManager : public QMainWindow {
void AddSelectedToPlaylist();
void LoadSelectedToPlaylist();
void UpdateCoverInList(QListWidgetItem *item, const QString &cover);
void UpdateCoverInList(QListWidgetItem *item, const QUrl &cover);
void UpdateExportStatus(int exported, int bad, int count);
private:
@@ -152,7 +152,7 @@ class AlbumCoverManager : public QMainWindow {
void UpdateStatusText();
bool ShouldHide(const QListWidgetItem &item, const QString &filter, HideCovers hide) const;
void SaveAndSetCover(QListWidgetItem *item, const QImage &image);
void SaveAndSetCover(QListWidgetItem *item, const QUrl &cover_url, const QImage &image);
private:
Ui_CoverManager *ui_;

View File

@@ -127,7 +127,7 @@ AlbumCoverSearcher::AlbumCoverSearcher(const QIcon &no_cover_icon, Application *
options_.scale_output_image_ = false;
options_.pad_output_image_ = false;
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(ImageLoaded(quint64, QImage)));
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(ImageLoaded(quint64, QUrl, QImage)));
connect(ui_->search, SIGNAL(clicked()), SLOT(Search()));
connect(ui_->covers, SIGNAL(doubleClicked(QModelIndex)), SLOT(CoverDoubleClicked(QModelIndex)));
@@ -145,7 +145,7 @@ AlbumCoverSearcher::~AlbumCoverSearcher() {
void AlbumCoverSearcher::Init(AlbumCoverFetcher *fetcher) {
fetcher_ = fetcher;
connect(fetcher_, SIGNAL(SearchFinished(quint64,CoverSearchResults,CoverSearchStatistics)), SLOT(SearchFinished(quint64, CoverSearchResults)));
connect(fetcher_, SIGNAL(SearchFinished(quint64, CoverSearchResults, CoverSearchStatistics)), SLOT(SearchFinished(quint64, CoverSearchResults)));
}
@@ -197,7 +197,7 @@ void AlbumCoverSearcher::Search() {
}
void AlbumCoverSearcher::SearchFinished(quint64 id, const CoverSearchResults &results) {
void AlbumCoverSearcher::SearchFinished(const quint64 id, const CoverSearchResults &results) {
if (id != id_)
return;
@@ -212,7 +212,7 @@ void AlbumCoverSearcher::SearchFinished(quint64 id, const CoverSearchResults &re
for (const CoverSearchResult &result : results) {
if (result.image_url.isEmpty()) continue;
quint64 id = app_->album_cover_loader()->LoadImageAsync(options_, result.image_url.toString(), QString());
quint64 id = app_->album_cover_loader()->LoadImageAsync(options_, result.image_url, QUrl());
QStandardItem *item = new QStandardItem;
item->setIcon(no_cover_icon_);
@@ -232,7 +232,7 @@ void AlbumCoverSearcher::SearchFinished(quint64 id, const CoverSearchResults &re
}
void AlbumCoverSearcher::ImageLoaded(quint64 id, const QImage &image) {
void AlbumCoverSearcher::ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) {
if (!cover_loading_tasks_.contains(id)) return;
QStandardItem *item = cover_loading_tasks_.take(id);

View File

@@ -87,8 +87,8 @@ protected:
private slots:
void Search();
void SearchFinished(quint64 id, const CoverSearchResults &results);
void ImageLoaded(quint64 id, const QImage &image);
void SearchFinished(const quint64 id, const CoverSearchResults &results);
void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image);
void CoverDoubleClicked(const QModelIndex &index);

View File

@@ -59,11 +59,11 @@ QString CoverExportRunnable::GetCoverPath() {
// Export downloaded covers?
}
else if (!song_.art_manual().isEmpty() && dialog_result_.export_downloaded_) {
return song_.art_manual();
return song_.art_manual().toLocalFile();
// Export embedded covers?
}
else if (!song_.art_automatic().isEmpty() && song_.art_automatic() == Song::kEmbeddedCover && dialog_result_.export_embedded_) {
return song_.art_automatic();
else if (!song_.art_automatic().isEmpty() && song_.art_automatic().path() == Song::kEmbeddedCover && dialog_result_.export_embedded_) {
return song_.art_automatic().toLocalFile();
}
else {
return QString();
@@ -168,8 +168,7 @@ void CoverExportRunnable::ExportCover() {
return;
}
// we're handling overwrite as remove + copy so we need to delete the old file
// first
// We're handling overwrite as remove + copy so we need to delete the old file first
if (dialog_result_.overwrite_ != AlbumCoverExport::OverwriteMode_None && QFile::exists(new_file)) {
if (!QFile::remove(new_file)) {
EmitCoverSkipped();
@@ -186,7 +185,7 @@ void CoverExportRunnable::ExportCover() {
}
}
else {
// automatic or manual cover, available in an image file
// Automatic or manual cover, available in an image file
if (!QFile::copy(cover_path, new_file)) {
EmitCoverSkipped();
return;

View File

@@ -29,8 +29,9 @@
#include <QList>
#include <QString>
#include "albumcoverfetcher.h"
class Application;
struct CoverSearchResult;
// Each implementation of this interface downloads covers from one online service.
// There are no limitations on what this service might be - last.fm, Amazon, Google Images - you name it.
@@ -53,7 +54,7 @@ class CoverProvider : public QObject {
virtual void CancelSearch(int id) {}
signals:
void SearchFinished(int id, const QList<CoverSearchResult>& results);
void SearchFinished(int id, const CoverSearchResults& results);
private:
Application *app_;

View File

@@ -28,63 +28,71 @@
#include <QString>
#include <QStringBuilder>
#include <QImage>
#include <QStandardPaths>
#include <QTemporaryFile>
#include "core/application.h"
#include "playlist/playlistmanager.h"
#include "albumcoverloader.h"
#include "currentartloader.h"
#include "currentalbumcoverloader.h"
CurrentArtLoader::CurrentArtLoader(Application *app, QObject *parent)
CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(Application *app, QObject *parent)
: QObject(parent),
app_(app),
temp_file_pattern_(QDir::tempPath() + "/strawberry-art-XXXXXX.jpg"),
id_(0)
temp_file_pattern_(QDir::tempPath() + "/strawberry-cover-XXXXXX.jpg"),
id_(0)
{
options_.scale_output_image_ = false;
options_.pad_output_image_ = false;
options_.default_output_image_ = QImage(":/pictures/cdcase.png");
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QImage)), SLOT(TempArtLoaded(quint64, QImage)));
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(LoadArt(Song)));
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(TempAlbumCoverLoaded(quint64, QUrl, QImage)));
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(LoadAlbumCover(Song)));
}
CurrentArtLoader::~CurrentArtLoader() {}
CurrentAlbumCoverLoader::~CurrentAlbumCoverLoader() {}
void CurrentArtLoader::LoadArt(const Song &song) {
void CurrentAlbumCoverLoader::LoadAlbumCover(const Song &song) {
last_song_ = song;
id_ = app_->album_cover_loader()->LoadImageAsync(options_, last_song_);
}
void CurrentArtLoader::TempArtLoaded(quint64 id, const QImage &image) {
void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, const QUrl &remote_url, const QImage &image) {
if (id != id_) return;
id_ = 0;
QString uri;
QString thumbnail_uri;
QUrl cover_url;
QUrl thumbnail_url;
QImage thumbnail;
if (!image.isNull()) {
temp_art_.reset(new QTemporaryFile(temp_file_pattern_));
temp_art_->setAutoRemove(true);
temp_art_->open();
image.save(temp_art_->fileName(), "JPEG");
QString filename;
temp_cover_.reset(new QTemporaryFile(temp_file_pattern_));
temp_cover_->setAutoRemove(true);
temp_cover_->open();
image.save(temp_cover_->fileName(), "JPEG");
// Scale the image down to make a thumbnail. It's a bit crap doing it here since it's the GUI thread, but the alternative is hard.
temp_art_thumbnail_.reset(new QTemporaryFile(temp_file_pattern_));
temp_art_thumbnail_->open();
temp_art_thumbnail_->setAutoRemove(true);
temp_cover_thumbnail_.reset(new QTemporaryFile(temp_file_pattern_));
temp_cover_thumbnail_->open();
temp_cover_thumbnail_->setAutoRemove(true);
thumbnail = image.scaledToHeight(120, Qt::SmoothTransformation);
thumbnail.save(temp_art_thumbnail_->fileName(), "JPEG");
thumbnail.save(temp_cover_thumbnail_->fileName(), "JPEG");
uri = "file://" + temp_art_->fileName();
thumbnail_uri = "file://" + temp_art_thumbnail_->fileName();
cover_url.setScheme("file");
cover_url.setPath(temp_cover_->fileName());
thumbnail_url.setScheme("file");
thumbnail_url.setPath(temp_cover_thumbnail_->fileName());
}
emit ArtLoaded(last_song_, uri, image);
emit ThumbnailLoaded(last_song_, thumbnail_uri, thumbnail);
emit AlbumCoverLoaded(last_song_, cover_url, image);
emit ThumbnailLoaded(last_song_, thumbnail_url, thumbnail);
}

View File

@@ -18,8 +18,8 @@
*
*/
#ifndef CURRENTARTLOADER_H
#define CURRENTARTLOADER_H
#ifndef CURRENTALBUMCOVERLOADER_H
#define CURRENTALBUMCOVERLOADER_H
#include "config.h"
@@ -36,25 +36,25 @@
class Application;
class CurrentArtLoader : public QObject {
class CurrentAlbumCoverLoader : public QObject {
Q_OBJECT
public:
explicit CurrentArtLoader(Application *app, QObject *parent = nullptr);
~CurrentArtLoader();
explicit CurrentAlbumCoverLoader(Application *app, QObject *parent = nullptr);
~CurrentAlbumCoverLoader();
const AlbumCoverLoaderOptions &options() const { return options_; }
const Song &last_song() const { return last_song_; }
public slots:
void LoadArt(const Song &song);
void LoadAlbumCover(const Song &song);
signals:
void ArtLoaded(const Song &song, const QString &uri, const QImage &image);
void ThumbnailLoaded(const Song &song, const QString &uri, const QImage &image);
signals:
void AlbumCoverLoaded(const Song &song, const QUrl &cover_url, const QImage &image);
void ThumbnailLoaded(const Song &song, const QUrl &thumbnail_uri, const QImage &image);
private slots:
void TempArtLoaded(quint64 id, const QImage &image);
void TempAlbumCoverLoaded(const quint64 id, const QUrl &remote_url, const QImage &image);
private:
Application *app_;
@@ -62,11 +62,12 @@ signals:
QString temp_file_pattern_;
std::unique_ptr<QTemporaryFile> temp_art_;
std::unique_ptr<QTemporaryFile> temp_art_thumbnail_;
std::unique_ptr<QTemporaryFile> temp_cover_;
std::unique_ptr<QTemporaryFile> temp_cover_thumbnail_;
quint64 id_;
Song last_song_;
};
#endif // CURRENTARTLOADER_H
#endif // CURRENTALBUMCOVERLOADER_H

View File

@@ -53,13 +53,13 @@ DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): Cov
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
typedef QPair<QString, QString> Param;
typedef QList<Param> Parameters;
typedef QList<Param> Params;
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> EncodedParamList;
Parameters params = Parameters() << Param("output", "json")
<< Param("q", QString(artist + " " + album))
<< Param("limit", QString::number(kLimit));
const Params params = Params() << Param("output", "json")
<< Param("q", QString(artist + " " + album))
<< Param("limit", QString::number(kLimit));
QUrlQuery url_query;
for (const Param &param : params) {
@@ -88,18 +88,18 @@ QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
if (reply->error() != QNetworkReply::NoError && 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);
QString error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Error(error);
}
else {
// See if there is Json data containing "error" - then use that instead.
// See if there is Json data containing "error" object - 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()) {
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
QString error;
if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("error")) {
QJsonValue json_value_error = json_obj["error"];
@@ -108,12 +108,19 @@ QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) {
int code = json_error["code"].toInt();
QString message = json_error["message"].toString();
QString type = json_error["type"].toString();
failure_reason = QString("%1 (%2)").arg(message).arg(code);
error = QString("%1 (%2)").arg(message).arg(code);
}
}
}
if (failure_reason.isEmpty()) failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Error(failure_reason);
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
Error(error);
}
return QByteArray();
}

View File

@@ -184,30 +184,32 @@ QByteArray DiscogsCoverProvider::GetReplyData(QNetworkReply *reply) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
if (reply->error() != QNetworkReply::NoError && 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);
QString error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Error(error);
}
else {
// See if there is Json data containing "message" - 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()) {
QString error;
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("message")) {
failure_reason = json_obj["message"].toString();
error = json_obj["message"].toString();
}
}
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
Error(failure_reason);
Error(error);
}
return QByteArray();
}

View File

@@ -40,6 +40,7 @@ class Application;
// This struct represents a single search-for-cover request. It identifies and describes the request.
struct DiscogsCoverSearchContext {
DiscogsCoverSearchContext() : id(-1), r_count(0) {}
// The unique request identifier
int id;
@@ -55,6 +56,7 @@ Q_DECLARE_METATYPE(DiscogsCoverSearchContext)
// This struct represents a single release request. It identifies and describes the request.
struct DiscogsCoverReleaseContext {
DiscogsCoverReleaseContext() : id(-1) {}
int id; // The unique request identifier
int s_id; // The search request identifier
@@ -102,4 +104,3 @@ class DiscogsCoverProvider : public CoverProvider {
};
#endif // DISCOGSCOVERPROVIDER_H

View File

@@ -204,32 +204,33 @@ QByteArray LastFmCoverProvider::GetReplyData(QNetworkReply *reply) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
if (reply->error() != QNetworkReply::NoError && 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);
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
else {
// See if there is Json data containing "error" and "message" - 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()) {
QString error;
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("error") && json_obj.contains("message")) {
int error = json_obj["error"].toInt();
int code = json_obj["error"].toInt();
QString message = json_obj["message"].toString();
failure_reason = "Error: " + QString::number(error) + ": " + message;
error = "Error: " + QString::number(code) + ": " + message;
}
}
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
Error(failure_reason);
Error(error);
}
return QByteArray();
}

View File

@@ -184,7 +184,7 @@ QByteArray MusicbrainzCoverProvider::GetReplyData(QNetworkReply *reply) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
if (reply->error() != QNetworkReply::NoError && 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);
@@ -192,22 +192,24 @@ QByteArray MusicbrainzCoverProvider::GetReplyData(QNetworkReply *reply) {
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()) {
QString error;
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_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();
error = json_obj["error"].toString();
}
}
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
else {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
Error(failure_reason);
Error(error);
}
return QByteArray();
}

View File

@@ -57,14 +57,14 @@ TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) :
}
bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, int id) {
bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) {
if (!service_ || !service_->authenticated()) return false;
QList<Param> parameters;
parameters << Param("query", QString(artist + " " + album));
parameters << Param("limit", QString::number(kLimit));
QNetworkReply *reply = CreateRequest("search/albums", parameters);
ParamList params = ParamList() << Param("query", QString(artist + " " + album))
<< Param("limit", QString::number(kLimit));
QNetworkReply *reply = CreateRequest("search/albums", params);
NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply*, int)), reply, id);
return true;
@@ -73,20 +73,13 @@ bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album
void TidalCoverProvider::CancelSearch(int id) {}
QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name, const QList<Param> &params_supplied) {
QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name, const ParamList &params_supplied) {
typedef QPair<QString, QString> Param;
typedef QList<Param> ParamList;
typedef QPair<QByteArray, QByteArray> EncodedParam;
typedef QList<EncodedParam> EncodedParamList;
ParamList parameters = ParamList()
<< params_supplied
<< Param("sessionId", service_->session_id())
<< Param("countryCode", service_->country_code());
const ParamList params = ParamList() << params_supplied
<< Param("countryCode", service_->country_code());
QUrlQuery url_query;
for (const Param& param : parameters) {
for (const Param &param : params) {
EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
url_query.addQueryItem(encoded_param.first, encoded_param.second);
}
@@ -95,7 +88,8 @@ QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name,
url.setQuery(url_query);
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
req.setRawHeader("X-Tidal-SessionId", service_->session_id().toUtf8());
if (!service_->access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + service_->access_token().toUtf8());
if (!service_->session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", service_->session_id().toUtf8());
QNetworkReply *reply = network_->get(req);
return reply;
@@ -106,38 +100,42 @@ QByteArray TidalCoverProvider::GetReplyData(QNetworkReply *reply, QString &error
QByteArray data;
if (reply->error() == QNetworkReply::NoError) {
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() < 200) {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
// This is a network error, there is nothing more to do.
error = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
else {
// See if there is Json data containing "userMessage" - then use that instead.
// See if there is Json data containing "status" and "userMessage" - then use that instead.
data = reply->readAll();
QJsonParseError parse_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
int status = 0;
int sub_status = 0;
QString failure_reason;
if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) {
status = json_obj["status"].toInt();
sub_status = json_obj["subStatus"].toInt();
QString user_message = json_obj["userMessage"].toString();
failure_reason = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status);
error = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status);
}
}
if (failure_reason.isEmpty()) {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
if (status == 401 && sub_status == 6001) { // User does not have a valid session
service_->Logout();
}
error = Error(failure_reason);
error = Error(error);
}
return QByteArray();
}
@@ -195,7 +193,7 @@ QJsonValue TidalCoverProvider::ExtractItems(QJsonObject &json_obj, QString &erro
}
void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, int id) {
void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
reply->deleteLater();

View File

@@ -43,20 +43,21 @@ class TidalCoverProvider : public CoverProvider {
public:
explicit TidalCoverProvider(Application *app, QObject *parent = nullptr);
bool StartSearch(const QString &artist, const QString &album, int id);
bool StartSearch(const QString &artist, const QString &album, const int id);
void CancelSearch(int id);
private slots:
void HandleSearchReply(QNetworkReply *reply, int id);
void HandleSearchReply(QNetworkReply *reply, const int id);
private:
typedef QPair<QString, QString> Param;
typedef QList<Param> ParamList;
typedef QPair<QByteArray, QByteArray> EncodedParam;
static const char *kApiUrl;
static const char *kResourcesUrl;
static const char *kApiTokenB64;
static const int kLimit;
QNetworkReply *CreateRequest(const QString &ressource_name, const QList<Param> &params_supplied);
QNetworkReply *CreateRequest(const QString &ressource_name, const ParamList &params_supplied);
QByteArray GetReplyData(QNetworkReply *reply, QString &error);
QJsonObject ExtractJsonObj(QByteArray &data, QString &error);
QJsonValue ExtractItems(QByteArray &data, QString &error);