1 /* This file is part of Clementine.
2    Copyright 2010-2013, David Sansome <me@davidsansome.com>
3    Copyright 2010-2012, 2014, John Maguire <john.maguire@gmail.com>
4    Copyright 2011, Paweł Bara <keirangtp@gmail.com>
5    Copyright 2014, Andreas <asfa194@gmail.com>
6    Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
7 
8    Clementine is free software: you can redistribute it and/or modify
9    it under the terms of the GNU General Public License as published by
10    the Free Software Foundation, either version 3 of the License, or
11    (at your option) any later version.
12 
13    Clementine is distributed in the hope that it will be useful,
14    but WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16    GNU General Public License for more details.
17 
18    You should have received a copy of the GNU General Public License
19    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
20 */
21 
22 #include "albumcoverloader.h"
23 
24 #include <QPainter>
25 #include <QDir>
26 #include <QCoreApplication>
27 #include <QUrl>
28 #include <QNetworkReply>
29 
30 #include "config.h"
31 #include "core/closure.h"
32 #include "core/logging.h"
33 #include "core/network.h"
34 #include "core/tagreaderclient.h"
35 #include "core/utilities.h"
36 #include "internet/core/internetmodel.h"
37 #ifdef HAVE_SPOTIFY
38 #include "internet/spotify/spotifyservice.h"
39 #endif
40 
AlbumCoverLoader(QObject * parent)41 AlbumCoverLoader::AlbumCoverLoader(QObject* parent)
42     : QObject(parent),
43       stop_requested_(false),
44       next_id_(1),
45       network_(new NetworkAccessManager(this)),
46       connected_spotify_(false) {}
47 
ImageCacheDir()48 QString AlbumCoverLoader::ImageCacheDir() {
49   return Utilities::GetConfigPath(Utilities::Path_AlbumCovers);
50 }
51 
CancelTask(quint64 id)52 void AlbumCoverLoader::CancelTask(quint64 id) {
53   QMutexLocker l(&mutex_);
54   for (QQueue<Task>::iterator it = tasks_.begin(); it != tasks_.end(); ++it) {
55     if (it->id == id) {
56       tasks_.erase(it);
57       break;
58     }
59   }
60 }
61 
CancelTasks(const QSet<quint64> & ids)62 void AlbumCoverLoader::CancelTasks(const QSet<quint64>& ids) {
63   QMutexLocker l(&mutex_);
64   for (QQueue<Task>::iterator it = tasks_.begin(); it != tasks_.end();) {
65     if (ids.contains(it->id)) {
66       it = tasks_.erase(it);
67     } else {
68       ++it;
69     }
70   }
71 }
72 
LoadImageAsync(const AlbumCoverLoaderOptions & options,const QString & art_automatic,const QString & art_manual,const QString & song_filename,const QImage & embedded_image)73 quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions& options,
74                                          const QString& art_automatic,
75                                          const QString& art_manual,
76                                          const QString& song_filename,
77                                          const QImage& embedded_image) {
78   Task task;
79   task.options = options;
80   task.art_automatic = art_automatic;
81   task.art_manual = art_manual;
82   task.song_filename = song_filename;
83   task.embedded_image = embedded_image;
84   task.state = State_TryingManual;
85 
86   {
87     QMutexLocker l(&mutex_);
88     task.id = next_id_++;
89     tasks_.enqueue(task);
90   }
91 
92   metaObject()->invokeMethod(this, "ProcessTasks", Qt::QueuedConnection);
93 
94   return task.id;
95 }
96 
ProcessTasks()97 void AlbumCoverLoader::ProcessTasks() {
98   while (!stop_requested_) {
99     // Get the next task
100     Task task;
101     {
102       QMutexLocker l(&mutex_);
103       if (tasks_.isEmpty()) return;
104       task = tasks_.dequeue();
105     }
106 
107     ProcessTask(&task);
108   }
109 }
110 
ProcessTask(Task * task)111 void AlbumCoverLoader::ProcessTask(Task* task) {
112   TryLoadResult result = TryLoadImage(*task);
113   if (result.started_async) {
114     // The image is being loaded from a remote URL, we'll carry on later
115     // when it's done
116     return;
117   }
118 
119   if (result.loaded_success) {
120     QImage scaled = ScaleAndPad(task->options, result.image);
121     emit ImageLoaded(task->id, scaled);
122     emit ImageLoaded(task->id, scaled, result.image);
123     return;
124   }
125 
126   NextState(task);
127 }
128 
NextState(Task * task)129 void AlbumCoverLoader::NextState(Task* task) {
130   if (task->state == State_TryingManual) {
131     // Try the automatic one next
132     task->state = State_TryingAuto;
133     ProcessTask(task);
134   } else {
135     // Give up
136     emit ImageLoaded(task->id, task->options.default_output_image_);
137     emit ImageLoaded(task->id, task->options.default_output_image_,
138                      task->options.default_output_image_);
139   }
140 }
141 
TryLoadImage(const Task & task)142 AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(
143     const Task& task) {
144   // An image embedded in the song itself takes priority
145   if (!task.embedded_image.isNull())
146     return TryLoadResult(false, true,
147                          ScaleAndPad(task.options, task.embedded_image));
148 
149   QString filename;
150   switch (task.state) {
151     case State_TryingAuto:
152       filename = task.art_automatic;
153       break;
154     case State_TryingManual:
155       filename = task.art_manual;
156       break;
157   }
158 
159   if (filename == Song::kManuallyUnsetCover)
160     return TryLoadResult(false, true, task.options.default_output_image_);
161 
162   if (filename == Song::kEmbeddedCover && !task.song_filename.isEmpty()) {
163     const QImage taglib_image =
164         TagReaderClient::Instance()->LoadEmbeddedArtBlocking(
165             task.song_filename);
166 
167     if (!taglib_image.isNull())
168       return TryLoadResult(false, true,
169                            ScaleAndPad(task.options, taglib_image));
170   }
171 
172   if (filename.toLower().startsWith("http://") ||
173       filename.toLower().startsWith("https://")) {
174     QUrl url(filename);
175     QNetworkReply* reply = network_->get(QNetworkRequest(url));
176     NewClosure(reply, SIGNAL(finished()), this,
177                SLOT(RemoteFetchFinished(QNetworkReply*)), reply);
178 
179     remote_tasks_.insert(reply, task);
180     return TryLoadResult(true, false, QImage());
181   }
182 #ifdef HAVE_SPOTIFY
183   else if (filename.toLower().startsWith("spotify://image/")) {
184     // HACK: we should add generic image URL handlers
185     SpotifyService* spotify = InternetModel::Service<SpotifyService>();
186 
187     if (!connected_spotify_) {
188       connect(spotify, SIGNAL(ImageLoaded(QString, QImage)),
189               SLOT(SpotifyImageLoaded(QString, QImage)));
190       connected_spotify_ = true;
191     }
192 
193     QString id = QUrl(filename).path();
194     if (id.startsWith('/')) {
195       id.remove(0, 1);
196     }
197     remote_spotify_tasks_.insert(id, task);
198 
199     // Need to schedule this in the spotify service's thread
200     QMetaObject::invokeMethod(spotify, "LoadImage", Qt::QueuedConnection,
201                               Q_ARG(QString, id));
202     return TryLoadResult(true, false, QImage());
203   }
204 #endif
205   else if (filename.isEmpty()) {
206     // Avoid "QFSFileEngine::open: No file name specified" messages if we know that the filename is empty
207     return TryLoadResult(false, false, task.options.default_output_image_);
208   }
209 
210   QImage image(filename);
211   return TryLoadResult(
212       false, !image.isNull(),
213       image.isNull() ? task.options.default_output_image_ : image);
214 }
215 
216 #ifdef HAVE_SPOTIFY
SpotifyImageLoaded(const QString & id,const QImage & image)217 void AlbumCoverLoader::SpotifyImageLoaded(const QString& id,
218                                           const QImage& image) {
219   if (!remote_spotify_tasks_.contains(id)) return;
220 
221   Task task = remote_spotify_tasks_.take(id);
222   QImage scaled = ScaleAndPad(task.options, image);
223   emit ImageLoaded(task.id, scaled);
224   emit ImageLoaded(task.id, scaled, image);
225 }
226 #endif
227 
RemoteFetchFinished(QNetworkReply * reply)228 void AlbumCoverLoader::RemoteFetchFinished(QNetworkReply* reply) {
229   reply->deleteLater();
230 
231   Task task = remote_tasks_.take(reply);
232 
233   // Handle redirects.
234   QVariant redirect =
235       reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
236   if (redirect.isValid()) {
237     if (++task.redirects > kMaxRedirects) {
238       return;  // Give up.
239     }
240     QNetworkRequest request = reply->request();
241     request.setUrl(redirect.toUrl());
242     QNetworkReply* redirected_reply = network_->get(request);
243     NewClosure(redirected_reply, SIGNAL(finished()), this,
244                SLOT(RemoteFetchFinished(QNetworkReply*)), redirected_reply);
245 
246     remote_tasks_.insert(redirected_reply, task);
247     return;
248   }
249 
250   if (reply->error() == QNetworkReply::NoError) {
251     // Try to load the image
252     QImage image;
253     if (image.load(reply, 0)) {
254       QImage scaled = ScaleAndPad(task.options, image);
255       emit ImageLoaded(task.id, scaled);
256       emit ImageLoaded(task.id, scaled, image);
257       return;
258     }
259   }
260 
261   NextState(&task);
262 }
263 
ScaleAndPad(const AlbumCoverLoaderOptions & options,const QImage & image)264 QImage AlbumCoverLoader::ScaleAndPad(const AlbumCoverLoaderOptions& options,
265                                      const QImage& image) {
266   if (image.isNull()) return image;
267 
268   // Scale the image down
269   QImage copy;
270   if (options.scale_output_image_) {
271     copy = image.scaled(QSize(options.desired_height_, options.desired_height_),
272                         Qt::KeepAspectRatio, Qt::SmoothTransformation);
273   } else {
274     copy = image;
275   }
276 
277   if (!options.pad_output_image_) return copy;
278 
279   // Pad the image to height_ x height_
280   QImage padded_image(options.desired_height_, options.desired_height_,
281                       QImage::Format_ARGB32);
282   padded_image.fill(0);
283 
284   QPainter p(&padded_image);
285   p.drawImage((options.desired_height_ - copy.width()) / 2,
286               (options.desired_height_ - copy.height()) / 2, copy);
287   p.end();
288 
289   return padded_image;
290 }
291 
TryLoadPixmap(const QString & automatic,const QString & manual,const QString & filename)292 QPixmap AlbumCoverLoader::TryLoadPixmap(const QString& automatic,
293                                         const QString& manual,
294                                         const QString& filename) {
295   QPixmap ret;
296   if (manual == Song::kManuallyUnsetCover) return ret;
297   if (!manual.isEmpty()) ret.load(manual);
298   if (ret.isNull()) {
299     if (automatic == Song::kEmbeddedCover && !filename.isNull())
300       ret = QPixmap::fromImage(
301           TagReaderClient::Instance()->LoadEmbeddedArtBlocking(filename));
302     else if (!automatic.isEmpty())
303       ret.load(automatic);
304   }
305   return ret;
306 }
307 
LoadImageAsync(const AlbumCoverLoaderOptions & options,const Song & song)308 quint64 AlbumCoverLoader::LoadImageAsync(const AlbumCoverLoaderOptions& options,
309                                          const Song& song) {
310   return LoadImageAsync(options, song.art_automatic(), song.art_manual(),
311                         song.url().toLocalFile(), song.image());
312 }
313