diff --git a/data/data.qrc b/data/data.qrc
index c64dea7c8..f3bbdad50 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -282,6 +282,7 @@
providers/mtvmusic.png
providers/cdbaby.png
providers/echonest.png
+ providers/songkick.png
providers/twitter.png
lumberjacksong.txt
schema/schema-18.sql
diff --git a/data/providers/songkick.png b/data/providers/songkick.png
new file mode 100644
index 000000000..d52946418
Binary files /dev/null and b/data/providers/songkick.png differ
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index ea273870e..af1ba2e28 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -276,6 +276,7 @@ set(SOURCES
songinfo/songinfosettingspage.cpp
songinfo/songinfotextview.cpp
songinfo/songinfoview.cpp
+ songinfo/songkickconcerts.cpp
songinfo/songplaystats.cpp
songinfo/twitterartistinfo.cpp
songinfo/ultimatelyricsprovider.cpp
@@ -529,6 +530,7 @@ set(HEADERS
songinfo/songinfosettingspage.h
songinfo/songinfotextview.h
songinfo/songinfoview.h
+ songinfo/songkickconcerts.h
songinfo/songplaystats.h
songinfo/twitterartistinfo.h
songinfo/ultimatelyricsprovider.h
diff --git a/src/songinfo/artistinfoview.cpp b/src/songinfo/artistinfoview.cpp
index c09c7b5cd..4fbef0e5e 100644
--- a/src/songinfo/artistinfoview.cpp
+++ b/src/songinfo/artistinfoview.cpp
@@ -19,6 +19,7 @@
#include "echonestbiographies.h"
#include "echonestimages.h"
#include "songinfofetcher.h"
+#include "songkickconcerts.h"
#include "twitterartistinfo.h"
#include "widgets/prettyimageview.h"
@@ -32,6 +33,7 @@ ArtistInfoView::ArtistInfoView(QWidget *parent)
{
fetcher_->AddProvider(new EchoNestBiographies);
fetcher_->AddProvider(new EchoNestImages);
+ fetcher_->AddProvider(new SongkickConcerts);
fetcher_->AddProvider(new TwitterArtistInfo);
#ifdef HAVE_LIBLASTFM
fetcher_->AddProvider(new EchoNestSimilarArtists);
diff --git a/src/songinfo/songinfotextview.cpp b/src/songinfo/songinfotextview.cpp
index 77c882b7c..385dcc79b 100644
--- a/src/songinfo/songinfotextview.cpp
+++ b/src/songinfo/songinfotextview.cpp
@@ -23,12 +23,15 @@
#include
#include
+#include "core/logging.h"
+
const qreal SongInfoTextView::kDefaultFontSize = 8.5;
const char* SongInfoTextView::kSettingsGroup = "SongInfo";
SongInfoTextView::SongInfoTextView(QWidget* parent)
: QTextBrowser(parent),
- last_width_(-1)
+ last_width_(-1),
+ recursion_filter_(false)
{
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@@ -88,3 +91,19 @@ void SongInfoTextView::SetHtml(const QString& html) {
setHtml(copy);
}
+
+// Prevents QTextDocument from trying to load remote images before they are
+// ready.
+QVariant SongInfoTextView::loadResource(int type, const QUrl& name) {
+ if (recursion_filter_) {
+ recursion_filter_ = false;
+ return QVariant();
+ }
+ recursion_filter_ = true;
+ if (type == QTextDocument::ImageResource && name.scheme() == "http") {
+ if (document()->resource(type, name).isNull()) {
+ return QVariant();
+ }
+ }
+ return QTextBrowser::loadResource(type, name);
+}
diff --git a/src/songinfo/songinfotextview.h b/src/songinfo/songinfotextview.h
index 902b74472..3b9fe7b7e 100644
--- a/src/songinfo/songinfotextview.h
+++ b/src/songinfo/songinfotextview.h
@@ -42,9 +42,11 @@ protected:
void resizeEvent(QResizeEvent* e);
void wheelEvent(QWheelEvent* e);
void contextMenuEvent(QContextMenuEvent* e);
+ QVariant loadResource(int type, const QUrl& name);
private:
int last_width_;
+ bool recursion_filter_;
};
#endif // SONGINFOTEXTVIEW_H
diff --git a/src/songinfo/songkickconcerts.cpp b/src/songinfo/songkickconcerts.cpp
new file mode 100644
index 000000000..1c8421819
--- /dev/null
+++ b/src/songinfo/songkickconcerts.cpp
@@ -0,0 +1,172 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ Clementine is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Clementine is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Clementine. If not, see .
+*/
+
+#include "songkickconcerts.h"
+
+#include
+#include
+
+#include
+
+#include
+
+#include "core/closure.h"
+#include "songinfotextview.h"
+
+const char* SongkickConcerts::kSongkickArtistBucket = "id:songkick";
+const char* SongkickConcerts::kSongkickArtistCalendarUrl =
+ "http://api.songkick.com/api/3.0/artists/%1/calendar.json?"
+ "per_page=5&"
+ "apikey=8rgKfy1WU6IlJFfN";
+
+SongkickConcerts::SongkickConcerts() {
+
+}
+
+void SongkickConcerts::FetchInfo(int id, const Song& metadata) {
+ Echonest::Artist::SearchParams params;
+ params.push_back(qMakePair(Echonest::Artist::Name, QVariant(metadata.artist())));
+ params.push_back(qMakePair(Echonest::Artist::IdSpace, QVariant(kSongkickArtistBucket)));
+ qLog(Debug) << "Params:" << params;
+ QNetworkReply* reply = Echonest::Artist::search(params);
+ qLog(Debug) << reply->request().url();
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistSearchFinished(QNetworkReply*, int)), reply, id);
+}
+
+void SongkickConcerts::ArtistSearchFinished(QNetworkReply* reply, int id) {
+ reply->deleteLater();
+ try {
+ Echonest::Artists artists = Echonest::Artist::parseSearch(reply);
+ if (artists.isEmpty()) {
+ qLog(Debug) << "Failed to find artist in echonest";
+ return;
+ }
+
+ const Echonest::Artist& artist = artists[0];
+ const Echonest::ForeignIds& foreign_ids = artist.foreignIds();
+ QString songkick_id;
+ foreach (const Echonest::ForeignId& id, foreign_ids) {
+ if (id.catalog == "songkick") {
+ songkick_id = id.foreign_id;
+ break;
+ }
+ }
+
+ if (songkick_id.isEmpty()) {
+ qLog(Debug) << "Failed to fetch songkick foreign id for artist";
+ return;
+ }
+
+ QStringList split = songkick_id.split(':');
+ if (split.count() != 3) {
+ qLog(Error) << "Weird songkick id";
+ return;
+ }
+
+ FetchSongkickCalendar(split[2], id);
+ } catch (Echonest::ParseError& e) {
+ qLog(Error) << "Error parsing echonest reply:" << e.errorType() << e.what();
+ }
+}
+
+void SongkickConcerts::FetchSongkickCalendar(const QString& artist_id, int id) {
+ QUrl url(QString(kSongkickArtistCalendarUrl).arg(artist_id));
+ qLog(Debug) << url;
+ QNetworkReply* reply = network_.get(QNetworkRequest(url));
+ NewClosure(reply, SIGNAL(finished()), this, SLOT(CalendarRequestFinished(QNetworkReply*, int)), reply, id);
+}
+
+void SongkickConcerts::CalendarRequestFinished(QNetworkReply* reply, int id) {
+ static const char* kStaticMapUrl =
+ "http://maps.googleapis.com/maps/api/staticmap"
+ "?key=AIzaSyDDJqmLOeE1mY_EBONhnQmdXbKtasgCtqg"
+ "&sensor=false"
+ "&size=100x100"
+ "&zoom=12"
+ "¢er=%1,%2"
+ "&markers=%1,%2";
+
+ QJson::Parser parser;
+ bool ok = false;
+ QVariant result = parser.parse(reply, &ok);
+
+ if (!ok) {
+ qLog(Error) << "Error parsing Songkick reply";
+ return;
+ }
+
+ QString html;
+ QXmlStreamWriter writer(&html);
+ SongInfoTextView* text_view = new SongInfoTextView;
+
+ QVariantMap root = result.toMap();
+ QVariantMap results_page = root["resultsPage"].toMap();
+ QVariantMap results = results_page["results"].toMap();
+ QVariantList events = results["event"].toList();
+ foreach (const QVariant& v, events) {
+ QVariantMap event = v.toMap();
+ {
+ writer.writeStartElement("div");
+ {
+ writer.writeStartElement("a");
+ writer.writeAttribute("href", event["uri"].toString());
+ writer.writeCharacters(event["displayName"].toString());
+ writer.writeEndElement();
+ }
+ QVariantMap venue = event["venue"].toMap();
+ if (venue["lng"].isValid() && venue["lat"].isValid()) {
+ writer.writeStartElement("img");
+ QString maps_url = QString(kStaticMapUrl).arg(
+ venue["lat"].toString(),
+ venue["lng"].toString());
+ writer.writeAttribute("src", maps_url);
+ writer.writeEndElement();
+
+ // QTextDocument does not support loading remote images, so we load
+ // them here and then inject them into the document later.
+ QNetworkRequest request(maps_url);
+ QNetworkReply* reply = network_.get(request);
+ NewClosure(reply, SIGNAL(finished()), this,
+ SLOT(InjectImage(QNetworkReply*, SongInfoTextView*)),
+ reply, text_view);
+ }
+ writer.writeEndElement();
+ }
+ }
+
+ CollapsibleInfoPane::Data data;
+ data.type_ = CollapsibleInfoPane::Data::Type_Biography;
+ data.id_ = QString("songkick/%1").arg(id);
+ data.title_ = tr("Upcoming Concerts");
+ data.icon_ = QIcon(":providers/songkick.png");
+
+ text_view->SetHtml(html);
+ data.contents_ = text_view;
+
+ emit InfoReady(id, data);
+ emit Finished(id);
+}
+
+void SongkickConcerts::InjectImage(
+ QNetworkReply* reply, SongInfoTextView* text_view) {
+ QImage image;
+ image.load(reply, "png");
+ text_view->document()->addResource(
+ QTextDocument::ImageResource,
+ reply->request().url(),
+ QVariant(image));
+}
diff --git a/src/songinfo/songkickconcerts.h b/src/songinfo/songkickconcerts.h
new file mode 100644
index 000000000..79206d488
--- /dev/null
+++ b/src/songinfo/songkickconcerts.h
@@ -0,0 +1,49 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ Clementine is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Clementine is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Clementine. If not, see .
+*/
+
+#ifndef SONGKICKCONCERTS_H
+#define SONGKICKCONCERTS_H
+
+#include "songinfoprovider.h"
+
+#include "core/network.h"
+
+class QNetworkReply;
+class SongInfoTextView;
+
+class SongkickConcerts : public SongInfoProvider {
+ Q_OBJECT
+
+ public:
+ SongkickConcerts();
+ void FetchInfo(int id, const Song& metadata);
+
+ private slots:
+ void ArtistSearchFinished(QNetworkReply* reply, int id);
+ void CalendarRequestFinished(QNetworkReply* reply, int id);
+ void InjectImage(QNetworkReply* reply, SongInfoTextView* text_view);
+
+ private:
+ void FetchSongkickCalendar(const QString& artist_id, int id);
+
+ NetworkAccessManager network_;
+
+ static const char* kSongkickArtistBucket;
+ static const char* kSongkickArtistCalendarUrl;
+};
+
+#endif