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