From 3a34d416f9cb4a71f5415b5bb5325122b47abfd5 Mon Sep 17 00:00:00 2001 From: David Sansome Date: Tue, 26 Apr 2011 17:06:36 +0000 Subject: [PATCH] Get playlist contents from Spotify and display them in the UI --- spotifyblob/spotifyclient.cpp | 152 +++++++++++++++++++++++++++++- spotifyblob/spotifyclient.h | 20 ++++ spotifyblob/spotifymessages.proto | 31 ++++++ src/radio/spotifyserver.cpp | 40 ++++++++ src/radio/spotifyserver.h | 9 ++ src/radio/spotifyservice.cpp | 82 +++++++++++++++- src/radio/spotifyservice.h | 11 ++- 7 files changed, 339 insertions(+), 6 deletions(-) diff --git a/spotifyblob/spotifyclient.cpp b/spotifyblob/spotifyclient.cpp index 8ecdeda82..7c380156f 100644 --- a/spotifyblob/spotifyclient.cpp +++ b/spotifyblob/spotifyclient.cpp @@ -38,16 +38,20 @@ SpotifyClient::SpotifyClient(QObject* parent) memset(&spotify_callbacks_, 0, sizeof(spotify_callbacks_)); memset(&spotify_config_, 0, sizeof(spotify_config_)); memset(&playlistcontainer_callbacks_, 0, sizeof(playlistcontainer_callbacks_)); + memset(&load_playlist_callbacks_, 0, sizeof(load_playlist_callbacks_)); spotify_callbacks_.logged_in = &LoggedInCallback; spotify_callbacks_.notify_main_thread = &NotifyMainThreadCallback; spotify_callbacks_.log_message = &LogMessageCallback; + spotify_callbacks_.metadata_updated = &MetadataUpdatedCallback; playlistcontainer_callbacks_.container_loaded = &PlaylistContainerLoadedCallback; playlistcontainer_callbacks_.playlist_added = &PlaylistAddedCallback; playlistcontainer_callbacks_.playlist_moved = &PlaylistMovedCallback; playlistcontainer_callbacks_.playlist_removed = &PlaylistRemovedCallback; + load_playlist_callbacks_.playlist_state_changed = &PlaylistStateChanged; + spotify_config_.api_version = SPOTIFY_API_VERSION; // From libspotify/api.h spotify_config_.cache_location = strdup(QDir::tempPath().toLocal8Bit().constData()); spotify_config_.settings_location = strdup(QDir::tempPath().toLocal8Bit().constData()); @@ -74,7 +78,7 @@ SpotifyClient::~SpotifyClient() { } void SpotifyClient::Init(quint16 port) { - qLog(Debug) << "connecting to port" << port; + qLog(Debug) << "Connecting to port" << port; socket_->connectToHost(QHostAddress::LocalHost, port); } @@ -108,7 +112,7 @@ void SpotifyClient::ProcessEvents() { } void SpotifyClient::LogMessageCallback(sp_session* session, const char* data) { - qLog(Debug) << "libspotify:" << data; + qLog(Debug) << "libspotify:" << QString::fromUtf8(data).trimmed(); } void SpotifyClient::Search(const QString& query) { @@ -153,6 +157,8 @@ void SpotifyClient::SocketReadyRead() { if (message.has_login_request()) { const protobuf::LoginRequest& r = message.login_request(); Login(QStringFromStdString(r.username()), QStringFromStdString(r.password())); + } else if (message.has_load_playlist_request()) { + LoadPlaylist(message.load_playlist_request()); } } @@ -228,3 +234,145 @@ void SpotifyClient::GetPlaylists() { SendMessage(socket_, message); } + +void SpotifyClient::LoadPlaylist(const protobuf::LoadPlaylistRequest& req) { + PendingLoadPlaylist pending_load; + pending_load.request_ = req; + pending_load.playlist_ = NULL; + + switch (req.type()) { + case protobuf::LoadPlaylistRequest_Type_Inbox: + pending_load.playlist_ = sp_session_inbox_create(session_); + break; + + case protobuf::LoadPlaylistRequest_Type_Starred: + pending_load.playlist_ = sp_session_starred_create(session_); + break; + + case protobuf::LoadPlaylistRequest_Type_UserPlaylist: { + const int index = req.user_playlist_index(); + sp_playlistcontainer* pc = sp_session_playlistcontainer(session_); + + if (pc && index <= sp_playlistcontainer_num_playlists(pc)) { + if (sp_playlistcontainer_playlist_type(pc, index) == SP_PLAYLIST_TYPE_PLAYLIST) { + pending_load.playlist_ = sp_playlistcontainer_playlist(pc, index); + sp_playlist_add_ref(pending_load.playlist_); + } + } + + break; + } + } + + // A null playlist might mean the user wasn't logged in, or an invalid + // playlist index was requested, so we'd better return an error straight away. + if (!pending_load.playlist_) { + qLog(Warning) << "Invalid playlist requested or not logged in"; + + protobuf::SpotifyMessage message; + protobuf::LoadPlaylistResponse* response = message.mutable_load_playlist_response(); + *response->mutable_request() = req; + SendMessage(socket_, message); + return; + } + + sp_playlist_add_callbacks(pending_load.playlist_, &load_playlist_callbacks_, this); + pending_load_playlists_ << pending_load; + + PlaylistStateChanged(pending_load.playlist_, this); +} + +void SpotifyClient::PlaylistStateChanged(sp_playlist* pl, void* userdata) { + SpotifyClient* me = reinterpret_cast(userdata); + + // If the playlist isn't loaded yet we have to wait + if (!sp_playlist_is_loaded(pl)) { + qLog(Debug) << "Playlist isn't loaded yet, waiting"; + return; + } + + // Find this playlist's pending load object + int pending_load_index = -1; + PendingLoadPlaylist* pending_load = NULL; + for (int i=0 ; ipending_load_playlists_.count() ; ++i) { + if (me->pending_load_playlists_[i].playlist_ == pl) { + pending_load_index = i; + pending_load = &me->pending_load_playlists_[i]; + break; + } + } + + if (!pending_load) { + qLog(Warning) << "Playlist not found in pending load list"; + return; + } + + // If the playlist was just loaded then get all its tracks and ref them + if (pending_load->tracks_.isEmpty()) { + const int count = sp_playlist_num_tracks(pl); + for (int i=0 ; itracks_ << track; + } + } + + // If any of the tracks aren't loaded yet we have to wait + foreach (sp_track* track, pending_load->tracks_) { + if (!sp_track_is_loaded(track)) { + qLog(Debug) << "One or more tracks aren't loaded yet, waiting"; + return; + } + } + + // Everything is loaded so send the response protobuf and unref everything. + protobuf::SpotifyMessage message; + protobuf::LoadPlaylistResponse* response = message.mutable_load_playlist_response(); + + *response->mutable_request() = pending_load->request_; + foreach (sp_track* track, pending_load->tracks_) { + me->ConvertTrack(track, response->add_track()); + sp_track_release(track); + } + me->SendMessage(me->socket_, message); + + // Unref the playlist and remove our callbacks + sp_playlist_remove_callbacks(pl, &me->load_playlist_callbacks_, me); + sp_playlist_release(pl); + + // Remove the pending load object + me->pending_load_playlists_.removeAt(pending_load_index); +} + +void SpotifyClient::ConvertTrack(sp_track* track, protobuf::Track* pb) { + sp_album* album = sp_track_album(track); + + pb->set_starred(sp_track_is_starred(session_, track)); + pb->set_title(sp_track_name(track)); + pb->set_album(sp_album_name(album)); + pb->set_year(sp_album_year(album)); + pb->set_duration_msec(sp_track_duration(track)); + pb->set_popularity(sp_track_popularity(track)); + pb->set_disc(sp_track_disc(track)); + pb->set_track(sp_track_index(track)); + + for (int i=0 ; iadd_artist(sp_artist_name(sp_track_artist(track, i))); + } + + // Blugh + char uri[256]; + sp_link* link = sp_link_create_from_track(track, 0); + sp_link_as_string(link, uri, sizeof(uri)); + sp_link_release(link); + + pb->set_uri(uri); +} + +void SpotifyClient::MetadataUpdatedCallback(sp_session* session) { + SpotifyClient* me = reinterpret_cast(sp_session_userdata(session)); + + foreach (const PendingLoadPlaylist& load, me->pending_load_playlists_) { + PlaylistStateChanged(load.playlist_, me); + } +} diff --git a/spotifyblob/spotifyclient.h b/spotifyblob/spotifyclient.h index 4a1f56895..82bc570b1 100644 --- a/spotifyblob/spotifyclient.h +++ b/spotifyblob/spotifyclient.h @@ -22,6 +22,7 @@ #ifndef SPOTIFYCLIENT_H #define SPOTIFYCLIENT_H +#include "spotifymessages.pb.h" #include "spotifymessageutils.h" #include @@ -54,6 +55,7 @@ private: static void NotifyMainThreadCallback(sp_session* session); static void LogMessageCallback(sp_session* session, const char* data); static void SearchCompleteCallback(sp_search* result, void* userdata); + static void MetadataUpdatedCallback(sp_session* session); // Spotify playlist container callbacks. static void PlaylistAddedCallback( @@ -68,10 +70,25 @@ private: static void PlaylistContainerLoadedCallback( sp_playlistcontainer* pc, void* userdata); + // Spotify playlist callbacks - when loading a playlist + static void PlaylistStateChanged(sp_playlist* pl, void* userdata); + // Request handlers. void Login(const QString& username, const QString& password); void GetPlaylists(); void Search(const QString& query); + void LoadPlaylist(const protobuf::LoadPlaylistRequest& req); + + void ConvertTrack(sp_track* track, protobuf::Track* pb); + +private: + struct PendingLoadPlaylist { + protobuf::LoadPlaylistRequest request_; + + sp_playlist* playlist_; + + QList tracks_; + }; QByteArray api_key_; @@ -80,9 +97,12 @@ private: sp_session_config spotify_config_; sp_session_callbacks spotify_callbacks_; sp_playlistcontainer_callbacks playlistcontainer_callbacks_; + sp_playlist_callbacks load_playlist_callbacks_; sp_session* session_; QTimer* events_timer_; + + QList pending_load_playlists_; }; #endif // SPOTIFYCLIENT_H diff --git a/spotifyblob/spotifymessages.proto b/spotifyblob/spotifymessages.proto index bec3b8180..717d18807 100644 --- a/spotifyblob/spotifymessages.proto +++ b/spotifyblob/spotifymessages.proto @@ -40,8 +40,39 @@ message Playlists { repeated Playlist playlist = 1; } +message Track { + required bool starred = 1; + required string title = 2; + repeated string artist = 3; + required string album = 4; + required int32 duration_msec = 5; + required int32 popularity = 6; + required int32 disc = 7; + required int32 track = 8; + required int32 year = 9; + required string uri = 10; +} + +message LoadPlaylistRequest { + enum Type { + Starred = 1; + Inbox = 2; + UserPlaylist = 3; + } + + required Type type = 1; + optional int32 user_playlist_index = 2; +} + +message LoadPlaylistResponse { + required LoadPlaylistRequest request = 1; + repeated Track track = 2; +} + message SpotifyMessage { optional LoginRequest login_request = 1; optional LoginResponse login_response = 2; optional Playlists playlists_updated = 3; + optional LoadPlaylistRequest load_playlist_request = 4; + optional LoadPlaylistResponse load_playlist_response = 5; } diff --git a/src/radio/spotifyserver.cpp b/src/radio/spotifyserver.cpp index 91ff26eb2..938c0aa59 100644 --- a/src/radio/spotifyserver.cpp +++ b/src/radio/spotifyserver.cpp @@ -103,5 +103,45 @@ void SpotifyServer::ProtocolSocketReadyRead() { emit LoginCompleted(response.success()); } else if (message.has_playlists_updated()) { emit PlaylistsUpdated(message.playlists_updated()); + } else if (message.has_load_playlist_response()) { + const protobuf::LoadPlaylistResponse& response = message.load_playlist_response(); + + switch (response.request().type()) { + case protobuf::LoadPlaylistRequest_Type_Inbox: + emit InboxLoaded(response); + break; + + case protobuf::LoadPlaylistRequest_Type_Starred: + emit StarredLoaded(response); + break; + + case protobuf::LoadPlaylistRequest_Type_UserPlaylist: + emit UserPlaylistLoaded(response); + break; + } } } + +void SpotifyServer::LoadPlaylist(protobuf::LoadPlaylistRequest_Type type, int index) { + protobuf::SpotifyMessage message; + protobuf::LoadPlaylistRequest* req = message.mutable_load_playlist_request(); + + req->set_type(type); + if (index != -1) { + req->set_user_playlist_index(index); + } + + SendMessage(message); +} + +void SpotifyServer::LoadInbox() { + LoadPlaylist(protobuf::LoadPlaylistRequest_Type_Inbox); +} + +void SpotifyServer::LoadStarred() { + LoadPlaylist(protobuf::LoadPlaylistRequest_Type_Starred); +} + +void SpotifyServer::LoadUserPlaylist(int index) { + LoadPlaylist(protobuf::LoadPlaylistRequest_Type_UserPlaylist, index); +} diff --git a/src/radio/spotifyserver.h b/src/radio/spotifyserver.h index 01e7f48f0..f12290f5a 100644 --- a/src/radio/spotifyserver.h +++ b/src/radio/spotifyserver.h @@ -35,17 +35,26 @@ public: void Init(); void Login(const QString& username, const QString& password); + void LoadStarred(); + void LoadInbox(); + void LoadUserPlaylist(int index); + int server_port() const; signals: void LoginCompleted(bool success); void PlaylistsUpdated(const protobuf::Playlists& playlists); + void StarredLoaded(const protobuf::LoadPlaylistResponse& response); + void InboxLoaded(const protobuf::LoadPlaylistResponse& response); + void UserPlaylistLoaded(const protobuf::LoadPlaylistResponse& response); + private slots: void NewConnection(); void ProtocolSocketReadyRead(); private: + void LoadPlaylist(protobuf::LoadPlaylistRequest_Type type, int index = -1); void SendMessage(const protobuf::SpotifyMessage& message); QTcpServer* server_; diff --git a/src/radio/spotifyservice.cpp b/src/radio/spotifyservice.cpp index 85a6d39ac..5852e6d98 100644 --- a/src/radio/spotifyservice.cpp +++ b/src/radio/spotifyservice.cpp @@ -38,6 +38,21 @@ void SpotifyService::LazyPopulate(QStandardItem* item) { EnsureServerCreated(); break; + case Type_InboxPlaylist: + EnsureServerCreated(); + server_->LoadInbox(); + break; + + case Type_StarredPlaylist: + EnsureServerCreated(); + server_->LoadStarred(); + break; + + case Type_UserPlaylist: + EnsureServerCreated(); + server_->LoadUserPlaylist(item->data(Role_UserPlaylistIndex).toInt()); + break; + default: break; } @@ -86,6 +101,12 @@ void SpotifyService::EnsureServerCreated(const QString& username, connect(server_, SIGNAL(LoginCompleted(bool)), SLOT(LoginCompleted(bool))); connect(server_, SIGNAL(PlaylistsUpdated(protobuf::Playlists)), SLOT(PlaylistsUpdated(protobuf::Playlists))); + connect(server_, SIGNAL(InboxLoaded(protobuf::LoadPlaylistResponse)), + SLOT(InboxLoaded(protobuf::LoadPlaylistResponse))); + connect(server_, SIGNAL(StarredLoaded(protobuf::LoadPlaylistResponse)), + SLOT(StarredLoaded(protobuf::LoadPlaylistResponse))); + connect(server_, SIGNAL(UserPlaylistLoaded(protobuf::LoadPlaylistResponse)), + SLOT(UserPlaylistLoaded(protobuf::LoadPlaylistResponse))); connect(blob_process_, SIGNAL(error(QProcess::ProcessError)), @@ -135,10 +156,67 @@ void SpotifyService::PlaylistsUpdated(const protobuf::Playlists& response) { const protobuf::Playlists::Playlist& msg = response.playlist(i); QStandardItem* item = new QStandardItem(QStringFromStdString(msg.name())); - item->setData(Type_Playlist, RadioModel::Role_Type); + item->setData(Type_UserPlaylist, RadioModel::Role_Type); item->setData(true, RadioModel::Role_CanLazyLoad); - item->setData(msg.index(), Role_PlaylistIndex); + item->setData(msg.index(), Role_UserPlaylistIndex); root_->appendRow(item); + playlists_ << item; } } + +void SpotifyService::InboxLoaded(const protobuf::LoadPlaylistResponse& response) { + FillPlaylist(inbox_, response); +} + +void SpotifyService::StarredLoaded(const protobuf::LoadPlaylistResponse& response) { + FillPlaylist(starred_, response); +} + +void SpotifyService::UserPlaylistLoaded(const protobuf::LoadPlaylistResponse& response) { + // Find a playlist with this index + foreach (QStandardItem* item, playlists_) { + if (item->data(Role_UserPlaylistIndex).toInt() == response.request().user_playlist_index()) { + FillPlaylist(item, response); + break; + } + } +} + +void SpotifyService::FillPlaylist(QStandardItem* item, const protobuf::LoadPlaylistResponse& response) { + if (item->hasChildren()) + item->removeRows(0, item->rowCount()); + + for (int i=0 ; isetData(Type_Track, RadioModel::Role_Type); + child->setData(QVariant::fromValue(song), Role_Metadata); + + item->appendRow(child); + } +} + +void SpotifyService::SongFromProtobuf(const protobuf::Track& track, Song* song) const { + song->set_rating(track.starred() ? 1.0 : 0.0); + song->set_title(QStringFromStdString(track.title())); + song->set_album(QStringFromStdString(track.album())); + song->set_length_nanosec(track.duration_msec() * kNsecPerMsec); + song->set_score(track.popularity()); + song->set_disc(track.disc()); + song->set_track(track.track()); + song->set_year(track.year()); + song->set_filename(QStringFromStdString(track.uri())); + + QStringList artists; + for (int i=0 ; iset_artist(artists.join(", ")); + + song->set_filetype(Song::Type_Stream); + song->set_valid(true); +} diff --git a/src/radio/spotifyservice.h b/src/radio/spotifyservice.h index 84bd7f312..c6423234d 100644 --- a/src/radio/spotifyservice.h +++ b/src/radio/spotifyservice.h @@ -20,11 +20,13 @@ public: enum Type { Type_StarredPlaylist = RadioModel::TypeCount, Type_InboxPlaylist, - Type_Playlist, + Type_UserPlaylist, + Type_Track, }; enum Role { - Role_PlaylistIndex = RadioModel::RoleCount, + Role_UserPlaylistIndex = RadioModel::RoleCount, + Role_Metadata, }; virtual QStandardItem* CreateRootItem(); @@ -44,11 +46,16 @@ protected: private: void EnsureServerCreated(const QString& username = QString(), const QString& password = QString()); + void FillPlaylist(QStandardItem* item, const protobuf::LoadPlaylistResponse& response); + void SongFromProtobuf(const protobuf::Track& track, Song* song) const; private slots: void BlobProcessError(QProcess::ProcessError error); void LoginCompleted(bool success); void PlaylistsUpdated(const protobuf::Playlists& response); + void InboxLoaded(const protobuf::LoadPlaylistResponse& response); + void StarredLoaded(const protobuf::LoadPlaylistResponse& response); + void UserPlaylistLoaded(const protobuf::LoadPlaylistResponse& response); private: SpotifyServer* server_;