1 /* This file is part of Clementine.
2 
3    Copyright 2010, David Sansome <me@davidsansome.com>
4 
5    Clementine is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9 
10    Clementine is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14 
15    You should have received a copy of the GNU General Public License
16    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #include "core/application.h"
20 #include "core/logging.h"
21 #include "core/utilities.h"
22 #include "covers/albumcoverfetcher.h"
23 #include "covers/albumcoverloader.h"
24 #include "covers/currentartloader.h"
25 #include "library/librarybackend.h"
26 #include "ui/albumcoverchoicecontroller.h"
27 #include "ui/albumcovermanager.h"
28 #include "ui/albumcoversearcher.h"
29 #include "ui/coverfromurldialog.h"
30 #include "ui/iconloader.h"
31 
32 #include <QAction>
33 #include <QDesktopWidget>
34 #include <QDialog>
35 #include <QDragEnterEvent>
36 #include <QFileDialog>
37 #include <QImageWriter>
38 #include <QLabel>
39 #include <QList>
40 #include <QMenu>
41 #include <QUrl>
42 #include <QMimeData>
43 
44 const char* AlbumCoverChoiceController::kLoadImageFileFilter = QT_TR_NOOP(
45     "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
46 const char* AlbumCoverChoiceController::kSaveImageFileFilter =
47     QT_TR_NOOP("Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
48 const char* AlbumCoverChoiceController::kAllFilesFilter =
49     QT_TR_NOOP("All files (*)");
50 
51 QSet<QString>* AlbumCoverChoiceController::sImageExtensions = nullptr;
52 
AlbumCoverChoiceController(QWidget * parent)53 AlbumCoverChoiceController::AlbumCoverChoiceController(QWidget* parent)
54     : QWidget(parent),
55       app_(nullptr),
56       cover_searcher_(nullptr),
57       cover_fetcher_(nullptr),
58       save_file_dialog_(nullptr),
59       cover_from_url_dialog_(nullptr),
60       album_cover_popup_(nullptr) {
61   cover_from_file_ =
62       new QAction(IconLoader::Load("document-open", IconLoader::Base),
63                   tr("Load cover from disk..."), this);
64   cover_to_file_ =
65       new QAction(IconLoader::Load("document-save", IconLoader::Base),
66                   tr("Save cover to disk..."), this);
67   cover_from_url_ = new QAction(IconLoader::Load("download", IconLoader::Base),
68                                 tr("Load cover from URL..."), this);
69   search_for_cover_ =
70       new QAction(IconLoader::Load("edit-find", IconLoader::Base),
71                   tr("Search for album covers..."), this);
72   unset_cover_ = new QAction(IconLoader::Load("list-remove", IconLoader::Base),
73                              tr("Unset cover"), this);
74   show_cover_ = new QAction(IconLoader::Load("zoom-in", IconLoader::Base),
75                             tr("Show fullsize..."), this);
76 
77   search_cover_auto_ = new QAction(tr("Search automatically"), this);
78   search_cover_auto_->setCheckable(true);
79   search_cover_auto_->setChecked(false);
80 
81   separator_ = new QAction(this);
82   separator_->setSeparator(true);
83 }
84 
~AlbumCoverChoiceController()85 AlbumCoverChoiceController::~AlbumCoverChoiceController() {}
86 
SetApplication(Application * app)87 void AlbumCoverChoiceController::SetApplication(Application* app) {
88   app_ = app;
89 
90   cover_fetcher_ = new AlbumCoverFetcher(app_->cover_providers(), this);
91   cover_searcher_ = new AlbumCoverSearcher(QIcon(":/nocover.png"), app, this);
92   cover_searcher_->Init(cover_fetcher_);
93 
94   connect(cover_fetcher_,
95           SIGNAL(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)),
96           this,
97           SLOT(AlbumCoverFetched(quint64, QImage, CoverSearchStatistics)));
98 }
99 
GetAllActions()100 QList<QAction*> AlbumCoverChoiceController::GetAllActions() {
101   return QList<QAction*>() << cover_from_file_ << cover_to_file_ << separator_
102                            << cover_from_url_ << search_for_cover_
103                            << unset_cover_ << show_cover_;
104 }
105 
LoadCoverFromFile(Song * song)106 QString AlbumCoverChoiceController::LoadCoverFromFile(Song* song) {
107   QString cover = QFileDialog::getOpenFileName(
108       this, tr("Load cover from disk"),
109       GetInitialPathForFileDialog(*song, QString()),
110       tr(kLoadImageFileFilter) + ";;" + tr(kAllFilesFilter));
111 
112   if (cover.isNull()) return QString();
113 
114   // Can we load the image?
115   QImage image(cover);
116 
117   if (!image.isNull()) {
118     SaveCover(song, cover);
119     return cover;
120   } else {
121     return QString();
122   }
123 }
124 
SaveCoverToFile(const Song & song,const QImage & image)125 void AlbumCoverChoiceController::SaveCoverToFile(const Song& song,
126                                                  const QImage& image) {
127   QString initial_file_name =
128       "/" + (song.effective_album().isEmpty() ? tr("Unknown")
129                                               : song.effective_album()) +
130       ".jpg";
131 
132   QString save_filename = QFileDialog::getSaveFileName(
133       this, tr("Save album cover"),
134       GetInitialPathForFileDialog(song, initial_file_name),
135       tr(kSaveImageFileFilter) + ";;" + tr(kAllFilesFilter));
136 
137   if (save_filename.isNull()) return;
138 
139   QString extension = save_filename.right(4);
140   if (!extension.startsWith('.') ||
141       !QImageWriter::supportedImageFormats().contains(
142           extension.right(3).toUtf8())) {
143     save_filename.append(".jpg");
144   }
145 
146   image.save(save_filename);
147 }
148 
GetInitialPathForFileDialog(const Song & song,const QString & filename)149 QString AlbumCoverChoiceController::GetInitialPathForFileDialog(
150     const Song& song, const QString& filename) {
151   // art automatic is first to show user which cover the album may be
152   // using now; the song is using it if there's no manual path but we
153   // cannot use manual path here because it can contain cached paths
154   if (!song.art_automatic().isEmpty() && !song.has_embedded_cover()) {
155     return song.art_automatic();
156 
157     // if no automatic art, start in the song's folder
158   } else if (!song.url().isEmpty() && song.url().toLocalFile().contains('/')) {
159     return song.url().toLocalFile().section('/', 0, -2) + filename;
160 
161     // fallback - start in home
162   } else {
163     return QDir::home().absolutePath() + filename;
164   }
165 }
166 
LoadCoverFromURL(Song * song)167 QString AlbumCoverChoiceController::LoadCoverFromURL(Song* song) {
168   if (!cover_from_url_dialog_) {
169     cover_from_url_dialog_ = new CoverFromURLDialog(this);
170   }
171 
172   QImage image = cover_from_url_dialog_->Exec();
173 
174   if (!image.isNull()) {
175     QString cover = SaveCoverInCache(song->artist(), song->album(), image);
176     SaveCover(song, cover);
177 
178     return cover;
179   } else {
180     return QString();
181   }
182 }
183 
SearchForCover(Song * song)184 QString AlbumCoverChoiceController::SearchForCover(Song* song) {
185   // Get something sensible to stick in the search box.
186   // We search for the 'effective' values, but we cache the covers with the
187   // Song's artist() and album().
188   QImage image = cover_searcher_->Exec(song->effective_albumartist(),
189                                        song->effective_album());
190 
191   if (!image.isNull()) {
192     QString cover = SaveCoverInCache(song->artist(), song->album(), image);
193     SaveCover(song, cover);
194 
195     return cover;
196   } else {
197     return QString();
198   }
199 }
200 
UnsetCover(Song * song)201 QString AlbumCoverChoiceController::UnsetCover(Song* song) {
202   QString cover = Song::kManuallyUnsetCover;
203   SaveCover(song, cover);
204 
205   return cover;
206 }
207 
ToggleCover(const Song & song)208 bool AlbumCoverChoiceController::ToggleCover(const Song& song) {
209   if (album_cover_popup_ != nullptr) {
210     album_cover_popup_->accept();
211     album_cover_popup_ = nullptr;
212     return false;
213   }
214 
215   album_cover_popup_ = ShowCoverPrivate(song);
216 
217   // keep track of our window to prevent endless stacking
218   connect(album_cover_popup_, SIGNAL(finished(int)), this,
219           SLOT(AlbumCoverPopupClosed()));
220 
221   return true;
222 }
223 
ShowCover(const Song & song)224 void AlbumCoverChoiceController::ShowCover(const Song& song) {
225   ShowCoverPrivate(song);
226 }
227 
ShowCoverPrivate(const Song & song)228 QDialog* AlbumCoverChoiceController::ShowCoverPrivate(const Song& song) {
229   QDialog* dialog = new QDialog(this);
230   dialog->setAttribute(Qt::WA_DeleteOnClose, true);
231 
232   // Use (Album)Artist - Album as the window title
233   QString title_text(song.effective_albumartist());
234   if (!song.effective_album().isEmpty())
235     title_text += " - " + song.effective_album();
236 
237   QLabel* label = new QLabel(dialog);
238   label->setPixmap(AlbumCoverLoader::TryLoadPixmap(
239       song.art_automatic(), song.art_manual(), song.url().toLocalFile()));
240 
241   // add (WxHpx) to the title before possibly resizing
242   title_text += " (" + QString::number(label->pixmap()->width()) + "x" +
243                 QString::number(label->pixmap()->height()) + "px)";
244 
245   // if the cover is larger than the screen, resize the window
246   // 85% seems to be enough to account for title bar and taskbar etc.
247   QDesktopWidget desktop;
248   int current_screen = desktop.screenNumber(this);
249   int desktop_height = desktop.screenGeometry(current_screen).height();
250   int desktop_width = desktop.screenGeometry(current_screen).width();
251 
252   // resize differently if monitor is in portrait mode
253   if (desktop_width < desktop_height) {
254     const int new_width = (double)desktop_width * 0.95;
255     if (new_width < label->pixmap()->width()) {
256       label->setPixmap(
257           label->pixmap()->scaledToWidth(new_width, Qt::SmoothTransformation));
258     }
259   } else {
260     const int new_height = (double)desktop_height * 0.85;
261     if (new_height < label->pixmap()->height()) {
262       label->setPixmap(label->pixmap()->scaledToHeight(
263           new_height, Qt::SmoothTransformation));
264     }
265   }
266 
267   dialog->setWindowTitle(title_text);
268   dialog->setFixedSize(label->pixmap()->size());
269   dialog->show();
270 
271   return dialog;
272 }
273 
AlbumCoverPopupClosed()274 void AlbumCoverChoiceController::AlbumCoverPopupClosed() {
275   album_cover_popup_ = nullptr;
276 }
277 
SearchCoverAutomatically(const Song & song)278 void AlbumCoverChoiceController::SearchCoverAutomatically(const Song& song) {
279   qint64 id = cover_fetcher_->FetchAlbumCover(song.effective_albumartist(),
280                                               song.effective_album(), false);
281   cover_fetching_tasks_[id] = song;
282 }
283 
AlbumCoverFetched(quint64 id,const QImage & image,const CoverSearchStatistics & statistics)284 void AlbumCoverChoiceController::AlbumCoverFetched(
285     quint64 id, const QImage& image, const CoverSearchStatistics& statistics) {
286   Song song;
287   if (cover_fetching_tasks_.contains(id)) {
288     song = cover_fetching_tasks_.take(id);
289   }
290 
291   if (!image.isNull()) {
292     QString cover = SaveCoverInCache(song.artist(), song.album(), image);
293     SaveCover(&song, cover);
294   }
295 
296   emit AutomaticCoverSearchDone();
297 }
298 
SaveCover(Song * song,const QString & cover)299 void AlbumCoverChoiceController::SaveCover(Song* song, const QString& cover) {
300   if (song->is_valid() && song->id() != -1) {
301     song->set_art_manual(cover);
302     app_->library_backend()->UpdateManualAlbumArtAsync(
303         song->artist(), song->albumartist(), song->album(), cover);
304 
305     if (song->url() == app_->current_art_loader()->last_song().url()) {
306       app_->current_art_loader()->LoadArt(*song);
307     }
308   }
309 }
310 
SaveCoverInCache(const QString & artist,const QString & album,const QImage & image)311 QString AlbumCoverChoiceController::SaveCoverInCache(const QString& artist,
312                                                      const QString& album,
313                                                      const QImage& image) {
314   // Hash the artist and album into a filename for the image
315   QString filename(Utilities::Sha1CoverHash(artist, album).toHex() + ".jpg");
316   QString path(AlbumCoverLoader::ImageCacheDir() + "/" + filename);
317 
318   // Make sure this directory exists first
319   QDir dir;
320   dir.mkdir(AlbumCoverLoader::ImageCacheDir());
321 
322   // Save the image to disk
323   image.save(path, "JPG");
324 
325   return path;
326 }
327 
IsKnownImageExtension(const QString & suffix)328 bool AlbumCoverChoiceController::IsKnownImageExtension(const QString& suffix) {
329   if (!sImageExtensions) {
330     sImageExtensions = new QSet<QString>();
331     (*sImageExtensions) << "png"
332                         << "jpg"
333                         << "jpeg"
334                         << "bmp"
335                         << "gif"
336                         << "xpm"
337                         << "pbm"
338                         << "pgm"
339                         << "ppm"
340                         << "xbm";
341   }
342 
343   return sImageExtensions->contains(suffix);
344 }
345 
CanAcceptDrag(const QDragEnterEvent * e)346 bool AlbumCoverChoiceController::CanAcceptDrag(const QDragEnterEvent* e) {
347   for (const QUrl& url : e->mimeData()->urls()) {
348     const QString suffix = QFileInfo(url.toLocalFile()).suffix().toLower();
349     if (IsKnownImageExtension(suffix)) return true;
350   }
351   return e->mimeData()->hasImage();
352 }
353 
SaveCover(Song * song,const QDropEvent * e)354 QString AlbumCoverChoiceController::SaveCover(Song* song, const QDropEvent* e) {
355   for (const QUrl& url : e->mimeData()->urls()) {
356     const QString filename = url.toLocalFile();
357     const QString suffix = QFileInfo(filename).suffix().toLower();
358 
359     if (IsKnownImageExtension(suffix)) {
360       SaveCover(song, filename);
361       return filename;
362     }
363   }
364 
365   if (e->mimeData()->hasImage()) {
366     QImage image = qvariant_cast<QImage>(e->mimeData()->imageData());
367     if (!image.isNull()) {
368       QString cover_path =
369           SaveCoverInCache(song->artist(), song->album(), image);
370       SaveCover(song, cover_path);
371       return cover_path;
372     }
373   }
374 
375   return QString();
376 }
377