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