From 39db4dbefedab32dd66ff7647f554c6921c4074f Mon Sep 17 00:00:00 2001 From: Arnaud Bienner Date: Mon, 22 Sep 2014 22:40:21 +0200 Subject: [PATCH] Add the ability to add a Spotify track to a Spotify playlist through context menu. TODO: drag and drop (i.e. implement DropMimeData), update special playlist (Favorites), remove from playlist. --- ext/clementine-spotifyblob/spotifyclient.cpp | 46 +++++++++++++++ ext/clementine-spotifyblob/spotifyclient.h | 1 + .../spotifymessages.proto | 15 ++++- src/internet/spotifyserver.cpp | 13 ++++ src/internet/spotifyserver.h | 1 + src/internet/spotifyservice.cpp | 59 ++++++++++++++++++- src/internet/spotifyservice.h | 10 +++- 7 files changed, 138 insertions(+), 7 deletions(-) diff --git a/ext/clementine-spotifyblob/spotifyclient.cpp b/ext/clementine-spotifyblob/spotifyclient.cpp index 265a863b5..bce8ae1c7 100644 --- a/ext/clementine-spotifyblob/spotifyclient.cpp +++ b/ext/clementine-spotifyblob/spotifyclient.cpp @@ -21,6 +21,7 @@ #include "spotifyclient.h" #include +#include #include #include @@ -295,6 +296,8 @@ void SpotifyClient::MessageArrived(const pb::spotify::Message& message) { BrowseToplist(message.browse_toplist_request()); } else if (message.has_pause_request()) { SetPaused(message.pause_request()); + } else if (message.has_add_tracks_to_playlist()) { + AddTracksToPlaylist(message.add_tracks_to_playlist()); } } @@ -438,6 +441,9 @@ void SpotifyClient::SendPlaylistList() { pb::spotify::Playlists::Playlist* msg = response->add_playlist(); msg->set_index(i); msg->set_name(sp_playlist_name(playlist)); + sp_user* playlist_owner = sp_playlist_owner(playlist); + msg->set_is_mine(sp_session_user(session_) == playlist_owner); + msg->set_owner(sp_user_display_name(playlist_owner)); sp_playlist_offline_status offline_status = sp_playlist_get_offline_status(session_, playlist); @@ -449,6 +455,7 @@ void SpotifyClient::SendPlaylistList() { } else if (offline_status == SP_PLAYLIST_OFFLINE_STATUS_WAITING) { msg->set_download_progress(0); } + msg->set_nb_tracks(sp_playlist_num_tracks(playlist)); } SendMessage(message); @@ -593,6 +600,45 @@ void SpotifyClient::PlaylistStateChangedForGetPlaylists(sp_playlist* pl, me->SendPlaylistList(); } +void SpotifyClient::AddTracksToPlaylist( + const pb::spotify::AddTracksToPlaylistRequest& req) { + + // Get the playlist we want to update + int playlist_index = req.playlist_index(); + sp_playlist* playlist = + GetPlaylist(pb::spotify::UserPlaylist, playlist_index); + if (!playlist) { + qLog(Error) << "Playlist " << playlist_index << "not found"; + return; + } + + // Get the tracks we want to add + std::unique_ptr tracks_array (new sp_track*[req.track_uri_size()]); + for (int i = 0; i < req.track_uri_size(); ++i) { + sp_link* track_link = sp_link_create_from_string(req.track_uri(i).c_str()); + sp_track* track = sp_link_as_track(track_link); + sp_track_add_ref(track); + sp_link_release(track_link); + if (!track) { + qLog(Error) << "Track" << QString::fromStdString(req.track_uri(i)) << "not found"; + } + tracks_array[i] = track; + } + + // Actually add the tracks to the playlist + if (sp_playlist_add_tracks(playlist, tracks_array.get(), + req.track_uri_size(), + 0 /* TODO: don't insert at a hardcoded position */, + session_) != SP_ERROR_OK) { + qLog(Error) << "Error when adding tracks!"; + } + + // Clean everything + for (int i = 0; i < req.track_uri_size(); ++i) { + sp_track_release(tracks_array[i]); + } +} + void SpotifyClient::ConvertTrack(sp_track* track, pb::spotify::Track* pb) { sp_album* album = sp_track_album(track); diff --git a/ext/clementine-spotifyblob/spotifyclient.h b/ext/clementine-spotifyblob/spotifyclient.h index 02d908cd6..df8918870 100644 --- a/ext/clementine-spotifyblob/spotifyclient.h +++ b/ext/clementine-spotifyblob/spotifyclient.h @@ -123,6 +123,7 @@ class SpotifyClient : public AbstractMessageHandler { void Search(const pb::spotify::SearchRequest& req); void LoadPlaylist(const pb::spotify::LoadPlaylistRequest& req); void SyncPlaylist(const pb::spotify::SyncPlaylistRequest& req); + void AddTracksToPlaylist(const pb::spotify::AddTracksToPlaylistRequest& req); void StartPlayback(const pb::spotify::PlaybackRequest& req); void Seek(qint64 offset_nsec); void LoadImage(const QString& id_b64); diff --git a/ext/libclementine-spotifyblob/spotifymessages.proto b/ext/libclementine-spotifyblob/spotifymessages.proto index fdaac676f..b7606436f 100644 --- a/ext/libclementine-spotifyblob/spotifymessages.proto +++ b/ext/libclementine-spotifyblob/spotifymessages.proto @@ -46,9 +46,12 @@ message Playlists { message Playlist { required int32 index = 1; required string name = 2; - required bool is_offline = 3; + required int32 nb_tracks = 3; + required bool is_mine = 4; + required string owner= 5; + required bool is_offline = 6; // Offline sync progress between 0-100. - optional int32 download_progress = 4; + optional int32 download_progress = 7; } repeated Playlist playlist = 1; @@ -191,7 +194,12 @@ message PauseRequest { optional bool paused = 1 [default = false]; } -// NEXT_ID: 23 +message AddTracksToPlaylistRequest { + required int64 playlist_index = 1; + repeated string track_uri = 2; +} + +// NEXT_ID: 24 message Message { // Not currently used optional int32 id = 18; @@ -217,4 +225,5 @@ message Message { optional BrowseToplistResponse browse_toplist_response = 20; optional PauseRequest pause_request = 21; optional SeekCompleted seek_completed = 22; + optional AddTracksToPlaylistRequest add_tracks_to_playlist = 23; } diff --git a/src/internet/spotifyserver.cpp b/src/internet/spotifyserver.cpp index dea7e72d6..308dc60f9 100644 --- a/src/internet/spotifyserver.cpp +++ b/src/internet/spotifyserver.cpp @@ -24,6 +24,7 @@ #include #include #include +#include SpotifyServer::SpotifyServer(QObject* parent) : AbstractMessageHandler(nullptr, parent), @@ -206,6 +207,18 @@ void SpotifyServer::LoadUserPlaylist(int index) { LoadPlaylist(pb::spotify::UserPlaylist, index); } +void SpotifyServer::AddSongsToPlaylist(int playlist_index, + const QList& songs_urls) { + pb::spotify::Message message; + pb::spotify::AddTracksToPlaylistRequest* req = + message.mutable_add_tracks_to_playlist(); + req->set_playlist_index(playlist_index); + for (const QUrl& song_url : songs_urls) { + req->add_track_uri(DataCommaSizeFromQString(song_url.toString())); + } + SendOrQueueMessage(message); +} + void SpotifyServer::StartPlaybackLater(const QString& uri, quint16 port) { QTimer* timer = new QTimer(this); connect(timer, SIGNAL(timeout()), timer, SLOT(deleteLater())); diff --git a/src/internet/spotifyserver.h b/src/internet/spotifyserver.h index 48f2d6773..abdcf3af8 100644 --- a/src/internet/spotifyserver.h +++ b/src/internet/spotifyserver.h @@ -43,6 +43,7 @@ class SpotifyServer : public AbstractMessageHandler { void SyncInbox(); void LoadUserPlaylist(int index); void SyncUserPlaylist(int index); + void AddSongsToPlaylist(int playlist_index, const QList& songs_urls); void StartPlaybackLater(const QString& uri, quint16 port); void Search(const QString& text, int limit, int limit_album = 0); void LoadImage(const QString& id); diff --git a/src/internet/spotifyservice.cpp b/src/internet/spotifyservice.cpp index 0ad02fef2..33d80debe 100644 --- a/src/internet/spotifyservice.cpp +++ b/src/internet/spotifyservice.cpp @@ -345,6 +345,17 @@ void SpotifyService::InstallBlob() { void SpotifyService::BlobDownloadFinished() { EnsureServerCreated(); } +void SpotifyService::AddCurrentSongToPlaylist(QAction* action) { + int playlist_index = action->data().toInt(); + AddSongsToPlaylist(playlist_index, QList() << current_song_url_); +} + +void SpotifyService::AddSongsToPlaylist(int playlist_index, + const QList& songs_urls) { + EnsureServerCreated(); + server_->AddSongsToPlaylist(playlist_index, songs_urls); +} + void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) { if (login_task_id_) { app_->task_manager()->SetTaskFinished(login_task_id_); @@ -391,6 +402,7 @@ void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) { qLog(Debug) << "Playlists haven't changed - not updating"; return; } + qLog(Debug) << "Playlist have changed: updating"; // Remove and recreate the other playlists for (QStandardItem* item : playlists_) { @@ -401,10 +413,16 @@ void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) { for (int i = 0; i < response.playlist_size(); ++i) { const pb::spotify::Playlists::Playlist& msg = response.playlist(i); - QStandardItem* item = new QStandardItem(QStringFromStdString(msg.name())); + QString playlist_title = QStringFromStdString(msg.name()); + if (!msg.is_mine()) { + const std::string& owner = msg.owner(); + playlist_title += tr(", by ") + QString::fromUtf8(owner.c_str(), owner.size()); + } + QStandardItem* item = new QStandardItem(playlist_title); item->setData(InternetModel::Type_UserPlaylist, InternetModel::Role_Type); item->setData(true, InternetModel::Role_CanLazyLoad); item->setData(msg.index(), Role_UserPlaylistIndex); + item->setData(msg.is_mine(), Role_UserPlaylistIsMine); item->setData(InternetModel::PlayBehaviour_MultipleItems, InternetModel::Role_PlayBehaviour); @@ -416,8 +434,8 @@ void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) { } } -bool SpotifyService::DoPlaylistsDiffer(const pb::spotify::Playlists& response) - const { +bool SpotifyService::DoPlaylistsDiffer( + const pb::spotify::Playlists& response) const { if (playlists_.count() != response.playlist_size()) { return true; } @@ -433,6 +451,10 @@ bool SpotifyService::DoPlaylistsDiffer(const pb::spotify::Playlists& response) if (QStringFromStdString(msg.name()) != item->text()) { return true; } + + if (msg.nb_tracks() != item->rowCount()) { + return true; + } } return false; @@ -528,6 +550,37 @@ void SpotifyService::SongFromProtobuf(const pb::spotify::Track& track, song->set_filesize(0); } +QList SpotifyService::playlistitem_actions(const Song& song) { + // Clear previous actions + while (!playlistitem_actions_.isEmpty()) { + QAction* action = playlistitem_actions_.takeFirst(); + delete action->menu(); + delete action; + } + + // Create a menu with 'add to playlist' actions for each Spotify playlist + QAction* add_to_playlists = new QAction(IconLoader::Load("list-add"), + tr("Add to Spotify playlists"), this); + QMenu* playlists_menu = new QMenu(); + for (const QStandardItem* playlist_item : playlists_) { + if (!playlist_item->data(Role_UserPlaylistIsMine).toBool()) { + continue; + } + QAction* add_to_playlist = new QAction(playlist_item->text(), this); + add_to_playlist->setData(playlist_item->data(Role_UserPlaylistIndex)); + playlists_menu->addAction(add_to_playlist); + } + connect(playlists_menu, SIGNAL(triggered(QAction*)), + SLOT(AddCurrentSongToPlaylist(QAction*))); + add_to_playlists->setMenu(playlists_menu); + playlistitem_actions_.append(add_to_playlists); + + // Keep in mind the current song URL + current_song_url_ = song.url(); + + return playlistitem_actions_; +} + QWidget* SpotifyService::HeaderWidget() const { if (IsLoggedIn()) return search_box_; return nullptr; diff --git a/src/internet/spotifyservice.h b/src/internet/spotifyservice.h index a52aa063b..75bbe52ec 100644 --- a/src/internet/spotifyservice.h +++ b/src/internet/spotifyservice.h @@ -29,7 +29,10 @@ class SpotifyService : public InternetService { Type_Toplist, }; - enum Role { Role_UserPlaylistIndex = InternetModel::RoleCount, }; + enum Role { + Role_UserPlaylistIndex = InternetModel::RoleCount, + Role_UserPlaylistIsMine, // Is this playlist owned by the user currently logged-in? + }; // Values are persisted - don't change. enum LoginState { @@ -53,6 +56,7 @@ class SpotifyService : public InternetService { void ShowContextMenu(const QPoint& global_pos); void ItemDoubleClicked(QStandardItem* item); void DropMimeData(const QMimeData* data, const QModelIndex& index); + QList playlistitem_actions(const Song& song) override; QWidget* HeaderWidget() const; void Logout(); @@ -88,6 +92,7 @@ signals: const google::protobuf::RepeatedPtrField& tracks); void FillPlaylist(QStandardItem* item, const pb::spotify::LoadPlaylistResponse& response); + void AddSongsToPlaylist(int playlist_index, const QList& songs_urls); void EnsureMenuCreated(); void ClearSearchResults(); @@ -100,6 +105,7 @@ signals: void BlobProcessError(QProcess::ProcessError error); void LoginCompleted(bool success, const QString& error, pb::spotify::LoginResponse_Error error_code); + void AddCurrentSongToPlaylist(QAction* action); void PlaylistsUpdated(const pb::spotify::Playlists& response); void InboxLoaded(const pb::spotify::LoadPlaylistResponse& response); void StarredLoaded(const pb::spotify::LoadPlaylistResponse& response); @@ -134,6 +140,8 @@ signals: QMenu* context_menu_; QMenu* playlist_context_menu_; QAction* playlist_sync_action_; + QList playlistitem_actions_; + QUrl current_song_url_; SearchBoxWidget* search_box_;