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(¤t_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(¤t_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(¤t_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