strawberry-audio-player-win.../src/covermanager/albumcoverchoicecontroller.cpp

827 lines
29 KiB
C++

/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019-2023, 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/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QGuiApplication>
#include <QtConcurrentRun>
#include <QFuture>
#include <QFutureWatcher>
#include <QScreen>
#include <QWidget>
#include <QDialog>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMimeData>
#include <QSet>
#include <QList>
#include <QVariant>
#include <QString>
#include <QRegularExpression>
#include <QUrl>
#include <QImage>
#include <QImageWriter>
#include <QPixmap>
#include <QIcon>
#include <QRect>
#include <QFileDialog>
#include <QLabel>
#include <QAction>
#include <QActionGroup>
#include <QMenu>
#include <QSettings>
#include <QtEvents>
#include "utilities/filenameconstants.h"
#include "utilities/strutils.h"
#include "utilities/mimeutils.h"
#include "utilities/coveroptions.h"
#include "utilities/coverutils.h"
#include "utilities/screenutils.h"
#include "core/application.h"
#include "core/song.h"
#include "core/iconloader.h"
#include "core/tagreaderclient.h"
#include "collection/collectionfilteroptions.h"
#include "collection/collectionbackend.h"
#include "settings/coverssettingspage.h"
#include "internet/internetservices.h"
#include "internet/internetservice.h"
#include "albumcoverchoicecontroller.h"
#include "albumcoverfetcher.h"
#include "albumcoverloader.h"
#include "albumcoversearcher.h"
#include "albumcoverimageresult.h"
#include "coverfromurldialog.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)");
const char *AlbumCoverChoiceController::kAllFilesFilter = QT_TR_NOOP("All files (*)");
QSet<QString> *AlbumCoverChoiceController::sImageExtensions = nullptr;
AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent)
: QWidget(parent),
app_(nullptr),
cover_searcher_(nullptr),
cover_fetcher_(nullptr),
save_file_dialog_(nullptr),
cover_from_url_dialog_(nullptr),
cover_from_file_(nullptr),
cover_to_file_(nullptr),
cover_from_url_(nullptr),
search_for_cover_(nullptr),
separator1_(nullptr),
unset_cover_(nullptr),
delete_cover_(nullptr),
clear_cover_(nullptr),
separator2_(nullptr),
show_cover_(nullptr),
search_cover_auto_(nullptr),
save_embedded_cover_override_(false) {
cover_from_file_ = new QAction(IconLoader::Load("document-open"), tr("Load cover from disk..."), this);
cover_to_file_ = new QAction(IconLoader::Load("document-save"), tr("Save cover to disk..."), this);
cover_from_url_ = new QAction(IconLoader::Load("download"), tr("Load cover from URL..."), this);
search_for_cover_ = new QAction(IconLoader::Load("search"), tr("Search for album covers..."), this);
unset_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Unset cover"), this);
delete_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Delete cover"), this);
clear_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Clear cover"), this);
separator1_ = new QAction(this);
separator1_->setSeparator(true);
show_cover_ = new QAction(IconLoader::Load("zoom-in"), tr("Show fullsize..."), this);
search_cover_auto_ = new QAction(tr("Search automatically"), this);
search_cover_auto_->setCheckable(true);
search_cover_auto_->setChecked(false);
separator2_ = new QAction(this);
separator2_->setSeparator(true);
ReloadSettings();
}
AlbumCoverChoiceController::~AlbumCoverChoiceController() = default;
void AlbumCoverChoiceController::Init(Application *app) {
app_ = app;
cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), app->network(), this);
cover_searcher_ = new AlbumCoverSearcher(QIcon(":/pictures/cdcase.png"), app, this);
cover_searcher_->Init(cover_fetcher_);
QObject::connect(cover_fetcher_, &AlbumCoverFetcher::AlbumCoverFetched, this, &AlbumCoverChoiceController::AlbumCoverFetched);
}
void AlbumCoverChoiceController::ReloadSettings() {
QSettings s;
s.beginGroup(CoversSettingsPage::kSettingsGroup);
cover_options_.cover_type = static_cast<CoverOptions::CoverType>(s.value(CoversSettingsPage::kSaveType, static_cast<int>(CoverOptions::CoverType::Cache)).toInt());
cover_options_.cover_filename = static_cast<CoverOptions::CoverFilename>(s.value(CoversSettingsPage::kSaveFilename, static_cast<int>(CoverOptions::CoverFilename::Pattern)).toInt());
cover_options_.cover_pattern = s.value(CoversSettingsPage::kSavePattern, "%albumartist-%album").toString();
cover_options_.cover_overwrite = s.value(CoversSettingsPage::kSaveOverwrite, false).toBool();
cover_options_.cover_lowercase = s.value(CoversSettingsPage::kSaveLowercase, false).toBool();
cover_options_.cover_replace_spaces = s.value(CoversSettingsPage::kSaveReplaceSpaces, false).toBool();
s.endGroup();
cover_types_ = AlbumCoverLoaderOptions::LoadTypes();
}
QList<QAction*> AlbumCoverChoiceController::GetAllActions() {
return QList<QAction*>() << show_cover_
<< cover_to_file_
<< separator1_
<< cover_from_file_
<< cover_from_url_
<< search_for_cover_
<< separator2_
<< unset_cover_
<< clear_cover_
<< delete_cover_;
}
AlbumCoverImageResult AlbumCoverChoiceController::LoadImageFromFile(Song *song) {
if (!song->url().isValid() || !song->url().isLocalFile()) {
return AlbumCoverImageResult();
}
QString cover_file = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
if (cover_file.isEmpty()) return AlbumCoverImageResult();
QFile file(cover_file);
if (!file.open(QIODevice::ReadOnly)) {
qLog(Error) << "Failed to open cover file" << cover_file << "for reading:" << file.errorString();
emit Error(tr("Failed to open cover file %1 for reading: %2").arg(cover_file, file.errorString()));
return AlbumCoverImageResult();
}
AlbumCoverImageResult result;
result.image_data = file.readAll();
file.close();
if (result.image_data.isEmpty()) {
qLog(Error) << "Cover file" << cover_file << "is empty.";
emit Error(tr("Cover file %1 is empty.").arg(cover_file));
return AlbumCoverImageResult();
}
if (result.image.loadFromData(result.image_data)) {
result.cover_url = QUrl::fromLocalFile(cover_file);
result.mime_type = Utilities::MimeTypeFromData(result.image_data);
}
return result;
}
QUrl AlbumCoverChoiceController::LoadCoverFromFile(Song *song) {
if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl();
QString cover_file = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
if (cover_file.isEmpty() || QImage(cover_file).isNull()) return QUrl();
switch (get_save_album_cover_type()) {
case CoverOptions::CoverType::Embedded:
if (song->save_embedded_cover_supported()) {
SaveCoverEmbeddedToCollectionSongs(*song, cover_file);
return QUrl();
}
[[fallthrough]];
case CoverOptions::CoverType::Cache:
case CoverOptions::CoverType::Album:{
const QUrl cover_url = QUrl::fromLocalFile(cover_file);
SaveArtManualToSong(song, cover_url);
return cover_url;
}
}
return QUrl();
}
void AlbumCoverChoiceController::SaveCoverToFileManual(const Song &song, const AlbumCoverImageResult &result) {
QString initial_file_name = "/";
if (!song.effective_albumartist().isEmpty()) {
initial_file_name = initial_file_name + song.effective_albumartist();
}
initial_file_name = initial_file_name + "-" + (song.effective_album().isEmpty() ? tr("unknown") : song.effective_album()) + ".jpg";
initial_file_name = initial_file_name.toLower();
initial_file_name.replace(QRegularExpression("\\s"), "-");
initial_file_name.remove(QRegularExpression(QString(kInvalidFatCharactersRegex), QRegularExpression::CaseInsensitiveOption));
QString save_filename = QFileDialog::getSaveFileName(this, tr("Save album cover"), GetInitialPathForFileDialog(song, initial_file_name), tr(kSaveImageFileFilter) + ";;" + tr(kAllFilesFilter));
if (save_filename.isEmpty()) return;
QFileInfo fileinfo(save_filename);
if (fileinfo.suffix().isEmpty()) {
save_filename.append(".jpg");
fileinfo.setFile(save_filename);
}
if (!QImageWriter::supportedImageFormats().contains(fileinfo.completeSuffix().toUtf8().toLower())) {
save_filename = Utilities::PathWithoutFilenameExtension(save_filename) + ".jpg";
fileinfo.setFile(save_filename);
}
if (result.is_jpeg() && fileinfo.completeSuffix().compare("jpg", Qt::CaseInsensitive) == 0) {
QFile file(save_filename);
if (!file.open(QIODevice::WriteOnly)) {
qLog(Error) << "Failed to open cover file" << save_filename << "for writing:" << file.errorString();
emit Error(tr("Failed to open cover file %1 for writing: %2").arg(save_filename, file.errorString()));
file.close();
return;
}
if (file.write(result.image_data) <= 0) {
qLog(Error) << "Failed writing cover to file" << save_filename << file.errorString();
emit Error(tr("Failed writing cover to file %1: %2").arg(save_filename, file.errorString()));
file.close();
return;
}
file.close();
}
else {
if (!result.image.save(save_filename)) {
qLog(Error) << "Failed writing cover to file" << save_filename;
emit Error(tr("Failed writing cover to file %1.").arg(save_filename));
}
}
}
QString AlbumCoverChoiceController::GetInitialPathForFileDialog(const Song &song, const QString &filename) {
// 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_is_valid()) {
return song.art_automatic().toLocalFile();
}
// If no automatic art, start in the song's folder
if (!song.url().isEmpty() && song.url().isValid() && song.url().isLocalFile() && song.url().toLocalFile().contains('/')) {
return song.url().toLocalFile().section('/', 0, -2) + filename;
}
return QDir::home().absolutePath() + filename;
}
void AlbumCoverChoiceController::LoadCoverFromURL(Song *song) {
if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return;
const AlbumCoverImageResult result = LoadImageFromURL();
if (!result.image.isNull()) {
SaveCoverAutomatic(song, result);
}
}
AlbumCoverImageResult AlbumCoverChoiceController::LoadImageFromURL() {
if (!cover_from_url_dialog_) { cover_from_url_dialog_ = new CoverFromURLDialog(app_->network(), this); }
return cover_from_url_dialog_->Exec();
}
void AlbumCoverChoiceController::SearchForCover(Song *song) {
if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return;
// Get something sensible to stick in the search box
AlbumCoverImageResult result = SearchForImage(song);
if (result.is_valid()) {
SaveCoverAutomatic(song, result);
}
}
AlbumCoverImageResult AlbumCoverChoiceController::SearchForImage(Song *song) {
if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return AlbumCoverImageResult();
QString album = song->effective_album();
album = album.remove(Song::kAlbumRemoveDisc).remove(Song::kAlbumRemoveMisc);
// Get something sensible to stick in the search box
return cover_searcher_->Exec(song->effective_albumartist(), album);
}
void AlbumCoverChoiceController::UnsetCover(Song *song) {
if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return;
UnsetAlbumCoverForSong(song);
}
void AlbumCoverChoiceController::ClearCover(Song *song) {
if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return;
ClearAlbumCoverForSong(song);
}
bool AlbumCoverChoiceController::DeleteCover(Song *song, const bool unset) {
if (!song->url().isValid() || !song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return false;
if (song->art_embedded() && song->save_embedded_cover_supported()) {
SaveCoverEmbeddedToCollectionSongs(*song, AlbumCoverImageResult());
}
bool success = true;
if (song->art_automatic().isValid() && song->art_automatic().isLocalFile()) {
const QString art_automatic = song->art_automatic().toLocalFile();
QFile file(art_automatic);
if (file.exists()) {
if (file.remove()) {
song->clear_art_automatic();
}
else {
success = false;
qLog(Error) << "Failed to delete cover file" << art_automatic << file.errorString();
emit Error(tr("Failed to delete cover file %1: %2").arg(art_automatic, file.errorString()));
}
}
else song->clear_art_automatic();
}
else song->clear_art_automatic();
if (song->art_manual().isValid() && song->art_manual().isLocalFile()) {
const QString art_manual = song->art_manual().toLocalFile();
QFile file(art_manual);
if (file.exists()) {
if (file.remove()) {
song->clear_art_manual();
}
else {
success = false;
qLog(Error) << "Failed to delete cover file" << art_manual << file.errorString();
emit Error(tr("Failed to delete cover file %1: %2").arg(art_manual, file.errorString()));
}
}
else song->clear_art_manual();
}
else song->clear_art_manual();
if (success) {
if (unset) UnsetCover(song);
else ClearCover(song);
}
return success;
}
void AlbumCoverChoiceController::ShowCover(const Song &song, const QImage &image) {
if (!image.isNull()) {
QPixmap pixmap = QPixmap::fromImage(image);
if (!pixmap.isNull()) {
pixmap.setDevicePixelRatio(devicePixelRatioF());
ShowCover(song, pixmap);
return;
}
}
for (const AlbumCoverLoaderOptions::Type type : cover_types_) {
switch (type) {
case AlbumCoverLoaderOptions::Type::Unset: {
if (song.art_unset()) {
return;
}
break;
}
case AlbumCoverLoaderOptions::Type::Manual:{
QPixmap pixmap;
if (song.art_manual_is_valid() && song.art_manual().isLocalFile() && pixmap.load(song.art_manual().toLocalFile())) {
pixmap.setDevicePixelRatio(devicePixelRatioF());
ShowCover(song, pixmap);
return;
}
break;
}
case AlbumCoverLoaderOptions::Type::Embedded:{
if (song.art_embedded() && !song.url().isEmpty() && song.url().isValid() && song.url().isLocalFile()) {
const QImage image_embedded_cover = TagReaderClient::Instance()->LoadEmbeddedArtAsImageBlocking(song.url().toLocalFile());
if (!image_embedded_cover.isNull()) {
QPixmap pixmap = QPixmap::fromImage(image_embedded_cover);
if (!pixmap.isNull()) {
pixmap.setDevicePixelRatio(devicePixelRatioF());
ShowCover(song, pixmap);
return;
}
}
}
break;
}
case AlbumCoverLoaderOptions::Type::Automatic:{
QPixmap pixmap;
if (song.art_automatic_is_valid() && song.art_automatic().isLocalFile() && pixmap.load(song.art_automatic().toLocalFile())) {
pixmap.setDevicePixelRatio(devicePixelRatioF());
ShowCover(song, pixmap);
return;
}
break;
}
}
}
}
void AlbumCoverChoiceController::ShowCover(const Song &song, const QPixmap &pixmap) {
QDialog *dialog = new QDialog(this);
dialog->setAttribute(Qt::WA_DeleteOnClose, true);
// Use Artist - Album as the window title
QString title_text(song.effective_albumartist());
if (!song.effective_album().isEmpty()) title_text += " - " + song.effective_album();
QLabel *label = new QLabel(dialog);
label->setPixmap(pixmap);
// Add (WxHpx) to the title before possibly resizing
title_text += " (" + QString::number(pixmap.width()) + "x" + QString::number(pixmap.height()) + "px)";
// If the cover is larger than the screen, resize the window 85% seems to be enough to account for title bar and taskbar etc.
QScreen *screen = Utilities::GetScreen(this);
QRect screenGeometry = screen->availableGeometry();
int desktop_height = screenGeometry.height();
int desktop_width = screenGeometry.width();
// Resize differently if monitor is in portrait mode
if (desktop_width < desktop_height) {
const int new_width = static_cast<int>(static_cast<double>(desktop_width) * 0.95);
if (new_width < pixmap.width()) {
label->setPixmap(pixmap.scaledToWidth(static_cast<int>(new_width * pixmap.devicePixelRatioF()), Qt::SmoothTransformation));
}
}
else {
const int new_height = static_cast<int>(static_cast<double>(desktop_height) * 0.85);
if (new_height < pixmap.height()) {
label->setPixmap(pixmap.scaledToHeight(static_cast<int>(new_height * pixmap.devicePixelRatioF()), Qt::SmoothTransformation));
}
}
dialog->setWindowTitle(title_text);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
dialog->setFixedSize(label->pixmap(Qt::ReturnByValue).size() / pixmap.devicePixelRatioF());
#else
dialog->setFixedSize(label->pixmap()->size() / pixmap.devicePixelRatioF());
#endif
dialog->show();
}
quint64 AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) {
quint64 id = cover_fetcher_->FetchAlbumCover(song.effective_albumartist(), song.album(), song.title(), true);
cover_fetching_tasks_.insert(id, song);
return id;
}
void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const AlbumCoverImageResult &result, const CoverSearchStatistics &statistics) {
Q_UNUSED(statistics);
Song song;
if (cover_fetching_tasks_.contains(id)) {
song = cover_fetching_tasks_.take(id);
}
if (result.is_valid()) {
SaveCoverAutomatic(&song, result);
}
emit AutomaticCoverSearchDone();
}
void AlbumCoverChoiceController::SaveArtEmbeddedToSong(Song *song, const bool art_embedded) {
if (!song->is_valid()) return;
song->set_art_embedded(art_embedded);
song->set_art_unset(false);
if (song->source() == Song::Source::Collection) {
app_->collection_backend()->UpdateEmbeddedAlbumArtAsync(song->effective_albumartist(), song->album(), art_embedded);
}
if (*song == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(*song);
}
}
void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art_manual) {
if (!song->is_valid()) return;
song->set_art_manual(art_manual);
song->set_art_unset(false);
// Update the backends.
switch (song->source()) {
case Song::Source::Collection:
app_->collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual);
break;
case Song::Source::LocalFile:
case Song::Source::CDDA:
case Song::Source::Device:
case Song::Source::Stream:
case Song::Source::RadioParadise:
case Song::Source::SomaFM:
case Song::Source::Unknown:
break;
case Song::Source::Tidal:
case Song::Source::Qobuz:
case Song::Source::Subsonic:
InternetServicePtr service = app_->internet_services()->ServiceBySource(song->source());
if (!service) break;
if (service->artists_collection_backend()) {
service->artists_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual);
}
if (service->albums_collection_backend()) {
service->albums_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual);
}
if (service->songs_collection_backend()) {
service->songs_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual);
}
break;
}
if (*song == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(*song);
}
}
void AlbumCoverChoiceController::ClearAlbumCoverForSong(Song *song) {
if (!song->is_valid()) return;
song->set_art_unset(false);
song->set_art_embedded(false);
song->clear_art_automatic();
song->clear_art_manual();
if (song->source() == Song::Source::Collection) {
app_->collection_backend()->ClearAlbumArtAsync(song->effective_albumartist(), song->album(), false);
}
if (*song == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(*song);
}
}
void AlbumCoverChoiceController::UnsetAlbumCoverForSong(Song *song) {
if (!song->is_valid()) return;
song->set_art_unset(true);
song->set_art_embedded(false);
song->clear_art_manual();
song->clear_art_automatic();
if (song->source() == Song::Source::Collection) {
app_->collection_backend()->UnsetAlbumArtAsync(song->effective_albumartist(), song->album());
}
if (*song == app_->current_albumcover_loader()->last_song()) {
app_->current_albumcover_loader()->LoadAlbumCover(*song);
}
}
QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, const AlbumCoverImageResult &result, const bool force_overwrite) {
return SaveCoverToFileAutomatic(song->source(),
song->effective_albumartist(),
song->effective_album(),
song->album_id(),
QFileInfo(song->url().toLocalFile()).path(),
result,
force_overwrite);
}
QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source source,
const QString &artist,
const QString &album,
const QString &album_id,
const QString &album_dir,
const AlbumCoverImageResult &result,
const bool force_overwrite) {
QString filepath = CoverUtils::CoverFilePath(cover_options_, source, artist, album, album_id, album_dir, result.cover_url, "jpg");
if (filepath.isEmpty()) return QUrl();
QFile file(filepath);
// Don't overwrite when saving in album dir if the filename is set to pattern unless "force_overwrite" is set.
if (source == Song::Source::Collection && !cover_options_.cover_overwrite && !force_overwrite && get_save_album_cover_type() == CoverOptions::CoverType::Album && cover_options_.cover_filename == CoverOptions::CoverFilename::Pattern && file.exists()) {
while (file.exists()) {
QFileInfo fileinfo(file.fileName());
file.setFileName(fileinfo.path() + "/0" + fileinfo.fileName());
}
filepath = file.fileName();
}
if (!result.image_data.isEmpty() && result.is_jpeg()) {
if (file.open(QIODevice::WriteOnly)) {
if (file.write(result.image_data) > 0) {
return QUrl::fromLocalFile(filepath);
}
else {
qLog(Error) << "Failed to write cover to file" << file.fileName() << file.errorString();
emit Error(tr("Failed to write cover to file %1: %2").arg(file.fileName(), file.errorString()));
}
file.close();
}
else {
qLog(Error) << "Failed to open cover file" << file.fileName() << "for writing:" << file.errorString();
emit Error(tr("Failed to open cover file %1 for writing: %2").arg(file.fileName(), file.errorString()));
}
}
else {
if (result.image.save(filepath, "JPG")) {
return QUrl::fromLocalFile(filepath);
}
}
return QUrl();
}
void AlbumCoverChoiceController::SaveCoverEmbeddedToCollectionSongs(const Song &song, const AlbumCoverImageResult &result) {
SaveCoverEmbeddedToCollectionSongs(song, QString(), result.image_data, result.mime_type);
}
void AlbumCoverChoiceController::SaveCoverEmbeddedToCollectionSongs(const Song &song, const QString &cover_filename, const QByteArray &image_data, const QString &mime_type) {
if (song.source() == Song::Source::Collection) {
SaveCoverEmbeddedToCollectionSongs(song.effective_albumartist(), song.effective_album(), cover_filename, image_data, mime_type);
}
else {
SaveCoverEmbeddedToSong(song, cover_filename, image_data, mime_type);
}
}
void AlbumCoverChoiceController::SaveCoverEmbeddedToCollectionSongs(const QString &effective_albumartist, const QString &effective_album, const QString &cover_filename, const QByteArray &image_data, const QString &mime_type) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QFuture<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), effective_albumartist, effective_album, CollectionFilterOptions());
#else
QFuture<SongList> future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, effective_albumartist, effective_album, CollectionFilterOptions());
#endif
QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [this, watcher, cover_filename, image_data, mime_type]() {
const SongList collection_songs = watcher->result();
watcher->deleteLater();
for (const Song &collection_song : collection_songs) {
SaveCoverEmbeddedToSong(collection_song, cover_filename, image_data, mime_type);
}
});
watcher->setFuture(future);
}
void AlbumCoverChoiceController::SaveCoverEmbeddedToSong(const Song &song, const QString &cover_filename, const QByteArray &image_data, const QString &mime_type) {
QMutexLocker l(&mutex_cover_save_tasks_);
cover_save_tasks_.append(song);
const bool art_embedded = !image_data.isNull();
TagReaderReply *reply = app_->tag_reader_client()->SaveEmbeddedArt(song.url().toLocalFile(), TagReaderClient::SaveCoverOptions(cover_filename, image_data, mime_type));
QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, song, art_embedded]() { SaveEmbeddedCoverFinished(reply, song, art_embedded); });
}
bool AlbumCoverChoiceController::IsKnownImageExtension(const QString &suffix) {
if (!sImageExtensions) {
sImageExtensions = new QSet<QString>();
(*sImageExtensions) << "png" << "jpg" << "jpeg" << "bmp" << "gif" << "xpm" << "pbm" << "pgm" << "ppm" << "xbm";
}
return sImageExtensions->contains(suffix);
}
bool AlbumCoverChoiceController::CanAcceptDrag(const QDragEnterEvent *e) {
for (const QUrl &url : e->mimeData()->urls()) {
const QString suffix = QFileInfo(url.toLocalFile()).suffix().toLower();
if (IsKnownImageExtension(suffix)) return true;
}
return e->mimeData()->hasImage();
}
void AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
for (const QUrl &url : e->mimeData()->urls()) {
const QString filename = url.toLocalFile();
const QString suffix = QFileInfo(filename).suffix().toLower();
if (IsKnownImageExtension(suffix)) {
if (get_save_album_cover_type() == CoverOptions::CoverType::Embedded && song->save_embedded_cover_supported()) {
SaveCoverEmbeddedToCollectionSongs(*song, filename);
}
else {
SaveArtManualToSong(song, url);
}
return;
}
}
if (e->mimeData()->hasImage()) {
QImage image = qvariant_cast<QImage>(e->mimeData()->imageData());
if (!image.isNull()) {
SaveCoverAutomatic(song, AlbumCoverImageResult(image));
}
}
}
QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCoverImageResult &result) {
QUrl cover_url;
switch(get_save_album_cover_type()) {
case CoverOptions::CoverType::Embedded:{
if (song->save_embedded_cover_supported()) {
SaveCoverEmbeddedToCollectionSongs(*song, result);
break;
}
}
[[fallthrough]];
case CoverOptions::CoverType::Cache:
case CoverOptions::CoverType::Album:{
cover_url = SaveCoverToFileAutomatic(song, result);
if (!cover_url.isEmpty()) SaveArtManualToSong(song, cover_url);
break;
}
}
return cover_url;
}
void AlbumCoverChoiceController::SaveEmbeddedCoverFinished(TagReaderReply *reply, Song song, const bool art_embedded) {
if (!cover_save_tasks_.contains(song)) return;
cover_save_tasks_.removeAll(song);
if (reply->is_successful()) {
SaveArtEmbeddedToSong(&song, art_embedded);
}
else {
emit Error(tr("Could not save cover to file %1.").arg(song.url().toLocalFile()));
}
}