1 /*
2  * Strawberry Music Player
3  * This file was part of Clementine.
4  * Copyright 2010, David Sansome <me@davidsansome.com>
5  * Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
6  *
7  * Strawberry is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Strawberry is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with Strawberry.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  */
21 
22 #include "config.h"
23 
24 #include <QtGlobal>
25 #include <QGuiApplication>
26 #include <QtConcurrentRun>
27 #include <QFuture>
28 #include <QFutureWatcher>
29 #include <QScreen>
30 #include <QWindow>
31 #include <QWidget>
32 #include <QDialog>
33 #include <QDir>
34 #include <QFile>
35 #include <QFileInfo>
36 #include <QMimeData>
37 #include <QSet>
38 #include <QList>
39 #include <QVariant>
40 #include <QString>
41 #include <QRegularExpression>
42 #include <QUrl>
43 #include <QImage>
44 #include <QImageWriter>
45 #include <QPixmap>
46 #include <QIcon>
47 #include <QRect>
48 #include <QFileDialog>
49 #include <QLabel>
50 #include <QAction>
51 #include <QActionGroup>
52 #include <QMenu>
53 #include <QSettings>
54 #include <QtEvents>
55 
56 #include "core/utilities.h"
57 #include "core/imageutils.h"
58 #include "core/application.h"
59 #include "core/song.h"
60 #include "core/iconloader.h"
61 
62 #include "collection/collectionbackend.h"
63 #include "settings/collectionsettingspage.h"
64 #include "organize/organizeformat.h"
65 #include "internet/internetservices.h"
66 #include "internet/internetservice.h"
67 #include "albumcoverchoicecontroller.h"
68 #include "albumcoverfetcher.h"
69 #include "albumcoverloader.h"
70 #include "albumcoversearcher.h"
71 #include "albumcoverimageresult.h"
72 #include "coverfromurldialog.h"
73 #include "currentalbumcoverloader.h"
74 
75 const char *AlbumCoverChoiceController::kLoadImageFileFilter = QT_TR_NOOP("Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
76 const char *AlbumCoverChoiceController::kSaveImageFileFilter = QT_TR_NOOP("Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
77 const char *AlbumCoverChoiceController::kAllFilesFilter = QT_TR_NOOP("All files (*)");
78 
79 QSet<QString> *AlbumCoverChoiceController::sImageExtensions = nullptr;
80 
AlbumCoverChoiceController(QWidget * parent)81 AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget *parent)
82     : QWidget(parent),
83       app_(nullptr),
84       cover_searcher_(nullptr),
85       cover_fetcher_(nullptr),
86       save_file_dialog_(nullptr),
87       cover_from_url_dialog_(nullptr),
88       cover_from_file_(nullptr),
89       cover_to_file_(nullptr),
90       cover_from_url_(nullptr),
91       search_for_cover_(nullptr),
92       separator1_(nullptr),
93       unset_cover_(nullptr),
94       delete_cover_(nullptr),
95       clear_cover_(nullptr),
96       separator2_(nullptr),
97       show_cover_(nullptr),
98       search_cover_auto_(nullptr),
99       save_cover_type_(CollectionSettingsPage::SaveCoverType_Cache),
100       save_cover_filename_(CollectionSettingsPage::SaveCoverFilename_Pattern),
101       cover_overwrite_(false),
102       cover_lowercase_(true),
103       cover_replace_spaces_(true),
104       save_embedded_cover_override_(false) {
105 
106   cover_from_file_ = new QAction(IconLoader::Load("document-open"), tr("Load cover from disk..."), this);
107   cover_to_file_ = new QAction(IconLoader::Load("document-save"), tr("Save cover to disk..."), this);
108   cover_from_url_ = new QAction(IconLoader::Load("download"), tr("Load cover from URL..."), this);
109   search_for_cover_ = new QAction(IconLoader::Load("search"), tr("Search for album covers..."), this);
110   unset_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Unset cover"), this);
111   delete_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Delete cover"), this);
112   clear_cover_ = new QAction(IconLoader::Load("list-remove"), tr("Clear cover"), this);
113   separator1_ = new QAction(this);
114   separator1_->setSeparator(true);
115   show_cover_ = new QAction(IconLoader::Load("zoom-in"), tr("Show fullsize..."), this);
116 
117   search_cover_auto_ = new QAction(tr("Search automatically"), this);
118   search_cover_auto_->setCheckable(true);
119   search_cover_auto_->setChecked(false);
120 
121   separator2_ = new QAction(this);
122   separator2_->setSeparator(true);
123 
124   ReloadSettings();
125 
126 }
127 
128 AlbumCoverChoiceController::~AlbumCoverChoiceController() = default;
129 
Init(Application * app)130 void AlbumCoverChoiceController::Init(Application *app) {
131 
132   app_ = app;
133 
134   cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), this);
135   cover_searcher_ = new AlbumCoverSearcher(QIcon(":/pictures/cdcase.png"), app, this);
136   cover_searcher_->Init(cover_fetcher_);
137 
138   QObject::connect(cover_fetcher_, &AlbumCoverFetcher::AlbumCoverFetched, this, &AlbumCoverChoiceController::AlbumCoverFetched);
139   QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::SaveEmbeddedCoverAsyncFinished, this, &AlbumCoverChoiceController::SaveEmbeddedCoverAsyncFinished);
140 
141 }
142 
ReloadSettings()143 void AlbumCoverChoiceController::ReloadSettings() {
144 
145   QSettings s;
146   s.beginGroup(CollectionSettingsPage::kSettingsGroup);
147   save_cover_type_ = CollectionSettingsPage::SaveCoverType(s.value("save_cover_type", CollectionSettingsPage::SaveCoverType_Cache).toInt());
148   save_cover_filename_ = CollectionSettingsPage::SaveCoverFilename(s.value("save_cover_filename", CollectionSettingsPage::SaveCoverFilename_Pattern).toInt());
149   cover_pattern_ = s.value("cover_pattern", "%albumartist-%album").toString();
150   cover_overwrite_ = s.value("cover_overwrite", false).toBool();
151   cover_lowercase_ = s.value("cover_lowercase", false).toBool();
152   cover_replace_spaces_ = s.value("cover_replace_spaces", false).toBool();
153   s.endGroup();
154 
155 }
156 
GetAllActions()157 QList<QAction*> AlbumCoverChoiceController::GetAllActions() {
158 
159   return QList<QAction*>() << show_cover_
160                            << cover_to_file_
161                            << separator1_
162                            << cover_from_file_
163                            << cover_from_url_
164                            << search_for_cover_
165                            << separator2_
166                            << unset_cover_
167                            << clear_cover_
168                            << delete_cover_;
169 
170 }
171 
LoadImageFromFile(Song * song)172 AlbumCoverImageResult AlbumCoverChoiceController::LoadImageFromFile(Song *song) {
173 
174   if (!song->url().isLocalFile()) return AlbumCoverImageResult();
175 
176   QString cover_file = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
177 
178   if (cover_file.isEmpty()) return AlbumCoverImageResult();
179 
180   AlbumCoverImageResult result;
181   QFile file(cover_file);
182   if (file.open(QIODevice::ReadOnly)) {
183     result.image_data = file.readAll();
184     file.close();
185     if (result.image_data.isEmpty()) {
186       qLog(Error) << "Cover file" << cover_file << "is empty.";
187       emit Error(tr("Cover file %1 is empty.").arg(cover_file));
188     }
189     else {
190       result.mime_type = Utilities::MimeTypeFromData(result.image_data);
191       result.image.loadFromData(result.image_data);
192       result.cover_url = QUrl::fromLocalFile(cover_file);
193     }
194   }
195   else {
196     qLog(Error) << "Failed to open cover file" << cover_file << "for reading:" << file.errorString();
197     emit Error(tr("Failed to open cover file %1 for reading: %2").arg(cover_file, file.errorString()));
198   }
199 
200   return result;
201 
202 }
203 
LoadCoverFromFile(Song * song)204 QUrl AlbumCoverChoiceController::LoadCoverFromFile(Song *song) {
205 
206   if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl();
207 
208   QString cover_file = QFileDialog::getOpenFileName(this, tr("Load cover from disk"), GetInitialPathForFileDialog(*song, QString()), tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
209 
210   if (cover_file.isEmpty()) return QUrl();
211 
212   if (QImage(cover_file).isNull()) return QUrl();
213 
214   switch(get_save_album_cover_type()) {
215     case CollectionSettingsPage::SaveCoverType_Embedded:
216       if (song->save_embedded_cover_supported()) {
217         SaveCoverEmbeddedAutomatic(*song, cover_file);
218         return QUrl::fromLocalFile(Song::kEmbeddedCover);
219       }
220       // fallthrough
221     case CollectionSettingsPage::SaveCoverType_Cache:
222     case CollectionSettingsPage::SaveCoverType_Album:{
223       QUrl cover_url = QUrl::fromLocalFile(cover_file);
224       SaveArtManualToSong(song, cover_url);
225       return cover_url;
226     }
227   }
228 
229   return QUrl();
230 
231 }
232 
SaveCoverToFileManual(const Song & song,const AlbumCoverImageResult & result)233 void AlbumCoverChoiceController::SaveCoverToFileManual(const Song &song, const AlbumCoverImageResult &result) {
234 
235   QString initial_file_name = "/";
236 
237   if (!song.effective_albumartist().isEmpty()) {
238     initial_file_name = initial_file_name + song.effective_albumartist();
239   }
240   initial_file_name = initial_file_name + "-" + (song.effective_album().isEmpty() ? tr("unknown") : song.effective_album()) + ".jpg";
241   initial_file_name = initial_file_name.toLower();
242   initial_file_name.replace(QRegularExpression("\\s"), "-");
243   initial_file_name.remove(OrganizeFormat::kInvalidFatCharacters);
244 
245   QString save_filename = QFileDialog::getSaveFileName(this, tr("Save album cover"), GetInitialPathForFileDialog(song, initial_file_name), tr(kSaveImageFileFilter) + ";;" + tr(kAllFilesFilter));
246 
247   if (save_filename.isEmpty()) return;
248 
249   QFileInfo fileinfo(save_filename);
250   if (fileinfo.suffix().isEmpty()) {
251     save_filename.append(".jpg");
252     fileinfo.setFile(save_filename);
253   }
254 
255   if (!QImageWriter::supportedImageFormats().contains(fileinfo.completeSuffix().toUtf8().toLower())) {
256     save_filename = Utilities::PathWithoutFilenameExtension(save_filename) + ".jpg";
257     fileinfo.setFile(save_filename);
258   }
259 
260   if (result.is_jpeg() && fileinfo.completeSuffix().compare("jpg", Qt::CaseInsensitive) == 0) {
261     QFile file(save_filename);
262     if (file.open(QIODevice::WriteOnly)) {
263       if (file.write(result.image_data) <= 0) {
264         qLog(Error) << "Failed writing cover to file" << save_filename << file.errorString();
265         emit Error(tr("Failed writing cover to file %1: %2").arg(save_filename, file.errorString()));
266       }
267       file.close();
268     }
269     else {
270       qLog(Error) << "Failed to open cover file" << save_filename << "for writing:" << file.errorString();
271       emit Error(tr("Failed to open cover file %1 for writing: %2").arg(save_filename, file.errorString()));
272     }
273   }
274   else {
275     if (!result.image.save(save_filename)) {
276       qLog(Error) << "Failed writing cover to file" << save_filename;
277       emit Error(tr("Failed writing cover to file %1.").arg(save_filename));
278     }
279   }
280 
281 }
282 
GetInitialPathForFileDialog(const Song & song,const QString & filename)283 QString AlbumCoverChoiceController::GetInitialPathForFileDialog(const Song &song, const QString &filename) {
284 
285   // Art automatic is first to show user which cover the album may be using now;
286   // The song is using it if there's no manual path but we cannot use manual path here because it can contain cached paths
287   if (!song.art_automatic().isEmpty() && !song.art_automatic().path().isEmpty() && !song.has_embedded_cover()) {
288     if (song.art_automatic().scheme().isEmpty() && QFile::exists(QFileInfo(song.art_automatic().path()).path())) {
289       return song.art_automatic().path();
290     }
291     else if (song.art_automatic().isLocalFile() && QFile::exists(QFileInfo(song.art_automatic().toLocalFile()).path())) {
292       return song.art_automatic().toLocalFile();
293     }
294     // If no automatic art, start in the song's folder
295   }
296   else if (!song.url().isEmpty() && song.url().toLocalFile().contains('/')) {
297     return song.url().toLocalFile().section('/', 0, -2) + filename;
298     // Fallback - start in home
299   }
300 
301   return QDir::home().absolutePath() + filename;
302 
303 }
304 
LoadCoverFromURL(Song * song)305 QUrl AlbumCoverChoiceController::LoadCoverFromURL(Song *song) {
306 
307   if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl();
308 
309   AlbumCoverImageResult result = LoadImageFromURL();
310 
311   if (result.image.isNull()) {
312     return QUrl();
313   }
314   else {
315     return SaveCoverAutomatic(song, result);
316   }
317 
318 }
319 
LoadImageFromURL()320 AlbumCoverImageResult AlbumCoverChoiceController::LoadImageFromURL() {
321 
322   if (!cover_from_url_dialog_) { cover_from_url_dialog_ = new CoverFromURLDialog(this); }
323 
324   return cover_from_url_dialog_->Exec();
325 
326 }
327 
SearchForCover(Song * song)328 QUrl AlbumCoverChoiceController::SearchForCover(Song *song) {
329 
330   if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl();
331 
332   // Get something sensible to stick in the search box
333   AlbumCoverImageResult result = SearchForImage(song);
334   if (result.is_valid()) {
335     return SaveCoverAutomatic(song, result);
336   }
337   else {
338     return QUrl();
339   }
340 
341 }
342 
SearchForImage(Song * song)343 AlbumCoverImageResult AlbumCoverChoiceController::SearchForImage(Song *song) {
344 
345   if (!song->url().isLocalFile()) return AlbumCoverImageResult();
346 
347   QString album = song->effective_album();
348   album = album.remove(Song::kAlbumRemoveDisc).remove(Song::kAlbumRemoveMisc);
349 
350   // Get something sensible to stick in the search box
351   return cover_searcher_->Exec(song->effective_albumartist(), album);
352 
353 }
354 
UnsetCover(Song * song,const bool clear_art_automatic)355 QUrl AlbumCoverChoiceController::UnsetCover(Song *song, const bool clear_art_automatic) {
356 
357   if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return QUrl();
358 
359   QUrl cover_url = QUrl::fromLocalFile(Song::kManuallyUnsetCover);
360   SaveArtManualToSong(song, cover_url, clear_art_automatic);
361 
362   return cover_url;
363 
364 }
365 
ClearCover(Song * song,const bool clear_art_automatic)366 void AlbumCoverChoiceController::ClearCover(Song *song, const bool clear_art_automatic) {
367 
368   if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return;
369 
370   song->clear_art_manual();
371   if (clear_art_automatic) song->clear_art_automatic();
372   SaveArtManualToSong(song, QUrl(), clear_art_automatic);
373 
374 }
375 
DeleteCover(Song * song,const bool manually_unset)376 bool AlbumCoverChoiceController::DeleteCover(Song *song, const bool manually_unset) {
377 
378   if (!song->url().isLocalFile() || song->effective_albumartist().isEmpty() || song->album().isEmpty()) return false;
379 
380   if (song->has_embedded_cover() && song->save_embedded_cover_supported()) {
381     SaveCoverEmbeddedAutomatic(*song, AlbumCoverImageResult());
382   }
383 
384   QString art_automatic;
385   QString art_manual;
386   if (song->art_automatic().isValid() && song->art_automatic().isLocalFile()) {
387     art_automatic = song->art_automatic().toLocalFile();
388   }
389   if (song->art_manual().isValid() && song->art_manual().isLocalFile()) {
390     art_manual = song->art_manual().toLocalFile();
391   }
392 
393   bool success = true;
394 
395   if (!art_automatic.isEmpty()) {
396     QFile file(art_automatic);
397     if (file.exists()) {
398       if (file.remove()) {
399         song->clear_art_automatic();
400         if (art_automatic == art_manual) song->clear_art_manual();
401       }
402       else {
403         success = false;
404         qLog(Error) << "Failed to delete cover file" << art_automatic << file.errorString();
405         emit Error(tr("Failed to delete cover file %1: %2").arg(art_automatic, file.errorString()));
406       }
407     }
408     else song->clear_art_automatic();
409   }
410   else song->clear_art_automatic();
411 
412   if (!art_manual.isEmpty()) {
413     QFile file(art_manual);
414     if (file.exists()) {
415       if (file.remove()) {
416         song->clear_art_manual();
417         if (art_automatic == art_manual) song->clear_art_automatic();
418       }
419       else {
420         success = false;
421         qLog(Error) << "Failed to delete cover file" << art_manual << file.errorString();
422         emit Error(tr("Failed to delete cover file %1: %2").arg(art_manual, file.errorString()));
423       }
424     }
425     else song->clear_art_manual();
426   }
427   else song->clear_art_manual();
428 
429   if (success) {
430     if (manually_unset) UnsetCover(song, true);
431     else ClearCover(song, true);
432   }
433 
434   return success;
435 
436 }
437 
ShowCover(const Song & song,const QImage & image)438 void AlbumCoverChoiceController::ShowCover(const Song &song, const QImage &image) {
439 
440   if (image.isNull()) {
441     if ((song.art_manual().isValid() && song.art_manual().isLocalFile() && QFile::exists(song.art_manual().toLocalFile())) ||
442         (song.art_automatic().isValid() && song.art_automatic().isLocalFile() && QFile::exists(song.art_automatic().toLocalFile())) ||
443         song.has_embedded_cover()
444     ) {
445       QPixmap pixmap = ImageUtils::TryLoadPixmap(song.art_automatic(), song.art_manual(), song.url());
446       if (!pixmap.isNull()) ShowCover(song, pixmap);
447     }
448   }
449   else {
450     QPixmap pixmap = QPixmap::fromImage(image);
451     if (!pixmap.isNull()) ShowCover(song, pixmap);
452   }
453 
454 }
455 
ShowCover(const Song & song,const QPixmap & pixmap)456 void AlbumCoverChoiceController::ShowCover(const Song &song, const QPixmap &pixmap) {
457 
458   QDialog *dialog = new QDialog(this);
459   dialog->setAttribute(Qt::WA_DeleteOnClose, true);
460 
461   // Use Artist - Album as the window title
462   QString title_text(song.effective_albumartist());
463   if (!song.effective_album().isEmpty()) title_text += " - " + song.effective_album();
464 
465   QLabel *label = new QLabel(dialog);
466   label->setPixmap(pixmap);
467 
468   // Add (WxHpx) to the title before possibly resizing
469   title_text += " (" + QString::number(pixmap.width()) + "x" + QString::number(pixmap.height()) + "px)";
470 
471   // If the cover is larger than the screen, resize the window 85% seems to be enough to account for title bar and taskbar etc.
472 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
473   QScreen *screen = QWidget::screen();
474 #else
475   QScreen *screen = (window() && window()->windowHandle() ? window()->windowHandle()->screen() : QGuiApplication::primaryScreen());
476 #endif
477   QRect screenGeometry = screen->availableGeometry();
478   int desktop_height = screenGeometry.height();
479   int desktop_width = screenGeometry.width();
480 
481   // Resize differently if monitor is in portrait mode
482   if (desktop_width < desktop_height) {
483     const int new_width = static_cast<int>(static_cast<double>(desktop_width) * 0.95);
484     if (new_width < pixmap.width()) {
485       label->setPixmap(pixmap.scaledToWidth(new_width, Qt::SmoothTransformation));
486     }
487   }
488   else {
489     const int new_height = static_cast<int>(static_cast<double>(desktop_height) * 0.85);
490     if (new_height < pixmap.height()) {
491       label->setPixmap(pixmap.scaledToHeight(new_height, Qt::SmoothTransformation));
492     }
493   }
494 
495   dialog->setWindowTitle(title_text);
496 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
497   dialog->setFixedSize(label->pixmap(Qt::ReturnByValue).size());
498 #else
499   dialog->setFixedSize(label->pixmap()->size());
500 #endif
501   dialog->show();
502 
503 }
504 
SearchCoverAutomatically(const Song & song)505 quint64 AlbumCoverChoiceController::SearchCoverAutomatically(const Song &song) {
506 
507   quint64 id = cover_fetcher_->FetchAlbumCover(song.effective_albumartist(), song.album(), song.title(), true);
508 
509   cover_fetching_tasks_[id] = song;
510 
511   return id;
512 
513 }
514 
AlbumCoverFetched(const quint64 id,const AlbumCoverImageResult & result,const CoverSearchStatistics & statistics)515 void AlbumCoverChoiceController::AlbumCoverFetched(const quint64 id, const AlbumCoverImageResult &result, const CoverSearchStatistics &statistics) {
516 
517   Q_UNUSED(statistics);
518 
519   Song song;
520   if (cover_fetching_tasks_.contains(id)) {
521     song = cover_fetching_tasks_.take(id);
522   }
523 
524   if (result.is_valid()) {
525     SaveCoverAutomatic(&song, result);
526   }
527 
528   emit AutomaticCoverSearchDone();
529 
530 }
531 
SaveArtAutomaticToSong(Song * song,const QUrl & art_automatic)532 void AlbumCoverChoiceController::SaveArtAutomaticToSong(Song *song, const QUrl &art_automatic) {
533 
534   if (!song->is_valid()) return;
535 
536   song->set_art_automatic(art_automatic);
537 
538   if (song->source() == Song::Source_Collection) {
539     app_->collection_backend()->UpdateAutomaticAlbumArtAsync(song->effective_albumartist(), song->album(), art_automatic);
540   }
541 
542   if (*song == app_->current_albumcover_loader()->last_song()) {
543     app_->current_albumcover_loader()->LoadAlbumCover(*song);
544   }
545 
546 }
547 
SaveArtManualToSong(Song * song,const QUrl & art_manual,const bool clear_art_automatic)548 void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art_manual, const bool clear_art_automatic) {
549 
550   if (!song->is_valid()) return;
551 
552   song->set_art_manual(art_manual);
553   if (clear_art_automatic) song->clear_art_automatic();
554 
555   // Update the backends.
556   switch (song->source()) {
557     case Song::Source_Collection:
558       app_->collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic);
559       break;
560     case Song::Source_LocalFile:
561     case Song::Source_CDDA:
562     case Song::Source_Device:
563     case Song::Source_Stream:
564     case Song::Source_RadioParadise:
565     case Song::Source_SomaFM:
566     case Song::Source_Unknown:
567       break;
568     case Song::Source_Tidal:
569     case Song::Source_Qobuz:
570     case Song::Source_Subsonic:
571       InternetService *service = app_->internet_services()->ServiceBySource(song->source());
572       if (!service) break;
573       if (service->artists_collection_backend()) {
574         service->artists_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic);
575       }
576       if (service->albums_collection_backend()) {
577         service->albums_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic);
578       }
579       if (service->songs_collection_backend()) {
580         service->songs_collection_backend()->UpdateManualAlbumArtAsync(song->effective_albumartist(), song->album(), art_manual, clear_art_automatic);
581       }
582       break;
583   }
584 
585   if (*song == app_->current_albumcover_loader()->last_song()) {
586     app_->current_albumcover_loader()->LoadAlbumCover(*song);
587   }
588 
589 }
590 
SaveCoverToFileAutomatic(const Song * song,const AlbumCoverImageResult & result,const bool force_overwrite)591 QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song *song, const AlbumCoverImageResult &result, const bool force_overwrite) {
592 
593   return SaveCoverToFileAutomatic(song->source(),
594                                   song->effective_albumartist(),
595                                   song->effective_album(),
596                                   song->album_id(),
597                                   song->url().adjusted(QUrl::RemoveFilename).path(),
598                                   result,
599                                   force_overwrite);
600 
601 }
602 
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)603 QUrl AlbumCoverChoiceController::SaveCoverToFileAutomatic(const Song::Source source,
604                                                           const QString &artist,
605                                                           const QString &album,
606                                                           const QString &album_id,
607                                                           const QString &album_dir,
608                                                           const AlbumCoverImageResult &result,
609                                                           const bool force_overwrite) {
610 
611   QString filepath = app_->album_cover_loader()->CoverFilePath(source, artist, album, album_id, album_dir, result.cover_url, "jpg");
612   if (filepath.isEmpty()) return QUrl();
613 
614   QFile file(filepath);
615   // Don't overwrite when saving in album dir if the filename is set to pattern unless "force_overwrite" is set.
616   if (source == Song::Source_Collection && !cover_overwrite_ && !force_overwrite && get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Album && save_cover_filename_ == CollectionSettingsPage::SaveCoverFilename_Pattern && file.exists()) {
617     while (file.exists()) {
618       QFileInfo fileinfo(file.fileName());
619       file.setFileName(fileinfo.path() + "/0" + fileinfo.fileName());
620     }
621     filepath = file.fileName();
622   }
623 
624   QUrl cover_url;
625   if (result.is_jpeg()) {
626     if (file.open(QIODevice::WriteOnly)) {
627       if (file.write(result.image_data) > 0) {
628         cover_url = QUrl::fromLocalFile(filepath);
629       }
630       else {
631         qLog(Error) << "Failed to write cover to file" << file.fileName() << file.errorString();
632         emit Error(tr("Failed to write cover to file %1: %2").arg(file.fileName(), file.errorString()));
633       }
634       file.close();
635     }
636     else {
637       qLog(Error) << "Failed to open cover file" << file.fileName() << "for writing:" << file.errorString();
638       emit Error(tr("Failed to open cover file %1 for writing: %2").arg(file.fileName(), file.errorString()));
639     }
640   }
641   else {
642     if (result.image.save(filepath, "JPG")) cover_url = QUrl::fromLocalFile(filepath);
643   }
644 
645   return cover_url;
646 
647 }
648 
SaveCoverEmbeddedAutomatic(const Song & song,const AlbumCoverImageResult & result)649 void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const AlbumCoverImageResult &result) {
650 
651   if (song.source() == Song::Source_Collection) {
652 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
653     QFuture<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions());
654 #else
655     QFuture<SongList> future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), QueryOptions());
656 #endif
657     QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
658     QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [=]() {
659       SongList songs = watcher->result();
660       watcher->deleteLater();
661       QList<QUrl> urls;
662       urls.reserve(songs.count());
663       for (const Song &s : songs) urls << s.url();
664       if (result.is_jpeg()) {
665         quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data);
666         QMutexLocker l(&mutex_cover_save_tasks_);
667         cover_save_tasks_.insert(id, song);
668       }
669       else {
670         quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image);
671         QMutexLocker l(&mutex_cover_save_tasks_);
672         cover_save_tasks_.insert(id, song);
673       }
674     });
675     watcher->setFuture(future);
676   }
677   else {
678     if (result.is_jpeg()) {
679       app_->album_cover_loader()->SaveEmbeddedCoverAsync(song.url().toLocalFile(), result.image_data);
680     }
681     else {
682       app_->album_cover_loader()->SaveEmbeddedCoverAsync(song.url().toLocalFile(), result.image);
683     }
684   }
685 
686 }
687 
SaveCoverEmbeddedAutomatic(const Song & song,const QUrl & cover_url)688 void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const QUrl &cover_url) {
689 
690   SaveCoverEmbeddedAutomatic(song, cover_url.toLocalFile());
691 
692 }
693 
SaveCoverEmbeddedAutomatic(const Song & song,const QString & cover_filename)694 void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const Song &song, const QString &cover_filename) {
695 
696   if (song.source() == Song::Source_Collection) {
697 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
698     QFuture<SongList> future = QtConcurrent::run(&CollectionBackend::GetAlbumSongs, app_->collection_backend(), song.effective_albumartist(), song.effective_album(), QueryOptions());
699 #else
700     QFuture<SongList> future = QtConcurrent::run(app_->collection_backend(), &CollectionBackend::GetAlbumSongs, song.effective_albumartist(), song.effective_album(), QueryOptions());
701 #endif
702     QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
703     QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [=]() {
704       SongList songs = watcher->result();
705       watcher->deleteLater();
706       QList<QUrl> urls;
707       urls.reserve(songs.count());
708       for (const Song &s : songs) urls << s.url();
709       quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, cover_filename);
710       QMutexLocker l(&mutex_cover_save_tasks_);
711       cover_save_tasks_.insert(id, song);
712     });
713     watcher->setFuture(future);
714   }
715   else {
716     app_->album_cover_loader()->SaveEmbeddedCoverAsync(song.url().toLocalFile(), cover_filename);
717   }
718 
719 }
720 
SaveCoverEmbeddedAutomatic(const QList<QUrl> & urls,const QImage & image)721 void AlbumCoverChoiceController::SaveCoverEmbeddedAutomatic(const QList<QUrl> &urls, const QImage &image) {
722 
723   app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, image);
724 
725 }
726 
IsKnownImageExtension(const QString & suffix)727 bool AlbumCoverChoiceController::IsKnownImageExtension(const QString &suffix) {
728 
729   if (!sImageExtensions) {
730     sImageExtensions = new QSet<QString>();
731    (*sImageExtensions) << "png" << "jpg" << "jpeg" << "bmp" << "gif" << "xpm" << "pbm" << "pgm" << "ppm" << "xbm";
732   }
733 
734   return sImageExtensions->contains(suffix);
735 
736 }
737 
CanAcceptDrag(const QDragEnterEvent * e)738 bool AlbumCoverChoiceController::CanAcceptDrag(const QDragEnterEvent *e) {
739 
740   for (const QUrl &url : e->mimeData()->urls()) {
741     const QString suffix = QFileInfo(url.toLocalFile()).suffix().toLower();
742     if (IsKnownImageExtension(suffix)) return true;
743   }
744   return e->mimeData()->hasImage();
745 
746 }
747 
SaveCover(Song * song,const QDropEvent * e)748 QUrl AlbumCoverChoiceController::SaveCover(Song *song, const QDropEvent *e) {
749 
750   for (const QUrl &url : e->mimeData()->urls()) {
751 
752     const QString filename = url.toLocalFile();
753     const QString suffix = QFileInfo(filename).suffix().toLower();
754 
755     if (IsKnownImageExtension(suffix)) {
756       if (get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Embedded && song->save_embedded_cover_supported()) {
757         SaveCoverEmbeddedAutomatic(*song, filename);
758         return QUrl::fromLocalFile(Song::kEmbeddedCover);
759       }
760       else {
761         SaveArtManualToSong(song, url);
762       }
763       return url;
764     }
765   }
766 
767   if (e->mimeData()->hasImage()) {
768     QImage image = qvariant_cast<QImage>(e->mimeData()->imageData());
769     if (!image.isNull()) {
770       return SaveCoverAutomatic(song, AlbumCoverImageResult(image));
771     }
772   }
773 
774   return QUrl();
775 
776 }
777 
SaveCoverAutomatic(Song * song,const AlbumCoverImageResult & result)778 QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCoverImageResult &result) {
779 
780   QUrl cover_url;
781   switch(get_save_album_cover_type()) {
782     case CollectionSettingsPage::SaveCoverType_Embedded:{
783       if (song->save_embedded_cover_supported()) {
784         SaveCoverEmbeddedAutomatic(*song, result);
785         cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover);
786         break;
787       }
788     }
789     // fallthrough
790     case CollectionSettingsPage::SaveCoverType_Cache:
791     case CollectionSettingsPage::SaveCoverType_Album:{
792       cover_url = SaveCoverToFileAutomatic(song, result);
793       if (!cover_url.isEmpty()) SaveArtManualToSong(song, cover_url);
794       break;
795     }
796   }
797 
798   return cover_url;
799 
800 }
801 
SaveEmbeddedCoverAsyncFinished(quint64 id,const bool success,const bool cleared)802 void AlbumCoverChoiceController::SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success, const bool cleared) {
803 
804   if (!cover_save_tasks_.contains(id)) return;
805 
806   Song song = cover_save_tasks_.take(id);
807   if (success) {
808     if (cleared) SaveArtAutomaticToSong(&song, QUrl());
809     else SaveArtAutomaticToSong(&song, QUrl::fromLocalFile(Song::kEmbeddedCover));
810   }
811 
812 }
813