From 5efe63462c75875e3a7ee429facc897201d18669 Mon Sep 17 00:00:00 2001 From: David Sansome Date: Fri, 29 Apr 2011 13:41:42 +0000 Subject: [PATCH] Get album art for Spotify tracks --- spotifyblob/spotifyclient.cpp | 95 ++++++++++++++++++++++++++++++- spotifyblob/spotifyclient.h | 15 +++++ spotifyblob/spotifymessages.proto | 12 ++++ src/core/logging.cpp | 2 +- src/covers/albumcoverloader.cpp | 42 +++++++++++++- src/covers/albumcoverloader.h | 4 ++ src/radio/spotifyserver.cpp | 18 ++++++ src/radio/spotifyserver.h | 7 +-- src/radio/spotifyservice.cpp | 22 +++++++ src/radio/spotifyservice.h | 3 + 10 files changed, 213 insertions(+), 7 deletions(-) diff --git a/spotifyblob/spotifyclient.cpp b/spotifyblob/spotifyclient.cpp index 9e17f940e..74228bfdc 100644 --- a/spotifyblob/spotifyclient.cpp +++ b/spotifyblob/spotifyclient.cpp @@ -30,6 +30,9 @@ #include #include +const int SpotifyClient::kSpotifyImageIDSize = 20; + + SpotifyClient::SpotifyClient(QObject* parent) : QObject(parent), api_key_(QByteArray::fromBase64(kSpotifyApiKey)), @@ -186,6 +189,8 @@ void SpotifyClient::HandleMessage(const protobuf::SpotifyMessage& message) { StartPlayback(message.playback_request()); } else if (message.has_search_request()) { Search(message.search_request()); + } else if (message.has_image_request()) { + LoadImage(QStringFromStdString(message.image_request().id())); } } @@ -410,11 +415,19 @@ void SpotifyClient::ConvertTrack(sp_track* track, protobuf::Track* pb) { pb->set_disc(sp_track_disc(track)); pb->set_track(sp_track_index(track)); + // Album art + const QByteArray art_id( + reinterpret_cast(sp_album_cover(sp_track_album(track))), + kSpotifyImageIDSize); + const QString art_id_b64 = QString::fromAscii(art_id.toBase64()); + pb->set_album_art_id(DataCommaSizeFromQString(art_id_b64)); + + // Artists for (int i=0 ; iadd_artist(sp_artist_name(sp_track_artist(track, i))); } - // Blugh + // URI - Blugh char uri[256]; sp_link* link = sp_link_create_from_track(track, 0); sp_link_as_string(link, uri, sizeof(uri)); @@ -598,3 +611,83 @@ void SpotifyClient::MediaSocketDisconnected() { media_socket_ = NULL; } } + +void SpotifyClient::LoadImage(const QString& id_b64) { + QByteArray id = QByteArray::fromBase64(id_b64.toAscii()); + if (id.length() != kSpotifyImageIDSize) { + qLog(Warning) << "Invalid image ID (did not decode to" + << kSpotifyImageIDSize << "bytes):" << id_b64; + + // Send an error response straight away + protobuf::SpotifyMessage message; + protobuf::ImageResponse* msg = message.mutable_image_response(); + msg->set_id(DataCommaSizeFromQString(id_b64)); + handler_->SendMessage(message); + return; + } + + PendingImageRequest pending_load; + pending_load.id_ = id; + pending_load.id_b64_ = id_b64; + pending_load.image_ = sp_image_create(session_, + reinterpret_cast(id.constData())); + pending_image_requests_ << pending_load; + + if (!image_callbacks_registered_[pending_load.image_]) { + sp_image_add_load_callback(pending_load.image_, &ImageLoaded, this); + } + image_callbacks_registered_[pending_load.image_] ++; + + TryImageAgain(pending_load.image_); +} + +void SpotifyClient::TryImageAgain(sp_image* image) { + if (!sp_image_is_loaded(image)) { + qLog(Debug) << "Image not loaded, will try again later"; + return; + } + + // Find the pending request for this image + int index = -1; + PendingImageRequest* req = NULL; + for (int i=0 ; iset_id(DataCommaSizeFromQString(req->id_b64_)); + if (data && size) { + msg->set_data(data, size); + } + handler_->SendMessage(message); + + // Free stuff + image_callbacks_registered_[image] --; + if (!image_callbacks_registered_[image]) { + sp_image_remove_load_callback(image, &ImageLoaded, this); + image_callbacks_registered_.remove(image); + } + + sp_image_release(image); + pending_image_requests_.removeAt(index); +} + +void SpotifyClient::ImageLoaded(sp_image* image, void* userdata) { + SpotifyClient* me = reinterpret_cast(userdata); + me->TryImageAgain(image); +} diff --git a/spotifyblob/spotifyclient.h b/spotifyblob/spotifyclient.h index e356489fe..eff7e65f2 100644 --- a/spotifyblob/spotifyclient.h +++ b/spotifyblob/spotifyclient.h @@ -42,6 +42,8 @@ public: SpotifyClient(QObject* parent = 0); ~SpotifyClient(); + static const int kSpotifyImageIDSize; + void Init(quint16 port); private slots: @@ -85,11 +87,15 @@ private: // Spotify playlist callbacks - when loading a playlist static void PlaylistStateChangedForLoadPlaylist(sp_playlist* pl, void* userdata); + // Spotify image callbacks. + static void ImageLoaded(sp_image* image, void* userdata); + // Request handlers. void Login(const QString& username, const QString& password); void Search(const protobuf::SearchRequest& req); void LoadPlaylist(const protobuf::LoadPlaylistRequest& req); void StartPlayback(const protobuf::PlaybackRequest& req); + void LoadImage(const QString& id_b64); void SendPlaylistList(); @@ -113,7 +119,14 @@ private: } }; + struct PendingImageRequest { + QString id_b64_; + QByteArray id_; + sp_image* image_; + }; + void TryPlaybackAgain(const PendingPlaybackRequest& req); + void TryImageAgain(sp_image* image); QByteArray api_key_; @@ -132,6 +145,8 @@ private: QList pending_load_playlists_; QList pending_playback_requests_; + QList pending_image_requests_; + QMap image_callbacks_registered_; QMap pending_searches_; int media_length_msec_; diff --git a/spotifyblob/spotifymessages.proto b/spotifyblob/spotifymessages.proto index 61139c899..aa09d9111 100644 --- a/spotifyblob/spotifymessages.proto +++ b/spotifyblob/spotifymessages.proto @@ -51,6 +51,7 @@ message Track { required int32 track = 8; required int32 year = 9; required string uri = 10; + required string album_art_id = 11; } message LoadPlaylistRequest { @@ -91,6 +92,15 @@ message SearchResponse { optional string error = 5; } +message ImageRequest { + required string id = 1; +} + +message ImageResponse { + required string id = 1; + optional bytes data = 2; +} + message SpotifyMessage { optional LoginRequest login_request = 1; optional LoginResponse login_response = 2; @@ -101,4 +111,6 @@ message SpotifyMessage { optional PlaybackError playback_error = 7; optional SearchRequest search_request = 8; optional SearchResponse search_response = 9; + optional ImageRequest image_request = 10; + optional ImageResponse image_response = 11; } diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 9b2885902..330ea07b0 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -34,7 +34,7 @@ static Level sDefaultLevel = Level_Debug; static QMap* sClassLevels = NULL; static QIODevice* sNullDevice = NULL; -const char* kDefaultLogLevels = "GstEnginePipeline:2,*:3"; +const char* kDefaultLogLevels = "GstEnginePipeline:2,SpotifyMessageHandler:2,*:3"; static const char* kMessageHandlerMagic = "__logging_message__"; static const int kMessageHandlerMagicLength = strlen(kMessageHandlerMagic); diff --git a/src/covers/albumcoverloader.cpp b/src/covers/albumcoverloader.cpp index 381435bb9..763417396 100644 --- a/src/covers/albumcoverloader.cpp +++ b/src/covers/albumcoverloader.cpp @@ -16,8 +16,12 @@ */ #include "albumcoverloader.h" +#include "config.h" +#include "core/logging.h" #include "core/network.h" #include "core/utilities.h" +#include "radio/radiomodel.h" +#include "radio/spotifyservice.h" #include #include @@ -32,7 +36,8 @@ AlbumCoverLoader::AlbumCoverLoader(QObject* parent) scale_(true), padding_(true), next_id_(0), - network_(new NetworkAccessManager(this)) + network_(new NetworkAccessManager(this)), + connected_spotify_(false) { } @@ -139,12 +144,47 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage( remote_tasks_.insert(reply, task); return TryLoadResult(true, false, QImage()); + } else if (filename.toLower().startsWith("spotify://image/")) { + // HACK: we should add generic image URL handlers + #ifdef HAVE_SPOTIFY + SpotifyService* spotify = RadioModel::Service(); + + if (!connected_spotify_) { + connect(spotify, SIGNAL(ImageLoaded(QUrl,QImage)), + SLOT(SpotifyImageLoaded(QUrl,QImage))); + connected_spotify_ = true; + } + + QUrl url = QUrl(filename); + remote_spotify_tasks_.insert(url, task); + + // Need to schedule this in the spotify service's thread + QMetaObject::invokeMethod(spotify, "LoadImage", Qt::QueuedConnection, + Q_ARG(QUrl, url)); + return TryLoadResult(true, false, QImage()); + #else + return TryLoadResult(false, false, QImage()); + #endif } QImage image(filename); return TryLoadResult(false, !image.isNull(), image.isNull() ? default_ : image); } +void AlbumCoverLoader::SpotifyImageLoaded(const QUrl& url, const QImage& image) { + qLog(Debug) << "Got image from spotify:" << url; + + if (!remote_spotify_tasks_.contains(url)) + return; + + Task task = remote_spotify_tasks_.take(url); + QImage scaled = ScaleAndPad(image); + emit ImageLoaded(task.id, scaled); + emit ImageLoaded(task.id, scaled, image); + + qLog(Debug) << "Spotify image was for task" << task.id; +} + void AlbumCoverLoader::RemoteFetchFinished() { QNetworkReply* reply = qobject_cast(sender()); if (!reply) diff --git a/src/covers/albumcoverloader.h b/src/covers/albumcoverloader.h index bbaa42bd5..5c86028b1 100644 --- a/src/covers/albumcoverloader.h +++ b/src/covers/albumcoverloader.h @@ -64,6 +64,7 @@ class AlbumCoverLoader : public QObject { protected slots: void ProcessTasks(); void RemoteFetchFinished(); + void SpotifyImageLoaded(const QUrl& url, const QImage& image); protected: enum State { @@ -106,10 +107,13 @@ class AlbumCoverLoader : public QObject { QMutex mutex_; QQueue tasks_; QMap remote_tasks_; + QMap remote_spotify_tasks_; quint64 next_id_; NetworkAccessManager* network_; + bool connected_spotify_; + static const int kMaxRedirects = 3; }; diff --git a/src/radio/spotifyserver.cpp b/src/radio/spotifyserver.cpp index 93a6b5b7c..4ac3e4c59 100644 --- a/src/radio/spotifyserver.cpp +++ b/src/radio/spotifyserver.cpp @@ -123,6 +123,16 @@ void SpotifyServer::HandleMessage(const protobuf::SpotifyMessage& message) { emit PlaybackError(QStringFromStdString(message.playback_error().error())); } else if (message.has_search_response()) { emit SearchResults(message.search_response()); + } else if (message.has_image_response()) { + const protobuf::ImageResponse& response = message.image_response(); + const QString id = QStringFromStdString(response.id()); + + if (response.has_data()) { + emit ImageLoaded(id, QImage::fromData(QByteArray( + response.data().data(), response.data().size()))); + } else { + emit ImageLoaded(id, QImage()); + } } } @@ -167,3 +177,11 @@ void SpotifyServer::Search(const QString& text, int limit) { req->set_limit(limit); SendMessage(message); } + +void SpotifyServer::LoadImage(const QString& id) { + protobuf::SpotifyMessage message; + protobuf::ImageRequest* req = message.mutable_image_request(); + + req->set_id(DataCommaSizeFromQString(id)); + SendMessage(message); +} diff --git a/src/radio/spotifyserver.h b/src/radio/spotifyserver.h index 9fa412aec..9bb2aa0b2 100644 --- a/src/radio/spotifyserver.h +++ b/src/radio/spotifyserver.h @@ -20,6 +20,7 @@ #include "spotifyblob/spotifymessages.pb.h" +#include #include class SpotifyMessageHandler; @@ -39,10 +40,9 @@ public: void LoadStarred(); void LoadInbox(); void LoadUserPlaylist(int index); - void StartPlayback(const QString& uri, quint16 port); - void Search(const QString& text, int limit); + void LoadImage(const QString& id); int server_port() const; @@ -53,10 +53,9 @@ signals: void StarredLoaded(const protobuf::LoadPlaylistResponse& response); void InboxLoaded(const protobuf::LoadPlaylistResponse& response); void UserPlaylistLoaded(const protobuf::LoadPlaylistResponse& response); - void PlaybackError(const QString& message); - void SearchResults(const protobuf::SearchResponse& response); + void ImageLoaded(const QString& id, const QImage& image); private slots: void NewConnection(); diff --git a/src/radio/spotifyservice.cpp b/src/radio/spotifyservice.cpp index 15618bedd..e1be3a90b 100644 --- a/src/radio/spotifyservice.cpp +++ b/src/radio/spotifyservice.cpp @@ -142,6 +142,8 @@ void SpotifyService::EnsureServerCreated(const QString& username, SIGNAL(StreamError(QString))); connect(server_, SIGNAL(SearchResults(protobuf::SearchResponse)), SLOT(SearchResults(protobuf::SearchResponse))); + connect(server_, SIGNAL(ImageLoaded(QString,QImage)), + SLOT(ImageLoaded(QString,QImage))); connect(blob_process_, SIGNAL(error(QProcess::ProcessError)), @@ -289,6 +291,7 @@ void SpotifyService::SongFromProtobuf(const protobuf::Track& track, Song* song) song->set_track(track.track()); song->set_year(track.year()); song->set_url(QUrl(QStringFromStdString(track.uri()))); + song->set_art_automatic("spotify://image/" + QStringFromStdString(track.album_art_id())); QStringList artists; for (int i=0 ; iLoadImage(image_id); +} + +void SpotifyService::ImageLoaded(const QString& id, const QImage& image) { + qLog(Debug) << "Image loaded:" << id; + emit ImageLoaded(QUrl("spotify://image/" + id), image); +} diff --git a/src/radio/spotifyservice.h b/src/radio/spotifyservice.h index c539b057a..4f74109d8 100644 --- a/src/radio/spotifyservice.h +++ b/src/radio/spotifyservice.h @@ -47,11 +47,13 @@ public: void Login(const QString& username, const QString& password); void Search(const QString& text, Playlist* playlist, bool now = false); + Q_INVOKABLE void LoadImage(const QUrl& url); SpotifyServer* server() const; signals: void LoginFinished(bool success); + void ImageLoaded(const QUrl& url, const QImage& image); protected: virtual QModelIndex GetCurrentIndex(); @@ -74,6 +76,7 @@ private slots: void StarredLoaded(const protobuf::LoadPlaylistResponse& response); void UserPlaylistLoaded(const protobuf::LoadPlaylistResponse& response); void SearchResults(const protobuf::SearchResponse& response); + void ImageLoaded(const QString& id, const QImage& image); void OpenSearchTab(); void DoSearch();