1 /*
2  * Strawberry Music Player
3  * This file was part of Clementine.
4  * Copyright 2010, David Sansome <me@davidsansome.com>
5  * Copyright 2018-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 <memory>
25 #include <algorithm>
26 
27 #include <QtGlobal>
28 #include <QObject>
29 #include <QMainWindow>
30 #include <QWidget>
31 #include <QtConcurrent>
32 #include <QFuture>
33 #include <QFutureWatcher>
34 #include <QScreen>
35 #include <QWindow>
36 #include <QItemSelectionModel>
37 #include <QListWidgetItem>
38 #include <QFile>
39 #include <QSet>
40 #include <QVariant>
41 #include <QString>
42 #include <QStringBuilder>
43 #include <QStringList>
44 #include <QUrl>
45 #include <QImage>
46 #include <QImageWriter>
47 #include <QPixmap>
48 #include <QPainter>
49 #include <QTimer>
50 #include <QMenu>
51 #include <QAction>
52 #include <QActionGroup>
53 #include <QShortcut>
54 #include <QSplitter>
55 #include <QStatusBar>
56 #include <QLabel>
57 #include <QListWidget>
58 #include <QMessageBox>
59 #include <QProgressBar>
60 #include <QPushButton>
61 #include <QToolButton>
62 #include <QKeySequence>
63 #include <QSettings>
64 #include <QFlags>
65 #include <QSize>
66 #include <QtEvents>
67 
68 #include "core/application.h"
69 #include "core/iconloader.h"
70 #include "core/utilities.h"
71 #include "core/imageutils.h"
72 #include "core/tagreaderclient.h"
73 #include "core/database.h"
74 #include "widgets/forcescrollperpixel.h"
75 #include "widgets/qsearchfield.h"
76 #include "collection/sqlrow.h"
77 #include "collection/collectionbackend.h"
78 #include "collection/collectionquery.h"
79 #include "playlist/songmimedata.h"
80 #include "settings/collectionsettingspage.h"
81 #include "coverproviders.h"
82 #include "albumcovermanager.h"
83 #include "albumcoversearcher.h"
84 #include "albumcoverchoicecontroller.h"
85 #include "albumcoverexport.h"
86 #include "albumcoverexporter.h"
87 #include "albumcoverfetcher.h"
88 #include "albumcoverloader.h"
89 #include "albumcoverloaderoptions.h"
90 #include "albumcoverloaderresult.h"
91 #include "albumcovermanagerlist.h"
92 #include "coversearchstatistics.h"
93 #include "coversearchstatisticsdialog.h"
94 #include "albumcoverimageresult.h"
95 
96 #include "ui_albumcovermanager.h"
97 
98 const char *AlbumCoverManager::kSettingsGroup = "CoverManager";
99 
AlbumCoverManager(Application * app,CollectionBackend * collection_backend,QMainWindow * mainwindow,QWidget * parent)100 AlbumCoverManager::AlbumCoverManager(Application *app, CollectionBackend *collection_backend, QMainWindow *mainwindow, QWidget *parent)
101     : QMainWindow(parent),
102       ui_(new Ui_CoverManager),
103       mainwindow_(mainwindow),
104       app_(app),
105       collection_backend_(collection_backend),
106       album_cover_choice_controller_(new AlbumCoverChoiceController(this)),
107       filter_all_(nullptr),
108       filter_with_covers_(nullptr),
109       filter_without_covers_(nullptr),
110       cover_fetcher_(new AlbumCoverFetcher(app_->cover_providers(), this)),
111       cover_searcher_(nullptr),
112       cover_export_(nullptr),
113       cover_exporter_(new AlbumCoverExporter(this)),
114       artist_icon_(IconLoader::Load("folder-sound")),
115       all_artists_icon_(IconLoader::Load("library-music")),
116       image_nocover_thumbnail_(ImageUtils::GenerateNoCoverImage(QSize(120, 120))),
117       icon_nocover_item_(QPixmap::fromImage(image_nocover_thumbnail_)),
118       context_menu_(new QMenu(this)),
119       progress_bar_(new QProgressBar(this)),
120       abort_progress_(new QPushButton(this)),
121       jobs_(0),
122       all_artists_(nullptr) {
123 
124   ui_->setupUi(this);
125   ui_->albums->set_cover_manager(this);
126 
127   // Icons
128   ui_->action_fetch->setIcon(IconLoader::Load("download"));
129   ui_->export_covers->setIcon(IconLoader::Load("document-save"));
130   ui_->view->setIcon(IconLoader::Load("view-choose"));
131   ui_->button_fetch->setIcon(IconLoader::Load("download"));
132   ui_->action_add_to_playlist->setIcon(IconLoader::Load("media-playback-start"));
133   ui_->action_load->setIcon(IconLoader::Load("media-playback-start"));
134 
135   album_cover_choice_controller_->Init(app_);
136 
137   cover_searcher_ = new AlbumCoverSearcher(icon_nocover_item_, app_, this);
138   cover_export_ = new AlbumCoverExport(this);
139 
140   // Set up the status bar
141   statusBar()->addPermanentWidget(progress_bar_);
142   statusBar()->addPermanentWidget(abort_progress_);
143   progress_bar_->hide();
144   abort_progress_->hide();
145   abort_progress_->setText(tr("Abort"));
146   QObject::connect(abort_progress_, &QPushButton::clicked, this, &AlbumCoverManager::CancelRequests);
147 
148   ui_->albums->setAttribute(Qt::WA_MacShowFocusRect, false);
149   ui_->artists->setAttribute(Qt::WA_MacShowFocusRect, false);
150 
151   QShortcut *close = new QShortcut(QKeySequence::Close, this);
152   QObject::connect(close, &QShortcut::activated, this, &AlbumCoverManager::close);
153 
154   cover_loader_options_.scale_output_image_ = true;
155   cover_loader_options_.pad_output_image_ = true;
156   cover_loader_options_.desired_height_ = 120;
157   cover_loader_options_.create_thumbnail_ = false;
158 
159   EnableCoversButtons();
160 
161 }
162 
~AlbumCoverManager()163 AlbumCoverManager::~AlbumCoverManager() {
164 
165   CancelRequests();
166   delete ui_;
167 
168 }
169 
Init()170 void AlbumCoverManager::Init() {
171 
172   // View menu
173   QActionGroup *filter_group = new QActionGroup(this);
174   filter_all_ = filter_group->addAction(tr("All albums"));
175   filter_with_covers_ = filter_group->addAction(tr("Albums with covers"));
176   filter_without_covers_ = filter_group->addAction(tr("Albums without covers"));
177   filter_all_->setCheckable(true);
178   filter_with_covers_->setCheckable(true);
179   filter_without_covers_->setCheckable(true);
180   filter_group->setExclusive(true);
181   filter_all_->setChecked(true);
182 
183   QMenu *view_menu = new QMenu(this);
184   view_menu->addActions(filter_group->actions());
185 
186   ui_->view->setMenu(view_menu);
187 
188   // Context menu
189 
190   QList<QAction*> actions = album_cover_choice_controller_->GetAllActions();
191 
192   QObject::connect(album_cover_choice_controller_, &AlbumCoverChoiceController::Error, this, &AlbumCoverManager::Error);
193   QObject::connect(album_cover_choice_controller_->cover_from_file_action(), &QAction::triggered, this, &AlbumCoverManager::LoadCoverFromFile);
194   QObject::connect(album_cover_choice_controller_->cover_to_file_action(), &QAction::triggered, this, &AlbumCoverManager::SaveCoverToFile);
195   QObject::connect(album_cover_choice_controller_->cover_from_url_action(), &QAction::triggered, this, &AlbumCoverManager::LoadCoverFromURL);
196   QObject::connect(album_cover_choice_controller_->search_for_cover_action(), &QAction::triggered, this, &AlbumCoverManager::SearchForCover);
197   QObject::connect(album_cover_choice_controller_->unset_cover_action(), &QAction::triggered, this, &AlbumCoverManager::UnsetCover);
198   QObject::connect(album_cover_choice_controller_->clear_cover_action(), &QAction::triggered, this, &AlbumCoverManager::ClearCover);
199   QObject::connect(album_cover_choice_controller_->delete_cover_action(), &QAction::triggered, this, &AlbumCoverManager::DeleteCover);
200   QObject::connect(album_cover_choice_controller_->show_cover_action(), &QAction::triggered, this, &AlbumCoverManager::ShowCover);
201 
202   QObject::connect(cover_exporter_, &AlbumCoverExporter::AlbumCoversExportUpdate, this, &AlbumCoverManager::UpdateExportStatus);
203 
204   context_menu_->addActions(actions);
205   context_menu_->addSeparator();
206   context_menu_->addAction(ui_->action_load);
207   context_menu_->addAction(ui_->action_add_to_playlist);
208 
209   ui_->albums->installEventFilter(this);
210 
211   // Connections
212   QObject::connect(ui_->artists, &QListWidget::currentItemChanged, this, &AlbumCoverManager::ArtistChanged);
213   QObject::connect(ui_->filter, &QSearchField::textChanged, this, &AlbumCoverManager::UpdateFilter);
214   QObject::connect(filter_group, &QActionGroup::triggered, this, &AlbumCoverManager::UpdateFilter);
215   QObject::connect(ui_->view, &QToolButton::clicked, ui_->view, &QToolButton::showMenu);
216   QObject::connect(ui_->button_fetch, &QPushButton::clicked, this, &AlbumCoverManager::FetchAlbumCovers);
217   QObject::connect(ui_->export_covers, &QPushButton::clicked, this, &AlbumCoverManager::ExportCovers);
218   QObject::connect(cover_fetcher_, &AlbumCoverFetcher::AlbumCoverFetched, this, &AlbumCoverManager::AlbumCoverFetched);
219   QObject::connect(ui_->action_fetch, &QAction::triggered, this, &AlbumCoverManager::FetchSingleCover);
220   QObject::connect(ui_->albums, &QListWidget::doubleClicked, this, &AlbumCoverManager::AlbumDoubleClicked);
221   QObject::connect(ui_->action_add_to_playlist, &QAction::triggered, this, &AlbumCoverManager::AddSelectedToPlaylist);
222   QObject::connect(ui_->action_load, &QAction::triggered, this, &AlbumCoverManager::LoadSelectedToPlaylist);
223 
224   // Restore settings
225   QSettings s;
226   s.beginGroup(kSettingsGroup);
227 
228   if (s.contains("geometry")) {
229     restoreGeometry(s.value("geometry").toByteArray());
230   }
231 
232   if (!s.contains("splitter_state") || !ui_->splitter->restoreState(s.value("splitter_state").toByteArray())) {
233     // Sensible default size for the artists view
234     ui_->splitter->setSizes(QList<int>() << 200 << width() - 200);
235   }
236 
237   s.endGroup();
238 
239   QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &AlbumCoverManager::AlbumCoverLoaded);
240   QObject::connect(app_->album_cover_loader(), &AlbumCoverLoader::SaveEmbeddedCoverAsyncFinished, this, &AlbumCoverManager::SaveEmbeddedCoverAsyncFinished);
241 
242   cover_searcher_->Init(cover_fetcher_);
243 
244   new ForceScrollPerPixel(ui_->albums, this);
245 
246 }
247 
showEvent(QShowEvent * e)248 void AlbumCoverManager::showEvent(QShowEvent *e) {
249 
250   if (!e->spontaneous()) {
251     LoadGeometry();
252     album_cover_choice_controller_->ReloadSettings();
253     Reset();
254   }
255 
256   QMainWindow::showEvent(e);
257 
258 }
259 
closeEvent(QCloseEvent * e)260 void AlbumCoverManager::closeEvent(QCloseEvent *e) {
261 
262   if (!cover_fetching_tasks_.isEmpty()) {
263     std::unique_ptr<QMessageBox> message_box(new QMessageBox(QMessageBox::Question, tr("Really cancel?"), tr("Closing this window will stop searching for album covers."), QMessageBox::Abort, this));
264     message_box->addButton(tr("Don't stop!"), QMessageBox::AcceptRole);
265 
266     if (message_box->exec() != QMessageBox::Abort) {
267       e->ignore();
268       return;
269     }
270   }
271 
272   SaveSettings();
273 
274   // Cancel any outstanding requests
275   CancelRequests();
276 
277   ui_->artists->clear();
278   ui_->albums->clear();
279 
280   QMainWindow::closeEvent(e);
281 
282 }
283 
LoadGeometry()284 void AlbumCoverManager::LoadGeometry() {
285 
286   QSettings s;
287   s.beginGroup(kSettingsGroup);
288   if (s.contains("geometry")) {
289     restoreGeometry(s.value("geometry").toByteArray());
290   }
291   if (s.contains("splitter_state")) {
292     ui_->splitter->restoreState(s.value("splitter_state").toByteArray());
293   }
294   else {
295     // Sensible default size for the artists view
296     ui_->splitter->setSizes(QList<int>() << 200 << width() - 200);
297   }
298   s.endGroup();
299 
300   // Center the window on the same screen as the mainwindow.
301 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
302   QScreen *screen = mainwindow_->screen();
303 #else
304   QScreen *screen = (mainwindow_->window() && mainwindow_->window()->windowHandle() ? mainwindow_->window()->windowHandle()->screen() : nullptr);
305 #endif
306   if (screen) {
307     const QRect sr = screen->availableGeometry();
308     const QRect wr({}, size().boundedTo(sr.size()));
309     resize(wr.size());
310     move(sr.center() - wr.center());
311   }
312 
313 }
314 
SaveSettings()315 void AlbumCoverManager::SaveSettings() {
316 
317   QSettings s;
318   s.beginGroup(kSettingsGroup);
319   s.setValue("geometry", saveGeometry());
320   s.setValue("splitter_state", ui_->splitter->saveState());
321   s.setValue("save_cover_type", album_cover_choice_controller_->get_save_album_cover_type());
322   s.endGroup();
323 
324 }
325 
CancelRequests()326 void AlbumCoverManager::CancelRequests() {
327 
328 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
329   app_->album_cover_loader()->CancelTasks(QSet<quint64>(cover_loading_tasks_.keyBegin(), cover_loading_tasks_.keyEnd()));
330 #else
331   app_->album_cover_loader()->CancelTasks(QSet<quint64>::fromList(cover_loading_tasks_.keys()));
332 #endif
333   cover_loading_tasks_.clear();
334   cover_save_tasks_.clear();
335   cover_save_tasks2_.clear();
336 
337   cover_exporter_->Cancel();
338 
339   cover_fetching_tasks_.clear();
340   cover_fetcher_->Clear();
341   progress_bar_->hide();
342   abort_progress_->hide();
343   statusBar()->clearMessage();
344   EnableCoversButtons();
345 
346 }
347 
CompareNocase(const QString & left,const QString & right)348 static bool CompareNocase(const QString &left, const QString &right) {
349   return QString::localeAwareCompare(left, right) < 0;
350 }
351 
CompareAlbumNameNocase(const CollectionBackend::Album & left,const CollectionBackend::Album & right)352 static bool CompareAlbumNameNocase(const CollectionBackend::Album &left, const CollectionBackend::Album &right) {
353   return CompareNocase(left.album, right.album);
354 }
355 
Reset()356 void AlbumCoverManager::Reset() {
357 
358   EnableCoversButtons();
359 
360   ui_->artists->clear();
361   all_artists_ = new QListWidgetItem(all_artists_icon_, tr("All artists"), ui_->artists, All_Artists);
362   new AlbumItem(artist_icon_, tr("Various artists"), ui_->artists, Various_Artists);
363 
364   QStringList artists(collection_backend_->GetAllArtistsWithAlbums());
365   std::stable_sort(artists.begin(), artists.end(), CompareNocase);
366 
367   for (const QString &artist : artists) {
368     if (artist.isEmpty()) continue;
369     new QListWidgetItem(artist_icon_, artist, ui_->artists, Specific_Artist);
370   }
371 
372 }
373 
EnableCoversButtons()374 void AlbumCoverManager::EnableCoversButtons() {
375   ui_->button_fetch->setEnabled(app_->cover_providers()->HasAnyProviders());
376   ui_->export_covers->setEnabled(true);
377 }
378 
DisableCoversButtons()379 void AlbumCoverManager::DisableCoversButtons() {
380   ui_->button_fetch->setEnabled(false);
381   ui_->export_covers->setEnabled(false);
382 }
383 
ArtistChanged(QListWidgetItem * current)384 void AlbumCoverManager::ArtistChanged(QListWidgetItem *current) {
385 
386   if (!current) return;
387 
388   ui_->albums->clear();
389   context_menu_items_.clear();
390   CancelRequests();
391 
392   // Get the list of albums.  How we do it depends on what thing we have selected in the artist list.
393   CollectionBackend::AlbumList albums;
394   switch (current->type()) {
395     case Various_Artists: albums = collection_backend_->GetCompilationAlbums(); break;
396     case Specific_Artist: albums = collection_backend_->GetAlbumsByArtist(current->text()); break;
397     case All_Artists:
398     default:              albums = collection_backend_->GetAllAlbums(); break;
399   }
400 
401   // Sort by album name.  The list is already sorted by sqlite but it was done case sensitively.
402   std::stable_sort(albums.begin(), albums.end(), CompareAlbumNameNocase);
403 
404   for (const CollectionBackend::Album &info : albums) {
405 
406     // Don't show songs without an album, obviously
407     if (info.album.isEmpty()) continue;
408 
409     QString display_text;
410 
411     if (current->type() == Specific_Artist) {
412       display_text = info.album;
413     }
414     else {
415       display_text = info.album_artist + " - " + info.album;
416     }
417 
418     AlbumItem *item = new AlbumItem(icon_nocover_item_, display_text, ui_->albums);
419     item->setData(Role_AlbumArtist, info.album_artist);
420     item->setData(Role_Album, info.album);
421     item->setData(Role_Filetype, info.filetype);
422     item->setData(Role_CuePath, info.cue_path);
423     item->setData(Qt::TextAlignmentRole, QVariant(Qt::AlignTop | Qt::AlignHCenter));
424     item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled);
425     item->urls = info.urls;
426 
427     if (info.album_artist.isEmpty()) {
428       item->setToolTip(info.album);
429     }
430     else {
431       item->setToolTip(info.album_artist + " - " + info.album);
432     }
433 
434     if (!info.art_automatic.isEmpty() || !info.art_manual.isEmpty()) {
435       item->setData(Role_PathAutomatic, info.art_automatic);
436       item->setData(Role_PathManual, info.art_manual);
437       quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, info.art_automatic, info.art_manual, info.urls.first());
438       cover_loading_tasks_[id] = item;
439     }
440 
441   }
442 
443   UpdateFilter();
444 
445 }
446 
AlbumCoverLoaded(const quint64 id,const AlbumCoverLoaderResult & result)447 void AlbumCoverManager::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
448 
449   if (!cover_loading_tasks_.contains(id)) return;
450 
451   AlbumItem *item = cover_loading_tasks_.take(id);
452 
453   if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) {
454     item->setIcon(icon_nocover_item_);
455   }
456   else {
457     item->setIcon(QPixmap::fromImage(result.image_scaled));
458   }
459 
460   //item->setData(Role_Image, result.image_original);
461   //item->setData(Role_ImageData, result.image_data);
462 
463   UpdateFilter();
464 
465 }
466 
UpdateFilter()467 void AlbumCoverManager::UpdateFilter() {
468 
469   const QString filter = ui_->filter->text().toLower();
470   const bool hide_with_covers = filter_without_covers_->isChecked();
471   const bool hide_without_covers = filter_with_covers_->isChecked();
472 
473   HideCovers hide = Hide_None;
474   if (hide_with_covers) {
475     hide = Hide_WithCovers;
476   }
477   else if (hide_without_covers) {
478     hide = Hide_WithoutCovers;
479   }
480 
481   qint32 total_count = 0;
482   qint32 without_cover = 0;
483 
484   for (int i = 0; i < ui_->albums->count(); ++i) {
485     AlbumItem *item = static_cast<AlbumItem*>(ui_->albums->item(i));
486     bool should_hide = ShouldHide(*item, filter, hide);
487     item->setHidden(should_hide);
488 
489     if (!should_hide) {
490       ++total_count;
491       if (!ItemHasCover(*item)) {
492         ++without_cover;
493       }
494     }
495   }
496 
497   ui_->total_albums->setText(QString::number(total_count));
498   ui_->without_cover->setText(QString::number(without_cover));
499 
500 }
501 
ShouldHide(const AlbumItem & item,const QString & filter,HideCovers hide) const502 bool AlbumCoverManager::ShouldHide(const AlbumItem &item, const QString &filter, HideCovers hide) const {
503 
504   bool has_cover = ItemHasCover(item);
505   if (hide == Hide_WithCovers && has_cover) {
506     return true;
507   }
508   else if (hide == Hide_WithoutCovers && !has_cover) {
509     return true;
510   }
511 
512   if (filter.isEmpty()) {
513     return false;
514   }
515 
516   QStringList query = filter.split(' ');
517   for (const QString &s : query) {
518     bool in_text = item.text().contains(s, Qt::CaseInsensitive);
519     bool in_albumartist = item.data(Role_AlbumArtist).toString().contains(s, Qt::CaseInsensitive);
520     if (!in_text && !in_albumartist) {
521       return true;
522     }
523   }
524 
525   return false;
526 
527 }
528 
FetchAlbumCovers()529 void AlbumCoverManager::FetchAlbumCovers() {
530 
531   for (int i = 0; i < ui_->albums->count(); ++i) {
532     AlbumItem *item = static_cast<AlbumItem*>(ui_->albums->item(i));
533     if (item->isHidden()) continue;
534     if (ItemHasCover(*item)) continue;
535 
536     quint64 id = cover_fetcher_->FetchAlbumCover(item->data(Role_AlbumArtist).toString(), item->data(Role_Album).toString(), QString(), true);
537     cover_fetching_tasks_[id] = item;
538     jobs_++;
539   }
540 
541   if (!cover_fetching_tasks_.isEmpty()) ui_->button_fetch->setEnabled(false);
542 
543   progress_bar_->setMaximum(jobs_);
544   progress_bar_->show();
545   abort_progress_->show();
546   fetch_statistics_ = CoverSearchStatistics();
547   UpdateStatusText();
548 
549 }
550 
AlbumCoverFetched(const quint64 id,const AlbumCoverImageResult & result,const CoverSearchStatistics & statistics)551 void AlbumCoverManager::AlbumCoverFetched(const quint64 id, const AlbumCoverImageResult &result, const CoverSearchStatistics &statistics) {
552 
553   if (!cover_fetching_tasks_.contains(id)) return;
554 
555   AlbumItem *item = cover_fetching_tasks_.take(id);
556   if (!result.image.isNull()) {
557     SaveAndSetCover(item, result);
558   }
559 
560   if (cover_fetching_tasks_.isEmpty()) {
561     EnableCoversButtons();
562   }
563 
564   fetch_statistics_ += statistics;
565   UpdateStatusText();
566 
567 }
568 
UpdateStatusText()569 void AlbumCoverManager::UpdateStatusText() {
570 
571   QString message = tr("Got %1 covers out of %2 (%3 failed)")
572                         .arg(fetch_statistics_.chosen_images_)
573                         .arg(jobs_)
574                         .arg(fetch_statistics_.missing_images_);
575 
576   if (fetch_statistics_.bytes_transferred_ > 0) {
577     message += ", " + tr("%1 transferred").arg(Utilities::PrettySize(fetch_statistics_.bytes_transferred_));
578   }
579 
580   statusBar()->showMessage(message);
581   progress_bar_->setValue(static_cast<int>(fetch_statistics_.chosen_images_ + fetch_statistics_.missing_images_));
582 
583   if (cover_fetching_tasks_.isEmpty()) {
584     QTimer::singleShot(2000, statusBar(), &QStatusBar::clearMessage);
585     progress_bar_->hide();
586     abort_progress_->hide();
587 
588     CoverSearchStatisticsDialog *dialog = new CoverSearchStatisticsDialog(this);
589     dialog->setAttribute(Qt::WA_DeleteOnClose);
590     dialog->Show(fetch_statistics_);
591 
592     jobs_ = 0;
593   }
594 
595 }
596 
eventFilter(QObject * obj,QEvent * e)597 bool AlbumCoverManager::eventFilter(QObject *obj, QEvent *e) {
598 
599   if (obj == ui_->albums && e->type() == QEvent::ContextMenu) {
600     context_menu_items_ = ui_->albums->selectedItems();
601     if (context_menu_items_.isEmpty()) return QMainWindow::eventFilter(obj, e);
602 
603     bool some_with_covers = false;
604     bool some_unset = false;
605     bool some_clear = false;
606 
607     for (QListWidgetItem *item : context_menu_items_) {
608       AlbumItem *album_item = static_cast<AlbumItem*>(item);
609       if (ItemHasCover(*album_item)) some_with_covers = true;
610       if (album_item->data(Role_PathManual).toUrl().path() == Song::kManuallyUnsetCover) {
611         some_unset = true;
612       }
613       else if (album_item->data(Role_PathAutomatic).toUrl().isEmpty() && album_item->data(Role_PathManual).toUrl().isEmpty()) {
614         some_clear = true;
615       }
616     }
617 
618     album_cover_choice_controller_->show_cover_action()->setEnabled(some_with_covers && context_menu_items_.size() == 1);
619     album_cover_choice_controller_->cover_to_file_action()->setEnabled(some_with_covers);
620     album_cover_choice_controller_->cover_from_file_action()->setEnabled(context_menu_items_.size() == 1);
621     album_cover_choice_controller_->cover_from_url_action()->setEnabled(context_menu_items_.size() == 1);
622     album_cover_choice_controller_->search_for_cover_action()->setEnabled(app_->cover_providers()->HasAnyProviders());
623     album_cover_choice_controller_->unset_cover_action()->setEnabled(some_with_covers || some_clear);
624     album_cover_choice_controller_->clear_cover_action()->setEnabled(some_with_covers || some_unset);
625     album_cover_choice_controller_->delete_cover_action()->setEnabled(some_with_covers);
626 
627     QContextMenuEvent *context_menu_event = static_cast<QContextMenuEvent*>(e);
628     context_menu_->popup(context_menu_event->globalPos());
629     return true;
630   }
631 
632   return QMainWindow::eventFilter(obj, e);
633 
634 }
635 
GetSingleSelectionAsSong()636 Song AlbumCoverManager::GetSingleSelectionAsSong() {
637   return context_menu_items_.size() != 1 ? Song() : ItemAsSong(context_menu_items_[0]);
638 }
639 
GetFirstSelectedAsSong()640 Song AlbumCoverManager::GetFirstSelectedAsSong() {
641   return context_menu_items_.isEmpty() ? Song() : ItemAsSong(context_menu_items_[0]);
642 }
643 
ItemAsSong(AlbumItem * item)644 Song AlbumCoverManager::ItemAsSong(AlbumItem *item) {
645 
646   Song result(Song::Source_Collection);
647 
648   QString title = item->data(Role_Album).toString();
649   QString artist_name = item->data(Role_AlbumArtist).toString();
650   if (!artist_name.isEmpty()) {
651     result.set_title(artist_name + " - " + title);
652   }
653   else {
654     result.set_title(title);
655   }
656 
657   result.set_artist(item->data(Role_AlbumArtist).toString());
658   result.set_albumartist(item->data(Role_AlbumArtist).toString());
659   result.set_album(item->data(Role_Album).toString());
660 
661   result.set_filetype(Song::FileType(item->data(Role_Filetype).toInt()));
662   result.set_url(item->urls.first());
663   result.set_cue_path(item->data(Role_CuePath).toString());
664 
665   result.set_art_automatic(item->data(Role_PathAutomatic).toUrl());
666   result.set_art_manual(item->data(Role_PathManual).toUrl());
667 
668   // force validity
669   result.set_valid(true);
670   result.set_id(0);
671 
672   return result;
673 }
674 
ShowCover()675 void AlbumCoverManager::ShowCover() {
676 
677   Song song = GetSingleSelectionAsSong();
678   if (!song.is_valid()) return;
679 
680   album_cover_choice_controller_->ShowCover(song);
681 
682 }
683 
FetchSingleCover()684 void AlbumCoverManager::FetchSingleCover() {
685 
686   for (QListWidgetItem *item : context_menu_items_) {
687     AlbumItem *album_item = static_cast<AlbumItem*>(item);
688     quint64 id = cover_fetcher_->FetchAlbumCover(album_item->data(Role_AlbumArtist).toString(), album_item->data(Role_Album).toString(), QString(), false);
689     cover_fetching_tasks_[id] = album_item;
690     jobs_++;
691   }
692 
693   progress_bar_->setMaximum(jobs_);
694   progress_bar_->show();
695   abort_progress_->show();
696   UpdateStatusText();
697 
698 }
699 
UpdateCoverInList(AlbumItem * item,const QUrl & cover_url)700 void AlbumCoverManager::UpdateCoverInList(AlbumItem *item, const QUrl &cover_url) {
701 
702   quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, QUrl(), cover_url);
703   item->setData(Role_PathManual, cover_url);
704   cover_loading_tasks_[id] = item;
705 
706 }
707 
LoadCoverFromFile()708 void AlbumCoverManager::LoadCoverFromFile() {
709 
710   Song song = GetSingleSelectionAsSong();
711   if (!song.is_valid()) return;
712 
713   AlbumCoverImageResult result = album_cover_choice_controller_->LoadImageFromFile(&song);
714   if (!result.image.isNull()) {
715     SaveImageToAlbums(&song, result);
716   }
717 
718 }
719 
SaveCoverToFile()720 void AlbumCoverManager::SaveCoverToFile() {
721 
722   Song song = GetSingleSelectionAsSong();
723   if (!song.is_valid() || song.has_manually_unset_cover()) return;
724 
725   AlbumCoverImageResult result;
726 
727   // Load the image from disk
728 
729   if (!song.art_manual().isEmpty() && !song.has_manually_unset_cover() && song.art_manual().isLocalFile() && QFile::exists(song.art_manual().toLocalFile())) {
730     result.image_data = Utilities::ReadDataFromFile(song.art_manual().toLocalFile());
731   }
732   else if (!song.art_manual().isEmpty() && !song.art_manual().path().isEmpty() && song.art_manual().scheme().isEmpty() && QFile::exists(song.art_manual().path())) {
733     result.image_data = Utilities::ReadDataFromFile(song.art_manual().path());
734   }
735   else if (song.has_embedded_cover()) {
736     result.image_data = TagReaderClient::Instance()->LoadEmbeddedArtBlocking(song.url().toLocalFile());
737   }
738   else if (!song.art_automatic().isEmpty() && song.art_automatic().isLocalFile() && QFile::exists(song.art_automatic().toLocalFile())) {
739     result.image_data = Utilities::ReadDataFromFile(song.art_automatic().toLocalFile());
740   }
741   else if (!song.art_automatic().isEmpty() && !song.art_automatic().path().isEmpty() && song.art_automatic().scheme().isEmpty() && QFile::exists(song.art_automatic().path())) {
742     result.image_data = Utilities::ReadDataFromFile(song.art_automatic().path());
743   }
744 
745   if (!result.is_valid()) return;
746 
747   result.mime_type = Utilities::MimeTypeFromData(result.image_data);
748 
749   if (!result.image_data.isEmpty()) {
750     result.image.loadFromData(result.image_data);
751   }
752 
753   album_cover_choice_controller_->SaveCoverToFileManual(song, result);
754 
755 }
756 
LoadCoverFromURL()757 void AlbumCoverManager::LoadCoverFromURL() {
758 
759   Song song = GetSingleSelectionAsSong();
760   if (!song.is_valid()) return;
761 
762   AlbumCoverImageResult result = album_cover_choice_controller_->LoadImageFromURL();
763   if (result.is_valid()) {
764     SaveImageToAlbums(&song, result);
765   }
766 
767 }
768 
SearchForCover()769 void AlbumCoverManager::SearchForCover() {
770 
771   Song song = GetFirstSelectedAsSong();
772   if (!song.is_valid()) return;
773 
774   AlbumCoverImageResult result = album_cover_choice_controller_->SearchForImage(&song);
775   if (result.is_valid()) {
776     SaveImageToAlbums(&song, result);
777   }
778 
779 }
780 
SaveImageToAlbums(Song * song,const AlbumCoverImageResult & result)781 void AlbumCoverManager::SaveImageToAlbums(Song *song, const AlbumCoverImageResult &result) {
782 
783   QUrl cover_url = result.cover_url;
784   switch (album_cover_choice_controller_->get_save_album_cover_type()) {
785     case CollectionSettingsPage::SaveCoverType_Cache:
786     case CollectionSettingsPage::SaveCoverType_Album:
787       if (cover_url.isEmpty() || !cover_url.isValid() || !cover_url.isLocalFile()) {
788         cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(song, result);
789       }
790       break;
791     case CollectionSettingsPage::SaveCoverType_Embedded:
792       cover_url = QUrl::fromLocalFile(Song::kEmbeddedCover);
793       break;
794   }
795 
796   // Force the found cover on all of the selected items
797   QList<QUrl> urls;
798   QList<AlbumItem*> album_items;
799   for (QListWidgetItem *item : context_menu_items_) {
800     AlbumItem *album_item = static_cast<AlbumItem*>(item);
801     switch (album_cover_choice_controller_->get_save_album_cover_type()) {
802       case CollectionSettingsPage::SaveCoverType_Cache:
803       case CollectionSettingsPage::SaveCoverType_Album:{
804         Song current_song = ItemAsSong(album_item);
805         album_cover_choice_controller_->SaveArtManualToSong(&current_song, cover_url);
806         UpdateCoverInList(album_item, cover_url);
807         break;
808       }
809       case CollectionSettingsPage::SaveCoverType_Embedded:{
810         urls << album_item->urls;
811         album_items << album_item;
812         break;
813       }
814     }
815   }
816 
817   if (album_cover_choice_controller_->get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Embedded && !urls.isEmpty()) {
818     quint64 id = -1;
819     if (result.is_jpeg()) {
820       id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data);
821     }
822     else {
823       id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image);
824     }
825     for (AlbumItem *album_item : album_items) {
826       cover_save_tasks_.insert(id, album_item);
827     }
828   }
829 
830 }
831 
UnsetCover()832 void AlbumCoverManager::UnsetCover() {
833 
834   Song song = GetFirstSelectedAsSong();
835   if (!song.is_valid()) return;
836 
837   AlbumItem *first_album_item = static_cast<AlbumItem*>(context_menu_items_[0]);
838 
839   QUrl cover_url = album_cover_choice_controller_->UnsetCover(&song);
840 
841   // Force the 'none' cover on all of the selected items
842   for (QListWidgetItem *item : context_menu_items_) {
843     AlbumItem *album_item = static_cast<AlbumItem*>(item);
844     album_item->setIcon(icon_nocover_item_);
845     album_item->setData(Role_PathManual, cover_url);
846 
847     // Don't save the first one twice
848     if (album_item != first_album_item) {
849       Song current_song = ItemAsSong(album_item);
850       album_cover_choice_controller_->SaveArtManualToSong(&current_song, cover_url);
851     }
852   }
853 
854 }
855 
ClearCover()856 void AlbumCoverManager::ClearCover() {
857 
858   Song song = GetFirstSelectedAsSong();
859   if (!song.is_valid()) return;
860 
861   AlbumItem *first_album_item = static_cast<AlbumItem*>(context_menu_items_[0]);
862 
863   album_cover_choice_controller_->ClearCover(&song);
864 
865   // Force the 'none' cover on all of the selected items
866   for (QListWidgetItem *item : context_menu_items_) {
867     AlbumItem *album_item = static_cast<AlbumItem*>(item);
868     album_item->setIcon(icon_nocover_item_);
869     album_item->setData(Role_PathManual, QUrl());
870 
871     // Don't save the first one twice
872     if (album_item != first_album_item) {
873       Song current_song = ItemAsSong(album_item);
874       album_cover_choice_controller_->SaveArtManualToSong(&current_song, QUrl(), false);
875     }
876   }
877 
878 }
879 
DeleteCover()880 void AlbumCoverManager::DeleteCover() {
881 
882   for (QListWidgetItem *item : context_menu_items_) {
883     AlbumItem *album_item = static_cast<AlbumItem*>(item);
884     Song song = ItemAsSong(album_item);
885     album_cover_choice_controller_->DeleteCover(&song);
886     album_item->setIcon(icon_nocover_item_);
887     album_item->setData(Role_PathManual, QUrl());
888     album_item->setData(Role_PathAutomatic, QUrl());
889   }
890 
891 }
892 
GetSongsInAlbum(const QModelIndex & idx) const893 SongList AlbumCoverManager::GetSongsInAlbum(const QModelIndex &idx) const {
894 
895   SongList ret;
896 
897   QMutexLocker l(collection_backend_->db()->Mutex());
898   QSqlDatabase db(collection_backend_->db()->Connect());
899 
900   CollectionQuery q(db, collection_backend_->songs_table(), collection_backend_->fts_table());
901   q.SetColumnSpec("ROWID," + Song::kColumnSpec);
902   q.AddWhere("album", idx.data(Role_Album).toString());
903   q.SetOrderBy("disc, track, title");
904 
905   QString albumartist = idx.data(Role_AlbumArtist).toString();
906   if (!albumartist.isEmpty()) {
907     q.AddWhere("effective_albumartist", albumartist);
908   }
909 
910   q.AddCompilationRequirement(albumartist.isEmpty());
911 
912   if (!q.Exec()) return ret;
913 
914   while (q.Next()) {
915     Song song;
916     song.InitFromQuery(q, true);
917     ret << song;
918   }
919   return ret;
920 
921 }
922 
GetSongsInAlbums(const QModelIndexList & indexes) const923 SongList AlbumCoverManager::GetSongsInAlbums(const QModelIndexList &indexes) const {
924 
925   SongList ret;
926   for (const QModelIndex &idx : indexes) {
927     ret << GetSongsInAlbum(idx);
928   }
929   return ret;
930 
931 }
932 
GetMimeDataForAlbums(const QModelIndexList & indexes) const933 SongMimeData *AlbumCoverManager::GetMimeDataForAlbums(const QModelIndexList &indexes) const {
934 
935   SongList songs = GetSongsInAlbums(indexes);
936   if (songs.isEmpty()) return nullptr;
937 
938   SongMimeData *mimedata = new SongMimeData;
939   mimedata->backend = collection_backend_;
940   mimedata->songs = songs;
941   return mimedata;
942 
943 }
944 
AlbumDoubleClicked(const QModelIndex & idx)945 void AlbumCoverManager::AlbumDoubleClicked(const QModelIndex &idx) {
946 
947   AlbumItem *item = static_cast<AlbumItem*>(idx.internalPointer());
948   if (!item) return;
949   album_cover_choice_controller_->ShowCover(ItemAsSong(item));
950 
951 }
952 
AddSelectedToPlaylist()953 void AlbumCoverManager::AddSelectedToPlaylist() {
954   emit AddToPlaylist(GetMimeDataForAlbums(ui_->albums->selectionModel()->selectedIndexes()));
955 }
956 
LoadSelectedToPlaylist()957 void AlbumCoverManager::LoadSelectedToPlaylist() {
958 
959   SongMimeData *mimedata = GetMimeDataForAlbums(ui_->albums->selectionModel()->selectedIndexes());
960   if (mimedata) {
961     mimedata->clear_first_ = true;
962     emit AddToPlaylist(mimedata);
963   }
964 
965 }
966 
SaveAndSetCover(AlbumItem * item,const AlbumCoverImageResult & result)967 void AlbumCoverManager::SaveAndSetCover(AlbumItem *item, const AlbumCoverImageResult &result) {
968 
969   const QString albumartist = item->data(Role_AlbumArtist).toString();
970   const QString album = item->data(Role_Album).toString();
971   const QList<QUrl> &urls = item->urls;
972   const Song::FileType filetype = Song::FileType(item->data(Role_Filetype).toInt());
973   const bool has_cue = !item->data(Role_CuePath).toString().isEmpty();
974 
975   if (album_cover_choice_controller_->get_save_album_cover_type() == CollectionSettingsPage::SaveCoverType_Embedded && Song::save_embedded_cover_supported(filetype) && !has_cue) {
976     if (result.is_jpeg()) {
977       quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image_data);
978       cover_save_tasks_.insert(id, item);
979     }
980     else if (!result.image.isNull()) {
981       quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.image);
982       cover_save_tasks_.insert(id, item);
983     }
984     else if (!result.cover_url.isEmpty() && result.cover_url.isLocalFile()) {
985       quint64 id = app_->album_cover_loader()->SaveEmbeddedCoverAsync(urls, result.cover_url.toLocalFile());
986       cover_save_tasks_.insert(id, item);
987     }
988   }
989   else {
990     QUrl cover_url;
991     if (!result.cover_url.isEmpty() && result.cover_url.isValid() && result.cover_url.isLocalFile()) {
992       cover_url = result.cover_url;
993     }
994     else if (!result.image_data.isEmpty() || !result.image.isNull()) {
995       cover_url = album_cover_choice_controller_->SaveCoverToFileAutomatic(Song::Source_Collection, albumartist, album, QString(), urls.first().adjusted(QUrl::RemoveFilename).path(), result, false);
996     }
997 
998     if (cover_url.isEmpty()) return;
999 
1000     // Save the image in the database
1001     collection_backend_->UpdateManualAlbumArtAsync(albumartist, album, cover_url);
1002 
1003     // Update the icon in our list
1004     UpdateCoverInList(item, cover_url);
1005   }
1006 
1007 }
1008 
ExportCovers()1009 void AlbumCoverManager::ExportCovers() {
1010 
1011   AlbumCoverExport::DialogResult result = cover_export_->Exec();
1012 
1013   if (result.cancelled_) {
1014     return;
1015   }
1016 
1017   DisableCoversButtons();
1018 
1019   cover_exporter_->SetDialogResult(result);
1020 
1021   for (int i = 0; i < ui_->albums->count(); ++i) {
1022     AlbumItem *item = static_cast<AlbumItem*>(ui_->albums->item(i));
1023 
1024     // skip hidden and coverless albums
1025     if (item->isHidden() || !ItemHasCover(*item)) {
1026       continue;
1027     }
1028 
1029     cover_exporter_->AddExportRequest(ItemAsSong(item));
1030   }
1031 
1032   if (cover_exporter_->request_count() > 0) {
1033     progress_bar_->setMaximum(cover_exporter_->request_count());
1034     progress_bar_->show();
1035     abort_progress_->show();
1036 
1037     cover_exporter_->StartExporting();
1038   }
1039   else {
1040     QMessageBox msg;
1041     msg.setWindowTitle(tr("Export finished"));
1042     msg.setText(tr("No covers to export."));
1043     msg.exec();
1044   }
1045 
1046 }
1047 
UpdateExportStatus(const int exported,const int skipped,const int max)1048 void AlbumCoverManager::UpdateExportStatus(const int exported, const int skipped, const int max) {
1049 
1050   progress_bar_->setValue(exported);
1051 
1052   QString message = tr("Exported %1 covers out of %2 (%3 skipped)")
1053                         .arg(exported)
1054                         .arg(max)
1055                         .arg(skipped);
1056   statusBar()->showMessage(message);
1057 
1058   // End of the current process
1059   if (exported + skipped >= max) {
1060     QTimer::singleShot(2000, statusBar(), &QStatusBar::clearMessage);
1061 
1062     progress_bar_->hide();
1063     abort_progress_->hide();
1064     EnableCoversButtons();
1065 
1066     QMessageBox msg;
1067     msg.setWindowTitle(tr("Export finished"));
1068     msg.setText(message);
1069     msg.exec();
1070   }
1071 
1072 }
1073 
ItemHasCover(const AlbumItem & item) const1074 bool AlbumCoverManager::ItemHasCover(const AlbumItem &item) const {
1075   return item.icon().cacheKey() != icon_nocover_item_.cacheKey();
1076 }
1077 
SaveEmbeddedCoverAsyncFinished(quint64 id,const bool success)1078 void AlbumCoverManager::SaveEmbeddedCoverAsyncFinished(quint64 id, const bool success) {
1079 
1080   while (cover_save_tasks_.contains(id)) {
1081     AlbumItem *album_item = cover_save_tasks_.take(id);
1082     if (!success) continue;
1083     album_item->setData(Role_PathAutomatic, QUrl::fromLocalFile(Song::kEmbeddedCover));
1084     Song song = ItemAsSong(album_item);
1085     album_cover_choice_controller_->SaveArtAutomaticToSong(&song, QUrl::fromLocalFile(Song::kEmbeddedCover));
1086     quint64 cover_load_id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options_, album_item->data(Role_PathAutomatic).toUrl(), album_item->data(Role_PathManual).toUrl(), album_item->urls.first());
1087     cover_loading_tasks_[cover_load_id] = album_item;
1088   }
1089 
1090 }
1091