Improve album cover loader, lyrics search and streaming support
- Improve album cover loader - Add album cover loader result struct - Move album cover thumbnail scaling to album cover loader - Make init art manual look for album cover images in song directory - Make album cover search work for songs outside of collection and streams - Make album cover search work based on artist + title if album is not present - Update art manual in playlist for local files, devices and CDDA - Make lyrics search work for streams - Add stream dialog to menu - Remove dead code in InternetSearchModel - Simplify code in InternetSearchView
This commit is contained in:
@@ -312,12 +312,14 @@ void AlbumCoverChoiceController::ShowCover(const Song &song, const QPixmap &pixm
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) {
|
||||
qint64 AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) {
|
||||
|
||||
qint64 id = cover_fetcher_->FetchAlbumCover(song.effective_albumartist(), song.effective_album(), true);
|
||||
qint64 id = cover_fetcher_->FetchAlbumCover(song.effective_albumartist(), song.album(), song.title(), true);
|
||||
|
||||
cover_fetching_tasks_[id] = song;
|
||||
|
||||
return id;
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const QUrl &cover_url, const QImage &image, const CoverSearchStatistics &statistics) {
|
||||
@@ -371,7 +373,7 @@ void AlbumCoverChoiceController::SaveCoverToSong(Song *song, const QUrl &cover_u
|
||||
|
||||
}
|
||||
|
||||
if (song->url() == app_->current_albumcover_loader()->last_song().url()) {
|
||||
if (*song == app_->current_albumcover_loader()->last_song()) {
|
||||
app_->current_albumcover_loader()->LoadAlbumCover(*song);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class AlbumCoverChoiceController : public QWidget {
|
||||
void ShowCover(const Song &song, const QPixmap &pixmap);
|
||||
|
||||
// Search for covers automatically
|
||||
void SearchCoverAutomatically(const Song &song);
|
||||
qint64 SearchCoverAutomatically(const Song &song);
|
||||
|
||||
// Saves the chosen cover as manual cover path of this song in collection.
|
||||
void SaveCoverToSong(Song *song, const QUrl &cover_url);
|
||||
@@ -124,7 +124,7 @@ class AlbumCoverChoiceController : public QWidget {
|
||||
|
||||
static bool CanAcceptDrag(const QDragEnterEvent *e);
|
||||
|
||||
signals:
|
||||
signals:
|
||||
void AutomaticCoverSearchDone();
|
||||
|
||||
private slots:
|
||||
|
||||
@@ -44,14 +44,15 @@ AlbumCoverFetcher::AlbumCoverFetcher(CoverProviders *cover_providers, QObject *p
|
||||
connect(request_starter_, SIGNAL(timeout()), SLOT(StartRequests()));
|
||||
}
|
||||
|
||||
quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album, bool fetchall) {
|
||||
quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString &album, const QString &title, bool fetchall) {
|
||||
|
||||
CoverSearchRequest request;
|
||||
request.id = next_id_++;
|
||||
request.artist = artist;
|
||||
request.album = album;
|
||||
request.album.remove(Song::kAlbumRemoveDisc);
|
||||
request.album.remove(Song::kAlbumRemoveMisc);
|
||||
request.album = request.album.remove(Song::kAlbumRemoveDisc);
|
||||
request.album = request.album.remove(Song::kAlbumRemoveMisc);
|
||||
request.title = title;
|
||||
request.search = false;
|
||||
request.fetchall = fetchall;
|
||||
|
||||
@@ -60,14 +61,15 @@ quint64 AlbumCoverFetcher::FetchAlbumCover(const QString &artist, const QString
|
||||
|
||||
}
|
||||
|
||||
quint64 AlbumCoverFetcher::SearchForCovers(const QString &artist, const QString &album) {
|
||||
quint64 AlbumCoverFetcher::SearchForCovers(const QString &artist, const QString &album, const QString &title) {
|
||||
|
||||
CoverSearchRequest request;
|
||||
request.id = next_id_++;
|
||||
request.artist = artist;
|
||||
request.album = album;
|
||||
request.album.remove(Song::kAlbumRemoveDisc);
|
||||
request.album.remove(Song::kAlbumRemoveMisc);
|
||||
request.album = request.album.remove(Song::kAlbumRemoveDisc);
|
||||
request.album = request.album.remove(Song::kAlbumRemoveMisc);
|
||||
request.title = title;
|
||||
request.search = true;
|
||||
request.fetchall = false;
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ struct CoverSearchRequest {
|
||||
// A search query
|
||||
QString artist;
|
||||
QString album;
|
||||
QString title;
|
||||
|
||||
// Is this only a search request or should we also fetch the first cover that's found?
|
||||
bool search;
|
||||
@@ -92,12 +93,12 @@ class AlbumCoverFetcher : public QObject {
|
||||
|
||||
static const int kMaxConcurrentRequests;
|
||||
|
||||
quint64 SearchForCovers(const QString &artist, const QString &album);
|
||||
quint64 FetchAlbumCover(const QString &artist, const QString &album, const bool fetchall);
|
||||
quint64 SearchForCovers(const QString &artist, const QString &album, const QString &title = QString());
|
||||
quint64 FetchAlbumCover(const QString &artist, const QString &album, const QString &title, const bool fetchall);
|
||||
|
||||
void Clear();
|
||||
|
||||
signals:
|
||||
signals:
|
||||
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);
|
||||
|
||||
|
||||
@@ -42,11 +42,6 @@
|
||||
#include "coverprovider.h"
|
||||
#include "coverproviders.h"
|
||||
|
||||
using std::min;
|
||||
using std::max;
|
||||
using std::stable_sort;
|
||||
using std::sqrt;
|
||||
|
||||
const int AlbumCoverFetcherSearch::kSearchTimeoutMs = 25000;
|
||||
const int AlbumCoverFetcherSearch::kImageLoadTimeoutMs = 3000;
|
||||
const int AlbumCoverFetcherSearch::kTargetSize = 500;
|
||||
@@ -81,13 +76,16 @@ void AlbumCoverFetcherSearch::Start(CoverProviders *cover_providers) {
|
||||
|
||||
// Skip provider if it does not have fetchall set, and we are doing fetchall - "Fetch Missing Covers".
|
||||
if (!provider->fetchall() && request_.fetchall) {
|
||||
//qLog(Debug) << "Skipping provider" << provider->name();
|
||||
continue;
|
||||
}
|
||||
// If album is missing, check if we can still use this provider by searching using artist + title.
|
||||
if (!provider->allow_missing_album() && request_.album.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
const bool success = provider->StartSearch(request_.artist, request_.album, request_.title, id);
|
||||
|
||||
if (success) {
|
||||
pending_requests_[id] = provider;
|
||||
@@ -112,7 +110,7 @@ void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverSe
|
||||
CoverProvider *provider = pending_requests_.take(id);
|
||||
|
||||
CoverSearchResults results_copy(results);
|
||||
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].score = provider->quality();
|
||||
if (results_copy[i].artist.toLower() == request_.artist.toLower()) {
|
||||
@@ -170,7 +168,7 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
|
||||
|
||||
// Try the first one in each category.
|
||||
QString last_provider;
|
||||
for (int i = 0; i < results_.count(); ++i) {
|
||||
for (int i = 0 ; i < results_.count() ; ++i) {
|
||||
if (results_[i].provider == last_provider) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#include "organise/organiseformat.h"
|
||||
#include "albumcoverloader.h"
|
||||
#include "albumcoverloaderoptions.h"
|
||||
#include "albumcoverloaderresult.h"
|
||||
|
||||
AlbumCoverLoader::AlbumCoverLoader(QObject *parent)
|
||||
: QObject(parent),
|
||||
@@ -98,89 +99,7 @@ void AlbumCoverLoader::ReloadSettings() {
|
||||
|
||||
}
|
||||
|
||||
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::kInvalidFatCharacters);
|
||||
if (cover_lowercase_) filename = filename.toLower();
|
||||
if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-");
|
||||
}
|
||||
else {
|
||||
switch (source) {
|
||||
case Song::Source_Tidal:
|
||||
filename = album_id + "-" + cover_url.fileName();
|
||||
break;
|
||||
case Song::Source_Subsonic:
|
||||
case Song::Source_Qobuz:
|
||||
filename = AlbumCoverFileName(artist, album);
|
||||
if (filename.length() > 8 && (filename.length() - 5) >= (artist.length() + album.length() - 2)) {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (filename.isEmpty()) return QString();
|
||||
|
||||
QString filepath(path + "/" + filename);
|
||||
|
||||
return filepath;
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::AlbumCoverFileName(QString artist, QString album) {
|
||||
QString AlbumCoverLoader::AlbumCoverFilename(QString artist, QString album) {
|
||||
|
||||
artist.remove('/');
|
||||
album.remove('/');
|
||||
@@ -196,7 +115,79 @@ QString AlbumCoverLoader::AlbumCoverFileName(QString artist, QString album) {
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CreateCoverFilename(const QString &artist, const QString &album) {
|
||||
QString AlbumCoverLoader::CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url) {
|
||||
return CoverFilePath(song.source(), song.effective_albumartist(), song.album(), song.album_id(), album_dir, cover_url);
|
||||
}
|
||||
|
||||
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 = Song::ImageCacheDir(source);
|
||||
}
|
||||
|
||||
if (path.right(1) == QDir::separator()) {
|
||||
path.chop(1);
|
||||
}
|
||||
|
||||
QDir dir;
|
||||
if (!dir.mkpath(path)) {
|
||||
qLog(Error) << "Unable to create directory" << path;
|
||||
path = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
|
||||
}
|
||||
|
||||
QString filename;
|
||||
if (source == Song::Source_Collection && cover_album_dir_ && cover_filename_ == CollectionSettingsPage::SaveCover_Pattern && !cover_pattern_.isEmpty()) {
|
||||
filename = CoverFilenameFromVariable(artist, album) + ".jpg";
|
||||
filename.remove(OrganiseFormat::kInvalidFatCharacters);
|
||||
if (cover_lowercase_) filename = filename.toLower();
|
||||
if (cover_replace_spaces_) filename.replace(QRegExp("\\s"), "-");
|
||||
}
|
||||
else {
|
||||
filename = CoverFilenameFromSource(source, cover_url, artist, album, album_id);
|
||||
}
|
||||
|
||||
QString filepath(path + "/" + filename);
|
||||
|
||||
return filepath;
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id) {
|
||||
|
||||
QString filename;
|
||||
|
||||
switch (source) {
|
||||
case Song::Source_Tidal:
|
||||
filename = album_id + "-" + cover_url.fileName();
|
||||
break;
|
||||
case Song::Source_Subsonic:
|
||||
case Song::Source_Qobuz:
|
||||
filename = AlbumCoverFilename(artist, album);
|
||||
if (filename.length() > 8 && (filename.length() - 5) >= (artist.length() + album.length() - 2)) {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
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;
|
||||
}
|
||||
|
||||
return filename;
|
||||
|
||||
}
|
||||
|
||||
QString AlbumCoverLoader::CoverFilenameFromVariable(const QString &artist, const QString &album) {
|
||||
|
||||
QString filename(cover_pattern_);
|
||||
filename.replace("%albumartist", artist);
|
||||
@@ -215,6 +206,7 @@ void AlbumCoverLoader::CancelTask(const quint64 id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverLoader::CancelTasks(const QSet<quint64> &ids) {
|
||||
@@ -228,21 +220,25 @@ void AlbumCoverLoader::CancelTasks(const QSet<quint64> &ids) {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions& options, const Song &song) {
|
||||
return LoadImageAsync(options, song.art_automatic(), song.art_manual(), song.url().toLocalFile(), song.image());
|
||||
return LoadImageAsync(options, song.art_automatic(), song.art_manual(), song.url(), song, song.image());
|
||||
}
|
||||
|
||||
quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions &options, const QUrl &art_automatic, const QUrl &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 QUrl &song_url, const Song song, const QImage &embedded_image) {
|
||||
|
||||
Task task;
|
||||
task.options = options;
|
||||
task.art_automatic = art_automatic;
|
||||
task.song = song;
|
||||
task.song_url = song_url;
|
||||
task.art_manual = art_manual;
|
||||
task.song_filename = song_filename;
|
||||
task.art_automatic = art_automatic;
|
||||
task.art_updated = false;
|
||||
task.embedded_image = embedded_image;
|
||||
task.state = State_TryingManual;
|
||||
task.type = AlbumCoverLoaderResult::Type_None;
|
||||
task.state = State_Manual;
|
||||
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
@@ -269,20 +265,20 @@ void AlbumCoverLoader::ProcessTasks() {
|
||||
|
||||
ProcessTask(&task);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverLoader::ProcessTask(Task *task) {
|
||||
|
||||
TryLoadResult result = TryLoadImage(*task);
|
||||
TryLoadResult result = TryLoadImage(task);
|
||||
if (result.started_async) {
|
||||
// The image is being loaded from a remote URL, we'll carry on later when it's done
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.loaded_success) {
|
||||
QImage scaled = ScaleAndPad(task->options, result.image);
|
||||
emit ImageLoaded(task->id, result.cover_url, scaled);
|
||||
emit ImageLoaded(task->id, result.cover_url, scaled, result.image);
|
||||
QPair<QImage, QImage> images = ScaleAndPad(task->options, result.image);
|
||||
emit AlbumCoverLoaded(task->id, AlbumCoverLoaderResult(result.type, result.cover_url, result.image, images.first, images.second, task->art_updated));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,64 +288,85 @@ void AlbumCoverLoader::ProcessTask(Task *task) {
|
||||
|
||||
void AlbumCoverLoader::NextState(Task *task) {
|
||||
|
||||
if (task->state == State_TryingManual) {
|
||||
if (task->state == State_Manual) {
|
||||
// Try the automatic one next
|
||||
task->state = State_TryingAuto;
|
||||
task->state = State_Automatic;
|
||||
ProcessTask(task);
|
||||
}
|
||||
else {
|
||||
// Give up
|
||||
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_);
|
||||
emit AlbumCoverLoaded(task->id, AlbumCoverLoaderResult(AlbumCoverLoaderResult::Type_None, QUrl(), task->options.default_output_image_, task->options.default_output_image_, task->options.default_thumbnail_image_, task->art_updated));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(const Task &task) {
|
||||
AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(Task *task) {
|
||||
|
||||
// An image embedded in the song itself takes priority
|
||||
if (!task.embedded_image.isNull())
|
||||
return TryLoadResult(false, true, QUrl(), ScaleAndPad(task.options, task.embedded_image));
|
||||
if (!task->embedded_image.isNull()) {
|
||||
QPair<QImage, QImage> images = ScaleAndPad(task->options, task->embedded_image);
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, QUrl(), images.first);
|
||||
}
|
||||
|
||||
// Use cached album cover if possible.
|
||||
if (task->state == State_Manual &&
|
||||
!task->song.art_manual_is_valid() &&
|
||||
task->art_manual.isEmpty() &&
|
||||
task->song.source() != Song::Source_Collection &&
|
||||
!task->options.scale_output_image_ &&
|
||||
!task->options.pad_output_image_) {
|
||||
task->song.InitArtManual();
|
||||
if (task->art_manual != task->song.art_manual()) {
|
||||
task->art_manual = task->song.art_manual();
|
||||
task->art_updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
AlbumCoverLoaderResult::Type type(AlbumCoverLoaderResult::Type_None);
|
||||
QUrl cover_url;
|
||||
|
||||
switch (task.state) {
|
||||
case State_TryingAuto: cover_url = task.art_automatic; break;
|
||||
case State_TryingManual: cover_url = task.art_manual; break;
|
||||
switch (task->state) {
|
||||
case State_None:
|
||||
case State_Automatic:
|
||||
type = AlbumCoverLoaderResult::Type_Automatic;
|
||||
cover_url = task->art_automatic;
|
||||
break;
|
||||
case State_Manual:
|
||||
type = AlbumCoverLoaderResult::Type_Manual;
|
||||
cover_url = task->art_manual;
|
||||
break;
|
||||
}
|
||||
task->type = type;
|
||||
|
||||
if (cover_url.path() == Song::kManuallyUnsetCover)
|
||||
return TryLoadResult(false, true, QUrl(), task.options.default_output_image_);
|
||||
|
||||
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, QUrl(), ScaleAndPad(task.options, taglib_image));
|
||||
}
|
||||
|
||||
if (cover_url.path().isEmpty()) {
|
||||
return TryLoadResult(false, false, cover_url, task.options.default_output_image_);
|
||||
}
|
||||
else {
|
||||
if (cover_url.isLocalFile()) {
|
||||
if (!cover_url.isEmpty() && !cover_url.path().isEmpty()) {
|
||||
if (cover_url.path() == Song::kManuallyUnsetCover) {
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_ManuallyUnset, QUrl(), task->options.default_output_image_);
|
||||
}
|
||||
else if (cover_url.path() == Song::kEmbeddedCover && task->song_url.isLocalFile()) {
|
||||
const QImage taglib_image = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task->song_url.toLocalFile());
|
||||
if (!taglib_image.isNull()) {
|
||||
return TryLoadResult(false, true, AlbumCoverLoaderResult::Type_Embedded, QUrl(), ScaleAndPad(task->options, taglib_image).first);
|
||||
}
|
||||
}
|
||||
else if (cover_url.isLocalFile()) {
|
||||
QImage image(cover_url.toLocalFile());
|
||||
return TryLoadResult(false, !image.isNull(), cover_url, image.isNull() ? task.options.default_output_image_ : image);
|
||||
return TryLoadResult(false, !image.isNull(), type, 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);
|
||||
return TryLoadResult(false, !image.isNull(), type, cover_url, image.isNull() ? task->options.default_output_image_ : image);
|
||||
}
|
||||
else if (network_->supportedSchemes().contains(cover_url.scheme())) { // Remote URL
|
||||
QNetworkReply *reply = network_->get(QNetworkRequest(cover_url));
|
||||
QNetworkRequest request(cover_url);
|
||||
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
QNetworkReply *reply = network_->get(request);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, QUrl)), reply, cover_url);
|
||||
|
||||
remote_tasks_.insert(reply, task);
|
||||
return TryLoadResult(true, false, cover_url, QImage());
|
||||
remote_tasks_.insert(reply, *task);
|
||||
return TryLoadResult(true, false, type, cover_url, QImage());
|
||||
}
|
||||
}
|
||||
|
||||
return TryLoadResult(false, false, cover_url, task.options.default_output_image_);
|
||||
return TryLoadResult(false, false, AlbumCoverLoaderResult::Type_None, cover_url, task->options.default_output_image_);
|
||||
|
||||
}
|
||||
|
||||
@@ -367,6 +384,7 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply, const QUrl &cov
|
||||
return; // Give up.
|
||||
}
|
||||
QNetworkRequest request = reply->request();
|
||||
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
request.setUrl(redirect.toUrl());
|
||||
QNetworkReply *redirected_reply = network_->get(request);
|
||||
NewClosure(redirected_reply, SIGNAL(finished()), this, SLOT(RemoteFetchFinished(QNetworkReply*, QUrl)), redirected_reply, redirect.toUrl());
|
||||
@@ -379,41 +397,67 @@ void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply *reply, const QUrl &cov
|
||||
// Try to load the image
|
||||
QImage image;
|
||||
if (image.load(reply, 0)) {
|
||||
QImage scaled = ScaleAndPad(task.options, image);
|
||||
emit ImageLoaded(task.id, cover_url, scaled);
|
||||
emit ImageLoaded(task.id, cover_url, scaled, image);
|
||||
QPair<QImage, QImage> images = ScaleAndPad(task.options, image);
|
||||
emit AlbumCoverLoaded(task.id, AlbumCoverLoaderResult(task.type, cover_url, image, images.first, images.second, task.art_updated));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Unable to load album cover image" << cover_url;
|
||||
}
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Unable to get album cover" << cover_url << reply->error() << reply->errorString();
|
||||
}
|
||||
|
||||
NextState(&task);
|
||||
|
||||
}
|
||||
|
||||
QImage AlbumCoverLoader::ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image) {
|
||||
QPair<QImage, QImage> AlbumCoverLoader::ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image) {
|
||||
|
||||
if (image.isNull()) return image;
|
||||
if (image.isNull()) return qMakePair(image, image);
|
||||
|
||||
// Scale the image down
|
||||
QImage copy;
|
||||
QImage image_scaled;
|
||||
if (options.scale_output_image_) {
|
||||
copy = image.scaled(QSize(options.desired_height_, options.desired_height_), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
image_scaled = image.scaled(QSize(options.desired_height_, options.desired_height_), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
else {
|
||||
copy = image;
|
||||
image_scaled = image;
|
||||
}
|
||||
|
||||
if (!options.pad_output_image_) return copy;
|
||||
// Pad the image to height x height
|
||||
if (options.pad_output_image_) {
|
||||
QImage image_padded(options.desired_height_, options.desired_height_, QImage::Format_ARGB32);
|
||||
image_padded.fill(0);
|
||||
|
||||
// Pad the image to height_ x height_
|
||||
QImage padded_image(options.desired_height_, options.desired_height_, QImage::Format_ARGB32);
|
||||
padded_image.fill(0);
|
||||
QPainter p(&image_padded);
|
||||
p.drawImage((options.desired_height_ - image_scaled.width()) / 2, (options.desired_height_ - image_scaled.height()) / 2, image_scaled);
|
||||
p.end();
|
||||
|
||||
QPainter p(&padded_image);
|
||||
p.drawImage((options.desired_height_ - copy.width()) / 2, (options.desired_height_ - copy.height()) / 2, copy);
|
||||
p.end();
|
||||
image_scaled = image_padded;
|
||||
}
|
||||
|
||||
return padded_image;
|
||||
// Create thumbnail
|
||||
QImage image_thumbnail;
|
||||
if (options.create_thumbnail_) {
|
||||
if (options.pad_thumbnail_image_) {
|
||||
image_thumbnail = image.scaled(options.thumbnail_size_, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QImage image_padded(options.thumbnail_size_, QImage::Format_ARGB32_Premultiplied);
|
||||
image_padded.fill(0);
|
||||
|
||||
QPainter p(&image_padded);
|
||||
p.drawImage((image_padded.width() - image_thumbnail.width()) / 2, (image_padded.height() - image_thumbnail.height()) / 2, image_thumbnail);
|
||||
p.end();
|
||||
|
||||
image_thumbnail = image_padded;
|
||||
}
|
||||
else {
|
||||
image_thumbnail = image.scaledToHeight(options.thumbnail_size_.height(), Qt::SmoothTransformation);
|
||||
}
|
||||
}
|
||||
|
||||
return qMakePair(image_scaled, image_thumbnail);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QMap>
|
||||
#include <QQueue>
|
||||
@@ -37,6 +38,7 @@
|
||||
#include "core/song.h"
|
||||
#include "settings/collectionsettingspage.h"
|
||||
#include "albumcoverloaderoptions.h"
|
||||
#include "albumcoverloaderresult.h"
|
||||
|
||||
class QThread;
|
||||
class QNetworkReply;
|
||||
@@ -48,29 +50,36 @@ class AlbumCoverLoader : public QObject {
|
||||
public:
|
||||
explicit AlbumCoverLoader(QObject *parent = nullptr);
|
||||
|
||||
enum State {
|
||||
State_None,
|
||||
State_Manual,
|
||||
State_Automatic,
|
||||
};
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
void ExitAsync();
|
||||
void Stop() { stop_requested_ = true; }
|
||||
|
||||
static QString ImageCacheDir(const Song::Source source);
|
||||
QString CreateCoverFilename(const QString &artist, const QString &album);
|
||||
static QString AlbumCoverFilename(QString artist, QString album);
|
||||
|
||||
QString CoverFilenameFromSource(const Song::Source source, const QUrl &cover_url, const QString &artist, const QString &album, const QString &album_id);
|
||||
QString CoverFilenameFromVariable(const QString &artist, const QString &album);
|
||||
QString CoverFilePath(const Song &song, const QString &album_dir, const QUrl &cover_url);
|
||||
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 QUrl &art_automatic, const QUrl &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 QUrl &song_url = QUrl(), const Song song = Song(), const QImage &embedded_image = QImage());
|
||||
|
||||
void CancelTask(const quint64 id);
|
||||
void CancelTasks(const QSet<quint64> &ids);
|
||||
|
||||
static QPixmap TryLoadPixmap(const QUrl &automatic, const QUrl &manual, const QUrl &url = QUrl());
|
||||
static QImage ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image);
|
||||
static QPair<QImage, QImage> ScaleAndPad(const AlbumCoverLoaderOptions &options, const QImage &image);
|
||||
|
||||
signals:
|
||||
void ExitFinished();
|
||||
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);
|
||||
void AlbumCoverLoaded(quint64 id, AlbumCoverLoaderResult result);
|
||||
|
||||
protected slots:
|
||||
void Exit();
|
||||
@@ -78,38 +87,38 @@ class AlbumCoverLoader : public QObject {
|
||||
void RemoteFetchFinished(QNetworkReply *reply, const QUrl &cover_url);
|
||||
|
||||
protected:
|
||||
enum State {
|
||||
State_TryingManual,
|
||||
State_TryingAuto,
|
||||
};
|
||||
|
||||
struct Task {
|
||||
explicit Task() : redirects(0) {}
|
||||
explicit Task() : id(0), state(State_None), type(AlbumCoverLoaderResult::Type_None), art_updated(false), redirects(0) {}
|
||||
|
||||
AlbumCoverLoaderOptions options;
|
||||
|
||||
quint64 id;
|
||||
QUrl art_automatic;
|
||||
QUrl art_manual;
|
||||
QString song_filename;
|
||||
QUrl art_automatic;
|
||||
QUrl song_url;
|
||||
Song song;
|
||||
QImage embedded_image;
|
||||
State state;
|
||||
AlbumCoverLoaderResult::Type type;
|
||||
bool art_updated;
|
||||
int redirects;
|
||||
};
|
||||
|
||||
struct TryLoadResult {
|
||||
explicit TryLoadResult(bool async, bool success, const QUrl &_cover_url, const QImage &_image) : started_async(async), loaded_success(success), cover_url(_cover_url), image(_image) {}
|
||||
explicit TryLoadResult(const bool _started_async = false, const bool _loaded_success = false, const AlbumCoverLoaderResult::Type _type = AlbumCoverLoaderResult::Type_None, const QUrl &_cover_url = QUrl(), const QImage &_image = QImage()) : started_async(_started_async), loaded_success(_loaded_success), type(_type), cover_url(_cover_url), image(_image) {}
|
||||
|
||||
bool started_async;
|
||||
bool loaded_success;
|
||||
|
||||
AlbumCoverLoaderResult::Type type;
|
||||
QUrl cover_url;
|
||||
QImage image;
|
||||
};
|
||||
|
||||
void ProcessTask(Task *task);
|
||||
void NextState(Task *task);
|
||||
TryLoadResult TryLoadImage(const Task &task);
|
||||
TryLoadResult TryLoadImage(Task *task);
|
||||
|
||||
bool stop_requested_;
|
||||
|
||||
|
||||
@@ -24,17 +24,24 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QSize>
|
||||
|
||||
struct AlbumCoverLoaderOptions {
|
||||
explicit AlbumCoverLoaderOptions()
|
||||
: desired_height_(120),
|
||||
scale_output_image_(true),
|
||||
pad_output_image_(true) {}
|
||||
pad_output_image_(true),
|
||||
create_thumbnail_(false),
|
||||
pad_thumbnail_image_(false) {}
|
||||
|
||||
int desired_height_;
|
||||
QSize thumbnail_size_;
|
||||
bool scale_output_image_;
|
||||
bool pad_output_image_;
|
||||
bool create_thumbnail_;
|
||||
bool pad_thumbnail_image_;
|
||||
QImage default_output_image_;
|
||||
QImage default_thumbnail_image_;
|
||||
};
|
||||
|
||||
#endif // ALBUMCOVERLOADEROPTIONS_H
|
||||
|
||||
52
src/covermanager/albumcoverloaderresult.h
Normal file
52
src/covermanager/albumcoverloaderresult.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef ALBUMCOVERLOADERRESULT_H
|
||||
#define ALBUMCOVERLOADERRESULT_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QUrl>
|
||||
|
||||
struct AlbumCoverLoaderResult {
|
||||
|
||||
enum Type {
|
||||
Type_None,
|
||||
Type_ManuallyUnset,
|
||||
Type_Embedded,
|
||||
Type_Automatic,
|
||||
Type_Manual,
|
||||
Type_Remote,
|
||||
};
|
||||
|
||||
explicit AlbumCoverLoaderResult(const Type _type = Type_None, const QUrl &_cover_url = QUrl(), const QImage &_image_original = QImage(), const QImage &_image_scaled = QImage(), const QImage &_image_thumbnail = QImage(), const bool _updated = false) : type(_type), cover_url(_cover_url), image_original(_image_original), image_scaled(_image_scaled), image_thumbnail(_image_thumbnail), updated(_updated) {}
|
||||
|
||||
Type type;
|
||||
QUrl cover_url;
|
||||
QImage image_original;
|
||||
QImage image_scaled;
|
||||
QImage image_thumbnail;
|
||||
bool updated;
|
||||
|
||||
QUrl temp_cover_url;
|
||||
|
||||
};
|
||||
|
||||
#endif // ALBUMCOVERLOADERRESULT_H
|
||||
@@ -77,6 +77,8 @@
|
||||
#include "albumcoverexporter.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "albumcoverloader.h"
|
||||
#include "albumcoverloaderoptions.h"
|
||||
#include "albumcoverloaderresult.h"
|
||||
#include "albumcovermanagerlist.h"
|
||||
#include "coversearchstatistics.h"
|
||||
#include "coversearchstatisticsdialog.h"
|
||||
@@ -216,7 +218,7 @@ void AlbumCoverManager::Init() {
|
||||
ui_->splitter->setSizes(QList<int>() << 200 << width() - 200);
|
||||
}
|
||||
|
||||
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(CoverImageLoaded(quint64, QUrl, QImage)));
|
||||
connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
|
||||
|
||||
cover_searcher_->Init(cover_fetcher_);
|
||||
|
||||
@@ -392,7 +394,7 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) {
|
||||
}
|
||||
|
||||
if (!info.art_automatic.isEmpty() || !info.art_manual.isEmpty()) {
|
||||
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, info.art_automatic, info.art_manual, info.first_url.toLocalFile());
|
||||
quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, info.art_automatic, info.art_manual, info.first_url);
|
||||
item->setData(Role_PathAutomatic, info.art_automatic);
|
||||
item->setData(Role_PathManual, info.art_manual);
|
||||
cover_loading_tasks_[id] = item;
|
||||
@@ -403,17 +405,15 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) {
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverManager::CoverImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) {
|
||||
|
||||
Q_UNUSED(cover_url);
|
||||
void AlbumCoverManager::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
|
||||
|
||||
if (!cover_loading_tasks_.contains(id)) return;
|
||||
|
||||
QListWidgetItem *item = cover_loading_tasks_.take(id);
|
||||
|
||||
if (image.isNull()) return;
|
||||
if (result.image_scaled.isNull()) return;
|
||||
|
||||
item->setIcon(QPixmap::fromImage(image));
|
||||
item->setIcon(QPixmap::fromImage(result.image_scaled));
|
||||
UpdateFilter();
|
||||
|
||||
}
|
||||
@@ -488,7 +488,7 @@ void AlbumCoverManager::FetchAlbumCovers() {
|
||||
if (item->isHidden()) continue;
|
||||
if (ItemHasCover(*item)) continue;
|
||||
|
||||
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), true);
|
||||
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), QString(), true);
|
||||
cover_fetching_tasks_[id] = item;
|
||||
jobs_++;
|
||||
}
|
||||
@@ -623,7 +623,7 @@ void AlbumCoverManager::ShowCover() {
|
||||
void AlbumCoverManager::FetchSingleCover() {
|
||||
|
||||
for (QListWidgetItem *item : context_menu_items_) {
|
||||
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), false);
|
||||
quint64 id = cover_fetcher_->FetchAlbumCover(EffectiveAlbumArtistName(*item), item->data(Role_AlbumName).toString(), QString(), false);
|
||||
cover_fetching_tasks_[id] = item;
|
||||
jobs_++;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
#include "core/song.h"
|
||||
#include "albumcoverloaderoptions.h"
|
||||
#include "albumcoverloaderresult.h"
|
||||
#include "coversearchstatistics.h"
|
||||
|
||||
class QWidget;
|
||||
@@ -132,7 +133,7 @@ class AlbumCoverManager : public QMainWindow {
|
||||
|
||||
private slots:
|
||||
void ArtistChanged(QListWidgetItem *current);
|
||||
void CoverImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image);
|
||||
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
|
||||
void UpdateFilter();
|
||||
void FetchAlbumCovers();
|
||||
void ExportCovers();
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/logging.h"
|
||||
#include "widgets/busyindicator.h"
|
||||
#include "widgets/forcescrollperpixel.h"
|
||||
#include "widgets/groupediconview.h"
|
||||
@@ -55,6 +56,7 @@
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "albumcoverloader.h"
|
||||
#include "albumcoverloaderoptions.h"
|
||||
#include "albumcoverloaderresult.h"
|
||||
#include "ui_albumcoversearcher.h"
|
||||
|
||||
const int SizeOverlayDelegate::kMargin = 4;
|
||||
@@ -129,8 +131,11 @@ AlbumCoverSearcher::AlbumCoverSearcher(const QIcon &no_cover_icon, Application *
|
||||
|
||||
options_.scale_output_image_ = false;
|
||||
options_.pad_output_image_ = false;
|
||||
options_.create_thumbnail_ = true;
|
||||
options_.pad_thumbnail_image_ = true;
|
||||
options_.thumbnail_size_ = ui_->covers->iconSize();
|
||||
|
||||
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(ImageLoaded(quint64, QUrl, QImage)));
|
||||
connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
|
||||
|
||||
connect(ui_->search, SIGNAL(clicked()), SLOT(Search()));
|
||||
connect(ui_->covers, SIGNAL(doubleClicked(QModelIndex)), SLOT(CoverDoubleClicked(QModelIndex)));
|
||||
@@ -235,37 +240,25 @@ void AlbumCoverSearcher::SearchFinished(const quint64 id, const CoverSearchResul
|
||||
|
||||
}
|
||||
|
||||
void AlbumCoverSearcher::ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image) {
|
||||
|
||||
Q_UNUSED(cover_url);
|
||||
void AlbumCoverSearcher::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
|
||||
|
||||
if (!cover_loading_tasks_.contains(id)) return;
|
||||
QStandardItem *item = cover_loading_tasks_.take(id);
|
||||
|
||||
if (cover_loading_tasks_.isEmpty()) ui_->busy->hide();
|
||||
|
||||
if (image.isNull()) {
|
||||
if (result.image_original.isNull()) {
|
||||
model_->removeRow(item->row());
|
||||
return;
|
||||
}
|
||||
|
||||
QIcon icon(QPixmap::fromImage(image));
|
||||
|
||||
// Create a pixmap that's padded and exactly the right size for the icon.
|
||||
QImage scaled_image(image.scaled(ui_->covers->iconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
|
||||
QImage padded_image(ui_->covers->iconSize(), QImage::Format_ARGB32_Premultiplied);
|
||||
padded_image.fill(0);
|
||||
|
||||
QPainter p(&padded_image);
|
||||
p.drawImage((padded_image.width() - scaled_image.width()) / 2, (padded_image.height() - scaled_image.height()) / 2, scaled_image);
|
||||
p.end();
|
||||
|
||||
icon.addPixmap(QPixmap::fromImage(padded_image));
|
||||
QIcon icon;
|
||||
icon.addPixmap(QPixmap::fromImage(result.image_original));
|
||||
icon.addPixmap(QPixmap::fromImage(result.image_thumbnail));
|
||||
|
||||
item->setData(true, Role_ImageFetchFinished);
|
||||
item->setData(image.width() * image.height(), Role_ImageDimensions);
|
||||
item->setData(image.size(), Role_ImageSize);
|
||||
item->setData(result.image_original.width() * result.image_original.height(), Role_ImageDimensions);
|
||||
item->setData(result.image_original.size(), Role_ImageSize);
|
||||
item->setIcon(icon);
|
||||
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "albumcoverloaderoptions.h"
|
||||
#include "albumcoverloaderresult.h"
|
||||
|
||||
class QWidget;
|
||||
class QStandardItem;
|
||||
@@ -88,7 +89,7 @@ class AlbumCoverSearcher : public QDialog {
|
||||
private slots:
|
||||
void Search();
|
||||
void SearchFinished(const quint64 id, const CoverSearchResults &results);
|
||||
void ImageLoaded(const quint64 id, const QUrl &cover_url, const QImage &image);
|
||||
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
|
||||
|
||||
void CoverDoubleClicked(const QModelIndex &index);
|
||||
|
||||
|
||||
@@ -26,5 +26,5 @@
|
||||
#include "core/application.h"
|
||||
#include "coverprovider.h"
|
||||
|
||||
CoverProvider::CoverProvider(const QString &name, const float &quality, const bool &fetchall, Application *app, QObject *parent)
|
||||
: QObject(parent), app_(app), name_(name), quality_(quality), fetchall_(fetchall) {}
|
||||
CoverProvider::CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent)
|
||||
: QObject(parent), app_(app), name_(name), quality_(quality), fetchall_(fetchall), allow_missing_album_(allow_missing_album) {}
|
||||
|
||||
@@ -37,17 +37,18 @@ class CoverProvider : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CoverProvider(const QString &name, const float &quality, const bool &fetchall, Application *app, QObject *parent);
|
||||
explicit CoverProvider(const QString &name, const float quality, const bool fetchall, const bool allow_missing_album, Application *app, QObject *parent);
|
||||
|
||||
// A name (very short description) of this provider, like "last.fm".
|
||||
QString name() const { return name_; }
|
||||
bool quality() const { return quality_; }
|
||||
bool fetchall() const { return fetchall_; }
|
||||
bool allow_missing_album() const { return allow_missing_album_; }
|
||||
|
||||
// Starts searching for covers matching the given query text.
|
||||
// Returns true if the query has been started, or false if an error occurred.
|
||||
// The provider should remember the ID and emit it along with the result when it finishes.
|
||||
virtual bool StartSearch(const QString &artist, const QString &album, int id) = 0;
|
||||
virtual bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) = 0;
|
||||
|
||||
virtual void CancelSearch(int id) { Q_UNUSED(id); }
|
||||
|
||||
@@ -59,6 +60,7 @@ class CoverProvider : public QObject {
|
||||
QString name_;
|
||||
float quality_;
|
||||
bool fetchall_;
|
||||
bool allow_missing_album_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2019-2020, 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
|
||||
@@ -32,6 +33,7 @@
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "albumcoverloader.h"
|
||||
#include "albumcoverloaderoptions.h"
|
||||
#include "albumcoverloaderresult.h"
|
||||
#include "currentalbumcoverloader.h"
|
||||
|
||||
CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(Application *app, QObject *parent)
|
||||
@@ -43,56 +45,73 @@ CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(Application *app, QObject *pare
|
||||
|
||||
options_.scale_output_image_ = false;
|
||||
options_.pad_output_image_ = false;
|
||||
options_.create_thumbnail_ = true;
|
||||
options_.thumbnail_size_ = QSize(120, 120);
|
||||
options_.default_output_image_ = QImage(":/pictures/cdcase.png");
|
||||
options_.default_thumbnail_image_ = options_.default_output_image_.scaledToHeight(120, Qt::SmoothTransformation);
|
||||
|
||||
connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64, QUrl, QImage)), SLOT(TempAlbumCoverLoaded(quint64, QUrl, QImage)));
|
||||
connect(app_->album_cover_loader(), SIGNAL(AlbumCoverLoaded(quint64, AlbumCoverLoaderResult)), SLOT(TempAlbumCoverLoaded(quint64, AlbumCoverLoaderResult)));
|
||||
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(LoadAlbumCover(Song)));
|
||||
|
||||
}
|
||||
|
||||
CurrentAlbumCoverLoader::~CurrentAlbumCoverLoader() {
|
||||
|
||||
if (temp_cover_) temp_cover_->remove();
|
||||
if (temp_cover_thumbnail_) temp_cover_thumbnail_->remove();
|
||||
|
||||
}
|
||||
|
||||
void CurrentAlbumCoverLoader::LoadAlbumCover(const Song &song) {
|
||||
|
||||
last_song_ = song;
|
||||
id_ = app_->album_cover_loader()->LoadImageAsync(options_, last_song_);
|
||||
|
||||
}
|
||||
|
||||
void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, const QUrl &remote_url, const QImage &image) {
|
||||
|
||||
Q_UNUSED(remote_url);
|
||||
void CurrentAlbumCoverLoader::TempAlbumCoverLoaded(const quint64 id, AlbumCoverLoaderResult result) {
|
||||
|
||||
if (id != id_) return;
|
||||
id_ = 0;
|
||||
|
||||
QUrl cover_url;
|
||||
QUrl thumbnail_url;
|
||||
QImage thumbnail;
|
||||
|
||||
if (!image.isNull()) {
|
||||
|
||||
QString filename;
|
||||
|
||||
if (!result.image_scaled.isNull()) {
|
||||
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_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_cover_thumbnail_->fileName(), "JPEG");
|
||||
|
||||
cover_url = QUrl::fromLocalFile(temp_cover_->fileName());
|
||||
thumbnail_url = QUrl::fromLocalFile(temp_cover_thumbnail_->fileName());
|
||||
if (temp_cover_->open()) {
|
||||
if (result.image_scaled.save(temp_cover_->fileName(), "JPEG")) {
|
||||
result.temp_cover_url = QUrl::fromLocalFile(temp_cover_->fileName());
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Unable to save cover image to" << temp_cover_->fileName();
|
||||
}
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Unable to open" << temp_cover_->fileName();
|
||||
}
|
||||
}
|
||||
|
||||
emit AlbumCoverLoaded(last_song_, cover_url, image);
|
||||
emit ThumbnailLoaded(last_song_, thumbnail_url, thumbnail);
|
||||
QUrl thumbnail_url;
|
||||
if (!result.image_thumbnail.isNull()) {
|
||||
temp_cover_thumbnail_.reset(new QTemporaryFile(temp_file_pattern_));
|
||||
temp_cover_thumbnail_->setAutoRemove(true);
|
||||
if (temp_cover_thumbnail_->open()) {
|
||||
if (result.image_thumbnail.save(temp_cover_thumbnail_->fileName(), "JPEG")) {
|
||||
thumbnail_url = QUrl::fromLocalFile(temp_cover_thumbnail_->fileName());
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Unable to save cover thumbnail image to" << temp_cover_thumbnail_->fileName();
|
||||
}
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Unable to open" << temp_cover_thumbnail_->fileName();
|
||||
}
|
||||
}
|
||||
|
||||
if (result.updated) {
|
||||
last_song_.set_art_manual(result.cover_url);
|
||||
}
|
||||
|
||||
emit AlbumCoverLoaded(last_song_, result);
|
||||
emit ThumbnailLoaded(last_song_, thumbnail_url, result.image_thumbnail);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2019-2020, 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
|
||||
@@ -33,6 +34,7 @@
|
||||
|
||||
#include "core/song.h"
|
||||
#include "albumcoverloaderoptions.h"
|
||||
#include "albumcoverloaderresult.h"
|
||||
|
||||
class Application;
|
||||
|
||||
@@ -50,11 +52,11 @@ class CurrentAlbumCoverLoader : public QObject {
|
||||
void LoadAlbumCover(const Song &song);
|
||||
|
||||
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);
|
||||
void AlbumCoverLoaded(Song song, AlbumCoverLoaderResult result);
|
||||
void ThumbnailLoaded(Song song, QUrl thumbnail_uri, QImage image);
|
||||
|
||||
private slots:
|
||||
void TempAlbumCoverLoaded(const quint64 id, const QUrl &remote_url, const QImage &image);
|
||||
void TempAlbumCoverLoaded(const quint64 id, AlbumCoverLoaderResult result);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
|
||||
@@ -50,16 +50,27 @@
|
||||
const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com";
|
||||
const int DeezerCoverProvider::kLimit = 10;
|
||||
|
||||
DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", 2.0, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
DeezerCoverProvider::DeezerCoverProvider(Application *app, QObject *parent): CoverProvider("Deezer", 2.0, true, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) {
|
||||
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> Params;
|
||||
typedef QPair<QByteArray, QByteArray> EncodedParam;
|
||||
|
||||
QUrl url(kApiUrl);
|
||||
QString search;
|
||||
if (album.isEmpty()) {
|
||||
url.setPath("/search/track");
|
||||
search = artist + " " + title;
|
||||
}
|
||||
else {
|
||||
url.setPath("/search/album");
|
||||
search = artist + " " + album;
|
||||
}
|
||||
|
||||
const Params params = Params() << Param("output", "json")
|
||||
<< Param("q", QString(artist + " " + album))
|
||||
<< Param("q", search)
|
||||
<< Param("limit", QString::number(kLimit));
|
||||
|
||||
QUrlQuery url_query;
|
||||
@@ -68,7 +79,6 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
url_query.addQueryItem(encoded_param.first, encoded_param.second);
|
||||
}
|
||||
|
||||
QUrl url(kApiUrl + QString("/search/album"));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
@@ -220,19 +230,27 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
for (const QJsonValue &value : json_data) {
|
||||
|
||||
if (!value.isObject()) {
|
||||
Error("Invalid Json reply, data is not an object.", value);
|
||||
Error("Invalid Json reply, data in array is not a object.", value);
|
||||
continue;
|
||||
}
|
||||
QJsonObject json_obj = value.toObject();
|
||||
QJsonObject json_album = json_obj;
|
||||
if (json_obj.contains("album") && json_obj["album"].isObject()) { // Song search, so extract the album.
|
||||
json_album = json_obj["album"].toObject();
|
||||
}
|
||||
|
||||
if (!json_obj.contains("id") || !json_obj.contains("type")) {
|
||||
Error("Invalid Json reply, item is missing ID or type.", json_obj);
|
||||
if (!json_obj.contains("id") || !json_album.contains("id")) {
|
||||
Error("Invalid Json reply, object is missing ID.", json_obj);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString type = json_obj["type"].toString();
|
||||
if (!json_album.contains("type")) {
|
||||
Error("Invalid Json reply, album object is missing type.", json_album);
|
||||
continue;
|
||||
}
|
||||
QString type = json_album["type"].toString();
|
||||
if (type != "album") {
|
||||
Error("Invalid Json reply, incorrect type returned", json_obj);
|
||||
Error("Invalid Json reply, incorrect type returned", json_album);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -242,7 +260,7 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
}
|
||||
QJsonValue json_value_artist = json_obj["artist"];
|
||||
if (!json_value_artist.isObject()) {
|
||||
Error("Invalid Json reply, item artist is not a object.", json_value_artist);
|
||||
Error("Invalid Json reply, artist is not a object.", json_value_artist);
|
||||
continue;
|
||||
}
|
||||
QJsonObject json_artist = json_value_artist.toObject();
|
||||
@@ -253,27 +271,27 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
}
|
||||
QString artist = json_artist["name"].toString();
|
||||
|
||||
if (!json_obj.contains("title")) {
|
||||
Error("Invalid Json reply, data missing title.", json_obj);
|
||||
if (!json_album.contains("title")) {
|
||||
Error("Invalid Json reply, data missing title.", json_album);
|
||||
continue;
|
||||
}
|
||||
QString album = json_obj["title"].toString();
|
||||
QString album = json_album["title"].toString();
|
||||
|
||||
QString cover;
|
||||
if (json_obj.contains("cover_xl")) {
|
||||
cover = json_obj["cover_xl"].toString();
|
||||
if (json_album.contains("cover_xl")) {
|
||||
cover = json_album["cover_xl"].toString();
|
||||
}
|
||||
else if (json_obj.contains("cover_big")) {
|
||||
cover = json_obj["cover_big"].toString();
|
||||
else if (json_album.contains("cover_big")) {
|
||||
cover = json_album["cover_big"].toString();
|
||||
}
|
||||
else if (json_obj.contains("cover_medium")) {
|
||||
cover = json_obj["cover_medium"].toString();
|
||||
else if (json_album.contains("cover_medium")) {
|
||||
cover = json_album["cover_medium"].toString();
|
||||
}
|
||||
else if (json_obj.contains("cover_small")) {
|
||||
cover = json_obj["cover_small"].toString();
|
||||
else if (json_album.contains("cover_small")) {
|
||||
cover = json_album["cover_small"].toString();
|
||||
}
|
||||
else {
|
||||
Error("Invalid Json reply, data missing cover.", json_obj);
|
||||
Error("Invalid Json reply, album missing cover.", json_album);
|
||||
continue;
|
||||
}
|
||||
QUrl url(cover);
|
||||
|
||||
@@ -40,7 +40,7 @@ class DeezerCoverProvider : public CoverProvider {
|
||||
|
||||
public:
|
||||
explicit DeezerCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
bool StartSearch(const QString &artist, const QString &album, const int id);
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
private slots:
|
||||
|
||||
@@ -59,9 +59,11 @@ const char *DiscogsCoverProvider::kUrlReleases = "https://api.discogs.com/releas
|
||||
const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk=";
|
||||
const char *DiscogsCoverProvider::kSecretKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI=";
|
||||
|
||||
DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", 0.0, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
DiscogsCoverProvider::DiscogsCoverProvider(Application *app, QObject *parent) : CoverProvider("Discogs", 0.0, false, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
bool DiscogsCoverProvider::StartSearch(const QString &artist, const QString &album, const int s_id) {
|
||||
bool DiscogsCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int s_id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
DiscogsCoverSearchContext *s_ctx = new DiscogsCoverSearchContext;
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class DiscogsCoverProvider : public CoverProvider {
|
||||
public:
|
||||
explicit DiscogsCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const int s_id);
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int s_id);
|
||||
|
||||
void CancelSearch(const int id);
|
||||
|
||||
|
||||
@@ -52,9 +52,11 @@ const char *LastFmCoverProvider::kUrl = "https://ws.audioscrobbler.com/2.0/";
|
||||
const char *LastFmCoverProvider::kApiKey = "211990b4c96782c05d1536e7219eb56e";
|
||||
const char *LastFmCoverProvider::kSecret = "80fd738f49596e9709b1bf9319c444a8";
|
||||
|
||||
LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", 1.0, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
LastFmCoverProvider::LastFmCoverProvider(Application *app, QObject *parent) : CoverProvider("last.fm", 1.0, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) {
|
||||
bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QPair<QByteArray, QByteArray> EncodedParam;
|
||||
|
||||
@@ -40,7 +40,7 @@ class LastFmCoverProvider : public CoverProvider {
|
||||
|
||||
public:
|
||||
explicit LastFmCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
bool StartSearch(const QString &artist, const QString &album, const int id);
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
|
||||
private slots:
|
||||
void QueryFinished(QNetworkReply *reply, const int id);
|
||||
|
||||
@@ -47,9 +47,11 @@ const char *MusicbrainzCoverProvider::kReleaseSearchUrl = "https://musicbrainz.o
|
||||
const char *MusicbrainzCoverProvider::kAlbumCoverUrl = "https://coverartarchive.org/release/%1/front";
|
||||
const int MusicbrainzCoverProvider::kLimit = 8;
|
||||
|
||||
MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", 1.5, true, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
MusicbrainzCoverProvider::MusicbrainzCoverProvider(Application *app, QObject *parent): CoverProvider("MusicBrainz", 1.5, true, false, app, parent), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) {
|
||||
bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
QString query = QString("release:\"%1\" AND artist:\"%2\"").arg(album.trimmed().replace('"', "\\\"")).arg(artist.trimmed().replace('"', "\\\""));
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class MusicbrainzCoverProvider : public CoverProvider {
|
||||
public:
|
||||
explicit MusicbrainzCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const int id);
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(const int id);
|
||||
|
||||
private slots:
|
||||
|
||||
@@ -52,13 +52,15 @@ const char *TidalCoverProvider::kResourcesUrl = "https://resources.tidal.com";
|
||||
const int TidalCoverProvider::kLimit = 10;
|
||||
|
||||
TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) :
|
||||
CoverProvider("Tidal", 2.0, true, app, parent),
|
||||
CoverProvider("Tidal", 2.0, true, false, app, parent),
|
||||
service_(app->internet_services()->Service<TidalService>()),
|
||||
network_(new NetworkAccessManager(this)) {
|
||||
|
||||
}
|
||||
|
||||
bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) {
|
||||
bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
if (!service_ || !service_->authenticated()) return false;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class TidalCoverProvider : public CoverProvider {
|
||||
|
||||
public:
|
||||
explicit TidalCoverProvider(Application *app, QObject *parent = nullptr);
|
||||
bool StartSearch(const QString &artist, const QString &album, const int id);
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id);
|
||||
void CancelSearch(int id);
|
||||
|
||||
private slots:
|
||||
|
||||
Reference in New Issue
Block a user