on b09b5b4 help sign in
unity8/plugins/Utils/albumartprovider.cpp
236 lines 8.0 kB history raw
1/*
2 * Copyright 2013 Canonical Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authors: Jussi Pakkanen <jussi.pakkanen@canonical.com>
17 * Pawel Stolowski <pawel.stolowski@canonical.com>
18*/
20#include "albumartprovider.h"
21#include <QString>
22#include <QNetworkAccessManager>
23#include <QEventLoop>
24#include <QNetworkRequest>
25#include <QNetworkReply>
26#include <QXmlQuery>
27#include <QTemporaryFile>
28#include <QBuffer>
29#include <QDebug>
30#include <QFile>
31#include <QStringList>
32#include <QByteArray>
33#include <QImage>
34#include <QUrl>
35#include <QUrlQuery>
36#include <QtConcurrent>
38using namespace std;
40const std::string AlbumArtProvider::DEFAULT_ALBUM_ART = "/usr/share/unity/icons/album_missing.png";
41const std::string AlbumArtProvider::UNITY_LENS_SCHEMA = "com.canonical.Unity.Lenses";
43static QByteArray download(QString url) {
44 QScopedPointer<QNetworkAccessManager> am(new QNetworkAccessManager);
45 QNetworkReply *reply = am->get(QNetworkRequest(QUrl(url)));
46 QEventLoop loop;
47 QObject::connect(am.data(), &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
48 loop.exec();
50 if (reply->error() != QNetworkReply::NoError) {
51 qWarning() << "Error downloading from the network:" << reply->errorString();
52 return QByteArray();
53 }
54 return reply->readAll();
57AlbumArtProvider::AlbumArtProvider()
58 : QQuickImageProvider(QQmlImageProviderBase::Image, QQmlImageProviderBase::ForceAsynchronousImageLoading),
59 m_settings(nullptr)
61 auto schemas = g_settings_list_schemas();
62 if (schemas) {
63 for (int i = 0; schemas[i]; i++) {
64 if (g_strcmp0(schemas[i], UNITY_LENS_SCHEMA.c_str()) == 0) {
65 m_settings = g_settings_new(UNITY_LENS_SCHEMA.c_str());
66 break;
67 }
68 }
69 }
70 if (m_settings == nullptr) {
71 qWarning() << "Missing" << QString::fromStdString(UNITY_LENS_SCHEMA) << "schema";
72 }
75AlbumArtProvider::~AlbumArtProvider()
77 if (m_settings)
78 g_object_unref(m_settings);
81std::string AlbumArtProvider::get_lastfm_url(const albuminfo &ai) {
82 QString artist = QString::fromStdString(ai.artist);
83 QString album = QString::fromStdString(ai.album);
84 // audioscrobbler wants it without any / (as it would break the URL)
85 artist.remove('/');
86 album.remove('/');
88 /// @todo: this is the old API which will probably get axed at some point in the future
89 /// The new 2.0 API requires an API key, but supports JSON output, etc, so switching
90 /// to it should be done ASAP.
91 QString request = QString("http://ws.audioscrobbler.com/1.0/album/%1/%2/info.xml").arg(artist)
92 .arg(album)
93 .toHtmlEscaped();
94 QFuture<QByteArray> future = QtConcurrent::run(download, request);
95 QByteArray arr = future.result();
96 if(arr.size() == 0) {
97 return "";
98 }
99 QXmlQuery query;
100 QBuffer tmp;
101 tmp.setData(arr);
102 tmp.open(QIODevice::ReadOnly);
103 query.bindVariable("reply", &tmp);
104 query.setQuery("doc($reply)/album/coverart/large[1]/text()");
105 QString image;
106 query.evaluateTo(&image);
107 image = image.trimmed();
109 // return empty if last.fm tries to offer their default
110 // fallback artwork
111 if (image.contains("catalogue/noimage")) {
112 return "";
113 }
115 return image.toStdString();
118bool AlbumArtProvider::download_and_store(const std::string &image_url, const std::string &output_file) {
119 QString url = QString::fromStdString(image_url);
120 QString fileName = QString::fromStdString(output_file);
121 QFuture<QByteArray> future = QtConcurrent::run(download, url);
122 QByteArray arr = future.result();
123 if (arr.size() == 0) {
124 return false;
125 }
126 QFile file(fileName);
127 if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
128 qWarning() << "Could not open file for writing:" << file.errorString();
129 return false;
130 }
131 if (file.write(arr) == -1){
132 qWarning() << "Could not write the image:" << file.error();
133 return false;
134 }
136 return true;
139void AlbumArtProvider::fix_format(const std::string &fname) {
140 // MediaArtSpec requires jpg. Convert to it if necessary.
141 FILE *f = fopen(fname.c_str(), "r");
142 if(!f)
143 return;
144 unsigned char buf[2];
145 fread(buf, 1, 2, f);
146 fclose(f);
147 if(buf[0] == 0xff && buf[1] == 0xd8) {
148 return;
149 }
150 QImage im(fname.c_str());
151 im.save(fname.c_str(), "JPEG");
154std::string AlbumArtProvider::get_image(const std::string &artist, const std::string &album) {
155 albuminfo info;
157 info.artist = artist;
158 info.album = album;
160 if(info.album.empty() || info.artist.empty()) {
161 return DEFAULT_ALBUM_ART;
162 }
163 if(cache.has_art(info.artist, info.album)) {
164 // Image may have expired from cache between these two lines.
165 // It might expire before we return. C'est la vie.
166 return cache.get_art_file(info.artist, info.album);
167 }
169 if (m_settings != nullptr && g_strcmp0(g_settings_get_string(m_settings, "remote-content-search"), "all") !=0) {
170 qDebug() << "Remote content disabled";
171 return DEFAULT_ALBUM_ART;
172 }
174 std::string image_url = get_lastfm_url(info);
175 if(image_url.empty()) {
176 return DEFAULT_ALBUM_ART;
177 }
178 QTemporaryFile tempFile;
179 tempFile.open();
180 tempFile.setAutoRemove(true);
181 QString fname = tempFile.fileName();
182 std::string tmpname = fname.toUtf8().data();
184 if(!download_and_store(image_url, tmpname)) {
185 return DEFAULT_ALBUM_ART;
186 }
187 fix_format(tmpname.c_str());
188 QFile f(fname);
189 if(!f.open(QIODevice::ReadWrite)) {
190 return DEFAULT_ALBUM_ART;
191 }
192 QByteArray arr = f.readAll();
193 f.close();
195 cache.add_art(info.artist, info.album, arr.data(), arr.size());
196 try {
197 std::string res = cache.get_art_file(info.artist, info.album);
198 if(res.empty())
199 return DEFAULT_ALBUM_ART;
200 return res;
201 } catch(...) {
202 }
203 return DEFAULT_ALBUM_ART;
206QImage AlbumArtProvider::requestImage(const QString &id, QSize *realSize, const QSize &requestedSize) {
207 Q_UNUSED(requestedSize)
209 QUrlQuery query(id);
210 if (!query.hasQueryItem(QStringLiteral("artist")) || !query.hasQueryItem(QStringLiteral("album"))) {
211 qWarning() << "Invalid albumart uri:" << id;
212 return QImage(QString::fromStdString(DEFAULT_ALBUM_ART));
213 }
214 const QString artist = query.queryItemValue(QStringLiteral("artist"), QUrl::FullyEncoded);
215 const QString album = query.queryItemValue(QStringLiteral("album"), QUrl::FullyEncoded);
217 std::string tgt_path;
218 try {
219 tgt_path = get_image(artist.toStdString(), album.toStdString());
220 if(!tgt_path.empty()) {
221 QImage image;
222 image.load(QString::fromStdString(tgt_path));
223 // FIXME: Rescale to requested size preserving aspect.
224 *realSize = image.size();
225 return image;
226 }
227 } catch(std::exception &e) {
228 qDebug() << "Album art loader failed: " << e.what();
229 } catch(...) {
230 qDebug() << "Unknown error when generating image.";
231 }
233 QImage fallback(QString::fromStdString(DEFAULT_ALBUM_ART));
234 *realSize = fallback.size();
235 return fallback;