diff --git a/data/data.qrc b/data/data.qrc
index 29790c1cc..a51757c54 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -290,6 +290,7 @@
providers/skyfm.png
providers/somafm.png
providers/songkick.png
+ providers/soundcloud.png
providers/twitter.png
providers/wikipedia.png
sample.mood
diff --git a/data/providers/soundcloud.png b/data/providers/soundcloud.png
new file mode 100644
index 000000000..034972f2f
Binary files /dev/null and b/data/providers/soundcloud.png differ
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 94462ef34..6be27725a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -152,6 +152,7 @@ set(SOURCES
globalsearch/searchproviderstatuswidget.cpp
globalsearch/simplesearchprovider.cpp
globalsearch/somafmsearchprovider.cpp
+ globalsearch/soundcloudsearchprovider.cpp
globalsearch/suggestionwidget.cpp
globalsearch/urlsearchprovider.cpp
@@ -185,6 +186,7 @@ set(SOURCES
internet/searchboxwidget.cpp
internet/somafmservice.cpp
internet/somafmurlhandler.cpp
+ internet/soundcloudservice.cpp
library/groupbydialog.cpp
library/library.cpp
@@ -424,6 +426,7 @@ set(HEADERS
globalsearch/groovesharksearchprovider.h
globalsearch/searchprovider.h
globalsearch/simplesearchprovider.h
+ globalsearch/soundcloudsearchprovider.h
globalsearch/suggestionwidget.h
internet/digitallyimportedclient.h
@@ -452,6 +455,7 @@ set(HEADERS
internet/searchboxwidget.h
internet/somafmservice.h
internet/somafmurlhandler.h
+ internet/soundcloudservice.h
library/groupbydialog.h
library/library.h
diff --git a/src/globalsearch/soundcloudsearchprovider.cpp b/src/globalsearch/soundcloudsearchprovider.cpp
new file mode 100644
index 000000000..375bf8a92
--- /dev/null
+++ b/src/globalsearch/soundcloudsearchprovider.cpp
@@ -0,0 +1,95 @@
+/* This file is part of Clementine.
+ Copyright 2011, 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 "soundcloudsearchprovider.h"
+
+#include
+
+#include "core/application.h"
+#include "core/logging.h"
+#include "covers/albumcoverloader.h"
+#include "internet/soundcloudservice.h"
+
+SoundCloudSearchProvider::SoundCloudSearchProvider(Application* app, QObject* parent)
+ : SearchProvider(app, parent),
+ service_(NULL)
+{
+}
+
+void SoundCloudSearchProvider::Init(SoundCloudService* service) {
+ service_ = service;
+ SearchProvider::Init("SoundCloud", "soundcloud",
+ QIcon(":providers/soundcloud.png"),
+ WantsDelayedQueries | ArtIsProbablyRemote | CanShowConfig);
+
+ connect(service_, SIGNAL(SimpleSearchResults(int, SongList)),
+ SLOT(SearchDone(int, SongList)));
+ connect(service_, SIGNAL(AlbumSearchResult(int, QList)),
+ SLOT(AlbumSearchResult(int, QList)));
+ connect(service_, SIGNAL(AlbumSongsLoaded(quint64, SongList)),
+ SLOT(AlbumSongsLoaded(quint64, SongList)));
+
+ cover_loader_options_.desired_height_ = kArtHeight;
+ cover_loader_options_.pad_output_image_ = true;
+ cover_loader_options_.scale_output_image_ = true;
+
+ connect(app_->album_cover_loader(),
+ SIGNAL(ImageLoaded(quint64, QImage)),
+ SLOT(AlbumArtLoaded(quint64, QImage)));
+}
+
+void SoundCloudSearchProvider::SearchAsync(int id, const QString& query) {
+ const int service_id = service_->SimpleSearch(query);
+ pending_searches_[service_id] = PendingState(id, TokenizeQuery(query));;
+}
+
+void SoundCloudSearchProvider::SearchDone(int id, const SongList& songs) {
+ // Map back to the original id.
+ const PendingState state = pending_searches_.take(id);
+ const int global_search_id = state.orig_id_;
+
+ ResultList ret;
+ foreach (const Song& song, songs) {
+ Result result(this);
+ result.metadata_ = song;
+
+ ret << result;
+ }
+
+ emit ResultsAvailable(global_search_id, ret);
+ MaybeSearchFinished(global_search_id);
+}
+
+void SoundCloudSearchProvider::MaybeSearchFinished(int id) {
+ if (pending_searches_.keys(PendingState(id, QStringList())).isEmpty()) {
+ emit SearchFinished(id);
+ }
+}
+
+void SoundCloudSearchProvider::LoadArtAsync(int id, const Result& result) {
+ quint64 loader_id = app_->album_cover_loader()->LoadImageAsync(
+ cover_loader_options_, result.metadata_);
+ cover_loader_tasks_[loader_id] = id;
+}
+
+void SoundCloudSearchProvider::AlbumArtLoaded(quint64 id, const QImage& image) {
+ if (!cover_loader_tasks_.contains(id)) {
+ return;
+ }
+ int original_id = cover_loader_tasks_.take(id);
+ emit ArtLoaded(original_id, image);
+}
diff --git a/src/globalsearch/soundcloudsearchprovider.h b/src/globalsearch/soundcloudsearchprovider.h
new file mode 100644
index 000000000..7ef38a334
--- /dev/null
+++ b/src/globalsearch/soundcloudsearchprovider.h
@@ -0,0 +1,53 @@
+/* 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 SOUNDCLOUDSEARCHPROVIDER_H
+#define SOUNDCLOUDSEARCHPROVIDER_H
+
+#include "searchprovider.h"
+#include "covers/albumcoverloaderoptions.h"
+#include "internet/soundcloudservice.h"
+
+class AlbumCoverLoader;
+
+class SoundCloudSearchProvider : public SearchProvider {
+ Q_OBJECT
+
+ public:
+ explicit SoundCloudSearchProvider(Application* app, QObject* parent = 0);
+ void Init(SoundCloudService* service);
+
+ // SearchProvider
+ void SearchAsync(int id, const QString& query);
+ void LoadArtAsync(int id, const Result& result);
+ InternetService* internet_service() { return service_; }
+
+ private slots:
+ void AlbumArtLoaded(quint64 id, const QImage& image);
+ void SearchDone(int id, const SongList& songs);
+
+ private:
+ void MaybeSearchFinished(int id);
+
+ SoundCloudService* service_;
+ QMap pending_searches_;
+
+ AlbumCoverLoaderOptions cover_loader_options_;
+ QMap cover_loader_tasks_;
+};
+
+#endif
diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp
index 2844d27c8..0453bfe94 100644
--- a/src/internet/internetmodel.cpp
+++ b/src/internet/internetmodel.cpp
@@ -16,6 +16,7 @@
*/
#include "digitallyimportedservicebase.h"
+#include "groovesharkservice.h"
#include "icecastservice.h"
#include "jamendoservice.h"
#include "magnatuneservice.h"
@@ -24,7 +25,7 @@
#include "internetservice.h"
#include "savedradio.h"
#include "somafmservice.h"
-#include "groovesharkservice.h"
+#include "soundcloudservice.h"
#include "core/logging.h"
#include "core/mergedproxymodel.h"
#include "podcasts/podcastservice.h"
@@ -71,6 +72,7 @@ InternetModel::InternetModel(Application* app, QObject* parent)
AddService(new SavedRadio(app, this));
AddService(new SkyFmService(app, this));
AddService(new SomaFMService(app, this));
+ AddService(new SoundCloudService(app, this));
#ifdef HAVE_SPOTIFY
AddService(new SpotifyService(app, this));
#endif
diff --git a/src/internet/soundcloudservice.cpp b/src/internet/soundcloudservice.cpp
new file mode 100644
index 000000000..ae0cef802
--- /dev/null
+++ b/src/internet/soundcloudservice.cpp
@@ -0,0 +1,293 @@
+/* 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 "soundcloudservice.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "internetmodel.h"
+#include "searchboxwidget.h"
+
+#include "core/application.h"
+#include "core/closure.h"
+#include "core/logging.h"
+#include "core/mergedproxymodel.h"
+#include "core/network.h"
+#include "core/song.h"
+#include "core/taskmanager.h"
+#include "core/timeconstants.h"
+#include "core/utilities.h"
+#include "globalsearch/globalsearch.h"
+#include "globalsearch/soundcloudsearchprovider.h"
+#include "ui/iconloader.h"
+
+const char* SoundCloudService::kApiClientId = "2add0f709fcfae1fd7a198ec7573d2d4";
+
+const char* SoundCloudService::kServiceName = "SoundCloud";
+const char* SoundCloudService::kSettingsGroup = "SoundCloud";
+const char* SoundCloudService::kUrl = "https://api.soundcloud.com/";
+const char* SoundCloudService::kHomepage = "http://soundcloud.com/";
+
+const int SoundCloudService::kSearchDelayMsec = 400;
+const int SoundCloudService::kSongSearchLimit = 100;
+const int SoundCloudService::kSongSimpleSearchLimit = 10;
+
+typedef QPair Param;
+
+SoundCloudService::SoundCloudService(Application* app, InternetModel *parent)
+ : InternetService(kServiceName, app, parent, parent),
+ root_(NULL),
+ search_(NULL),
+ network_(new NetworkAccessManager(this)),
+ context_menu_(NULL),
+ search_box_(new SearchBoxWidget(this)),
+ search_delay_(new QTimer(this)),
+ next_pending_search_id_(0) {
+
+ search_delay_->setInterval(kSearchDelayMsec);
+ search_delay_->setSingleShot(true);
+ connect(search_delay_, SIGNAL(timeout()), SLOT(DoSearch()));
+
+ SoundCloudSearchProvider* search_provider = new SoundCloudSearchProvider(app_, this);
+ search_provider->Init(this);
+ app_->global_search()->AddProvider(search_provider);
+
+ connect(search_box_, SIGNAL(TextChanged(QString)), SLOT(Search(QString)));
+}
+
+
+SoundCloudService::~SoundCloudService() {
+}
+
+QStandardItem* SoundCloudService::CreateRootItem() {
+ root_ = new QStandardItem(QIcon(":providers/soundcloud.png"), kServiceName);
+ root_->setData(true, InternetModel::Role_CanLazyLoad);
+ root_->setData(InternetModel::PlayBehaviour_DoubleClickAction,
+ InternetModel::Role_PlayBehaviour);
+ return root_;
+}
+
+void SoundCloudService::LazyPopulate(QStandardItem* item) {
+ switch (item->data(InternetModel::Role_Type).toInt()) {
+ case InternetModel::Type_Service: {
+ EnsureItemsCreated();
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void SoundCloudService::EnsureItemsCreated() {
+ search_ = new QStandardItem(IconLoader::Load("edit-find"),
+ tr("Search results"));
+ search_->setToolTip(tr("Start typing something on the search box above to "
+ "fill this search results list"));
+ search_->setData(InternetModel::PlayBehaviour_MultipleItems,
+ InternetModel::Role_PlayBehaviour);
+ root_->appendRow(search_);
+}
+
+QWidget* SoundCloudService::HeaderWidget() const {
+ return search_box_;
+}
+
+void SoundCloudService::Homepage() {
+ QDesktopServices::openUrl(QUrl(kHomepage));
+}
+
+void SoundCloudService::Search(const QString& text, bool now) {
+ pending_search_ = text;
+
+ // If there is no text (e.g. user cleared search box), we don't need to do a
+ // real query that will return nothing: we can clear the playlist now
+ if (text.isEmpty()) {
+ search_delay_->stop();
+ ClearSearchResults();
+ return;
+ }
+
+ if (now) {
+ search_delay_->stop();
+ DoSearch();
+ } else {
+ search_delay_->start();
+ }
+}
+
+void SoundCloudService::DoSearch() {
+ ClearSearchResults();
+
+ QList parameters;
+ parameters << Param("q", pending_search_);
+ QNetworkReply* reply = CreateRequest("tracks", parameters);
+ const int id = next_pending_search_id_++;
+ NewClosure(reply, SIGNAL(finished()),
+ this, SLOT(SearchFinished(QNetworkReply*,int)),
+ reply, id);
+}
+
+void SoundCloudService::SearchFinished(QNetworkReply* reply, int task_id) {
+ reply->deleteLater();
+
+ SongList songs = ExtractSongs(ExtractResult(reply));
+ // Fill results list
+ foreach (const Song& song, songs) {
+ QStandardItem* child = CreateSongItem(song);
+ search_->appendRow(child);
+ }
+
+ QModelIndex index = model()->merged_model()->mapFromSource(search_->index());
+ ScrollToIndex(index);
+}
+
+void SoundCloudService::ClearSearchResults() {
+ if (search_)
+ search_->removeRows(0, search_->rowCount());
+}
+
+int SoundCloudService::SimpleSearch(const QString& text) {
+ QList parameters;
+ parameters << Param("q", text);
+ QNetworkReply* reply = CreateRequest("tracks", parameters);
+ const int id = next_pending_search_id_++;
+ NewClosure(reply, SIGNAL(finished()),
+ this, SLOT(SimpleSearchFinished(QNetworkReply*,int)),
+ reply, id);
+ return id;
+}
+
+void SoundCloudService::SimpleSearchFinished(QNetworkReply* reply, int id) {
+ reply->deleteLater();
+
+ SongList songs = ExtractSongs(ExtractResult(reply));
+ emit SimpleSearchResults(id, songs);
+}
+
+void SoundCloudService::EnsureMenuCreated() {
+ if(!context_menu_) {
+ context_menu_ = new QMenu;
+ context_menu_->addActions(GetPlaylistActions());
+ context_menu_->addSeparator();
+ context_menu_->addAction(IconLoader::Load("download"),
+ tr("Open %1 in browser").arg("soundcloud.com"),
+ this, SLOT(Homepage()));
+ }
+}
+
+void SoundCloudService::ShowContextMenu(const QPoint& global_pos) {
+ EnsureMenuCreated();
+
+ context_menu_->popup(global_pos);
+}
+
+QNetworkReply* SoundCloudService::CreateRequest(
+ const QString& ressource_name,
+ const QList& params) {
+
+ QUrl url(kUrl);
+
+ url.setPath(ressource_name);
+
+ url.addQueryItem("client_id", kApiClientId);
+ foreach(const Param& param, params) {
+ url.addQueryItem(param.first, param.second);
+ }
+
+ qLog(Debug) << "Request Url: " << url.toEncoded();
+
+ QNetworkRequest req(url);
+ req.setRawHeader("Accept", "application/json");
+ QNetworkReply *reply = network_->get(req);
+ return reply;
+}
+
+QVariant SoundCloudService::ExtractResult(QNetworkReply* reply) {
+ QJson::Parser parser;
+ bool ok;
+ QVariant result = parser.parse(reply, &ok);
+ if (!ok) {
+ qLog(Error) << "Error while parsing SoundCloud result";
+ }
+ return result;
+}
+
+SongList SoundCloudService::ExtractSongs(const QVariant& result) {
+ SongList songs;
+
+ QVariantList q_variant_list = result.toList();
+ foreach(const QVariant& q, q_variant_list) {
+ Song song = ExtractSong(q.toMap());
+ if (song.is_valid()) {
+ songs << song;
+ }
+ }
+ return songs;
+}
+
+Song SoundCloudService::ExtractSong(const QVariantMap& result_song) {
+ Song song;
+ if (!result_song.isEmpty() && result_song["streamable"].toBool()) {
+ QUrl stream_url = result_song["stream_url"].toUrl();
+ stream_url.addQueryItem("client_id", kApiClientId);
+ song.set_url(stream_url);
+
+ QString username = result_song["user"].toMap()["username"].toString();
+ // We don't have a real artist name, but username is the most similar thing
+ // we have
+ song.set_artist(username);
+
+ QString title = result_song["title"].toString();
+ song.set_title(title);
+
+ QString genre = result_song["genre"].toString();
+ song.set_genre(genre);
+
+ float bpm = result_song["bpm"].toFloat();
+ song.set_bpm(bpm);
+
+ QVariant cover = result_song["artwork_url"];
+ if (cover.isValid()) {
+ // SoundCloud covers URL are https, but our cover loader doesn't seem to
+ // deal well with https URL. Anyway, we don't need a secure connection to
+ // get a cover image.
+ QUrl cover_url = cover.toUrl();
+ cover_url.setScheme("http");
+ song.set_art_automatic(cover_url.toEncoded());
+ }
+
+ int playcount = result_song["playback_count"].toInt();
+ song.set_playcount(playcount);
+
+ int year = result_song["release_year"].toInt();
+ song.set_year(year);
+
+ QVariant q_duration = result_song["duration"];
+ quint64 duration = q_duration.toULongLong() * kNsecPerMsec;
+ song.set_length_nanosec(duration);
+
+ song.set_valid(true);
+ }
+ return song;
+}
diff --git a/src/internet/soundcloudservice.h b/src/internet/soundcloudservice.h
new file mode 100644
index 000000000..c51f45482
--- /dev/null
+++ b/src/internet/soundcloudservice.h
@@ -0,0 +1,97 @@
+/* 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 SOUNDCLOUDSERVICE_H
+#define SOUNDCLOUDSERVICE_H
+
+#include "internetmodel.h"
+#include "internetservice.h"
+
+class NetworkAccessManager;
+class SearchBoxWidget;
+
+class QMenu;
+class QNetworkReply;
+
+class SoundCloudService : public InternetService {
+ Q_OBJECT
+ public:
+ SoundCloudService(Application* app, InternetModel *parent);
+ ~SoundCloudService();
+
+ // Internet Service methods
+ QStandardItem* CreateRootItem();
+ void LazyPopulate(QStandardItem *parent);
+
+ // TODO
+ //QList playlistitem_actions(const Song& song);
+ void ShowContextMenu(const QPoint& global_pos);
+ QWidget* HeaderWidget() const;
+
+ int SimpleSearch(const QString& query);
+
+ static const char* kServiceName;
+ static const char* kSettingsGroup;
+
+ signals:
+ void SimpleSearchResults(int id, SongList songs);
+
+ private slots:
+ void Search(const QString& text, bool now = false);
+ void DoSearch();
+ void SearchFinished(QNetworkReply* reply, int task);
+ void SimpleSearchFinished(QNetworkReply* reply, int id);
+
+ void Homepage();
+
+ private:
+ void ClearSearchResults();
+ void EnsureItemsCreated();
+ void EnsureMenuCreated();
+ QNetworkReply* CreateRequest(const QString& ressource_name,
+ const QList >& params);
+ // Convenient function for extracting result from reply
+ QVariant ExtractResult(QNetworkReply* reply);
+ SongList ExtractSongs(const QVariant& result);
+ Song ExtractSong(const QVariantMap& result_song);
+
+ QStandardItem* root_;
+ QStandardItem* search_;
+
+ NetworkAccessManager* network_;
+
+ QMenu* context_menu_;
+ SearchBoxWidget* search_box_;
+ QTimer* search_delay_;
+ QString pending_search_;
+ int next_pending_search_id_;
+
+ QByteArray api_key_;
+
+ static const char* kUrl;
+ static const char* kUrlCover;
+ static const char* kHomepage;
+
+ static const int kSongSearchLimit;
+ static const int kSongSimpleSearchLimit;
+ static const int kSearchDelayMsec;
+
+ static const char* kApiClientId;
+};
+
+
+#endif // SOUNDCLOUDSERVICE_H