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