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