@@ -0,0 +1,275 @@
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+*/
19+
20+#include "albumartprovider.h"
21+#include <QString>
22+#include <QNetworkAccessManager>
23+#include <QEventLoop>
24+#include <QNetworkRequest>
25+#include <QNetworkReply>
26+#include <QXmlQuery>
27+#include <QBuffer>
28+#include<cstdio>
29+#include<QDebug>
30+#include<QFile>
31+#include<QByteArray>
32+#include<gst/gst.h>
33+#include<unistd.h>
34+#include<memory>
35+#include<QImage>
36+
37+using namespace std;
38+
39+static void on_new_pad (GstElement * /*dec*/, GstPad * pad, GstElement * fakesink) {
40+ GstPad *sinkpad;
41+
42+ sinkpad = gst_element_get_static_pad (fakesink, "sink");
43+ if (!gst_pad_is_linked (sinkpad)) {
44+ if (gst_pad_link (pad, sinkpad) != GST_PAD_LINK_OK)
45+ g_error ("Failed to link pads!");
46+ }
47+ gst_object_unref (sinkpad);
48+}
49+
50+static void process_one_tag (const GstTagList * list, const gchar * tag, gpointer user_data) {
51+ struct albuminfo *md = (struct albuminfo*) user_data;
52+ int i, num;
53+ string tagname(tag);
54+
55+ num = gst_tag_list_get_tag_size (list, tag);
56+ for (i = 0; i < num; ++i) {
57+ gchar *val;
58+ if (gst_tag_get_type(tag) == G_TYPE_STRING) {
59+ if (gst_tag_list_get_string_index (list, tag, i, &val)) {
60+ if(tagname == "artist")
61+ md->artist = std::string(val);
62+ if(tagname == "album")
63+ md->album = std::string(val);
64+ }}
65+ }
66+}
67+
68+albuminfo AlbumArtProvider::get_album_info(const std::string &filename) throw (runtime_error)
69+{
70+ albuminfo ai;
71+ // FIXME: Need to do quoting. Files with %'s in their names seem to confuse gstreamer.
72+
73+ const string uri = "file://" + filename;
74+
75+ GstElement *dec = gst_element_factory_make ("uridecodebin", NULL);
76+ if (dec == nullptr)
77+ throw runtime_error("Failed to create uridecodebin element");
78+
79+ GstElement *pipe = gst_pipeline_new ("pipeline");
80+
81+ g_object_set (dec, "uri", uri.c_str(), NULL);
82+ gst_bin_add (GST_BIN (pipe), dec);
83+
84+ GstElement *sink = gst_element_factory_make ("fakesink", NULL);
85+ gst_bin_add (GST_BIN (pipe), sink);
86+ g_signal_connect (dec, "pad-added", G_CALLBACK (on_new_pad), sink);
87+
88+ gst_element_set_state (pipe, GST_STATE_PAUSED);
89+
90+ GstMessage *msg;
91+ while (true) {
92+ GstTagList *tags = NULL;
93+
94+ msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS (pipe),
95+ GST_CLOCK_TIME_NONE, (GstMessageType) (GST_MESSAGE_ASYNC_DONE | GST_MESSAGE_TAG | GST_MESSAGE_ERROR));
96+
97+ if (GST_MESSAGE_TYPE (msg) != GST_MESSAGE_TAG) /* error or async_done */
98+ break;
99+
100+ gst_message_parse_tag (msg, &tags);
101+ gst_tag_list_foreach (tags, process_one_tag, &ai);
102+ gst_tag_list_unref (tags);
103+
104+ gst_message_unref (msg);
105+ };
106+
107+ gst_element_set_state (pipe, GST_STATE_NULL);
108+ gst_object_unref (pipe);
109+
110+ if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
111+ GError *err;
112+ gchar *dbg_info;
113+ gst_message_parse_error(msg, &err, &dbg_info);
114+ string errortxt(err->message);
115+ string dbtxt(dbg_info);
116+ g_error_free(err);
117+ g_free(dbg_info);
118+
119+ gst_message_unref (msg);
120+ string msg = "Extracting metadata of file ";
121+ msg += filename;
122+ msg += " failed: ";
123+ msg += errortxt;
124+ msg += " ";
125+ msg += dbg_info;
126+ throw runtime_error(msg);
127+ }
128+
129+ gst_message_unref (msg);
130+
131+ return ai;
132+}
133+
134+std::string AlbumArtProvider::get_lastfm_url(const albuminfo &ai) {
135+ QString artist = QString::fromStdString(ai.artist);
136+ QString album = QString::fromStdString(ai.album);
137+
138+ /// @todo: this is the old API which will probably get axed at some point in the future
139+ /// The new 2.0 API requires an API key, but supports JSON output, etc, so switching
140+ /// to it should be done ASAP.
141+ QString request = QString("http://ws.audioscrobbler.com/1.0/album/%1/%2/info.xml").arg(artist)
142+ .arg(album)
143+ .toHtmlEscaped();
144+ QScopedPointer<QNetworkAccessManager> am(new QNetworkAccessManager);
145+ QNetworkReply *reply = am->get(QNetworkRequest(QUrl(request)));
146+ QEventLoop loop;
147+ QObject::connect(am.data(), &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
148+ loop.exec();
149+
150+ if (reply->error() != QNetworkReply::NoError) {
151+ qDebug() << "Error getting the XML:" << reply->errorString();
152+ return "";
153+ }
154+
155+ QXmlQuery query;
156+ QBuffer tmp;
157+ tmp.setData(reply->readAll());
158+ tmp.open(QIODevice::ReadOnly);
159+ query.bindVariable("reply", &tmp);
160+ query.setQuery("doc($reply)/album/coverart/large[1]/text()");
161+ QString image;
162+ query.evaluateTo(&image);
163+ image = image.trimmed();
164+
165+ return image.toStdString();
166+}
167+
168+bool AlbumArtProvider::download_and_store(const std::string &image_url, const std::string &output_file) {
169+ QString url = QString::fromStdString(image_url);
170+ QString fileName = QString::fromStdString(output_file);
171+
172+ QScopedPointer<QNetworkAccessManager> am(new QNetworkAccessManager);
173+ QNetworkReply *reply = am->get(QNetworkRequest(QUrl(url)));
174+ QEventLoop loop;
175+ QObject::connect(am.data(), &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
176+ loop.exec();
177+
178+ if (reply->error() != QNetworkReply::NoError) {
179+ qDebug() << "Error getting the image:" << reply->errorString();
180+ return false;
181+ }
182+
183+ QFile file(fileName);
184+ if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
185+ qDebug() << "Could not open file for writing:" << file.errorString();
186+ return false;
187+ }
188+ if (file.write(reply->readAll()) == -1){
189+ qDebug() << "Could not write the image:" << file.error();
190+ return false;
191+ }
192+
193+ return true;
194+}
195+
196+void AlbumArtProvider::fix_format(const std::string &fname) {
197+ // MediaArtSpec requires jpg. Convert to it if necessary.
198+ FILE *f = fopen(fname.c_str(), "r");
199+ if(!f)
200+ return;
201+ unsigned char buf[2];
202+ fread(buf, 1, 2, f);
203+ fclose(f);
204+ if(buf[0] == 0xff && buf[1] == 0xd8) {
205+ return;
206+ }
207+ QImage im(fname.c_str());
208+ im.save(fname.c_str(), "JPEG");
209+}
210+
211+std::string AlbumArtProvider::get_image(const std::string &filename) {
212+ albuminfo info;
213+ try {
214+ info = get_album_info(filename);
215+ } catch(std::exception &e) {
216+ qDebug() << "Could not determine album info: " << e.what();
217+ return "";
218+ }
219+ if(info.album.empty() || info.artist.empty()) {
220+ return "";
221+ }
222+ if(cache.has_art(info.artist, info.album)) {
223+ // Image may have expired from cache between these two lines.
224+ // It might expire before we return. C'est la vie.
225+ return cache.get_art_file(info.artist, info.album);
226+ }
227+ std::string image_url = get_lastfm_url(info);
228+ if(image_url.empty()) {
229+ return "";
230+ }
231+ char tmpname[] = "/tmp/path/to/some/file/somewhere/maybe.jpg";
232+ tmpnam(tmpname);
233+ //std::unique_ptr<char, int(*)(const char*)> deleter(tmpname, unlink);
234+ if(!download_and_store(image_url, tmpname)) {
235+ return "";
236+ }
237+ fix_format(tmpname);
238+ FILE *f = fopen(tmpname, "r");
239+ fseek(f, 0, SEEK_END);
240+ long s = ftell(f);
241+ fseek(f, 0, SEEK_SET);
242+ char *buf = new char[s];
243+ fread(buf, 1, s, f);
244+ fclose(f);
245+
246+ // Fixme, leaks buf if throws.
247+ cache.add_art(info.artist, info.album, buf, s);
248+ delete []buf;
249+ return cache.get_art_file(info.artist, info.album);
250+}
251+
252+QImage AlbumArtProvider::requestImage(const QString &id, QSize *realSize, const QSize &requestedSize) {
253+ Q_UNUSED(requestedSize)
254+
255+ std::string src_path(id.toUtf8().data());
256+ std::string tgt_path;
257+ try {
258+ tgt_path = get_image(src_path);
259+ if(!tgt_path.empty()) {
260+ QString tgt(tgt_path.c_str());
261+ QImage image;
262+ image.load(tgt);
263+ // FIXME: Rescale to requested size preserving aspect.
264+ *realSize = image.size();
265+ return image;
266+ }
267+ } catch(std::exception &e) {
268+ qDebug() << "Album art loader failed: " << e.what();
269+ } catch(...) {
270+ qDebug() << "Unknown error when generating image.";
271+ }
272+
273+ *realSize = QSize(0, 0);
274+ return QImage();
275+}