1
0
mirror of https://github.com/clementine-player/Clementine synced 2025-01-29 10:39:47 +01:00

Get album art for Spotify tracks

This commit is contained in:
David Sansome 2011-04-29 13:41:42 +00:00
parent 705bbce1e5
commit 5efe63462c
10 changed files with 213 additions and 7 deletions

View File

@ -30,6 +30,9 @@
#include <QTcpSocket>
#include <QTimer>
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<const char*>(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 ; i<sp_track_num_artists(track) ; ++i) {
pb->add_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<const byte*>(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 ; i<pending_image_requests_.count() ; ++i) {
if (pending_image_requests_[i].image_ == image) {
index = i;
req = &pending_image_requests_[i];
break;
}
}
if (index == -1) {
qLog(Warning) << "Image not found in pending load list";
return;
}
// Get the image data
size_t size = 0;
const void* data = sp_image_data(image, &size);
// Send the response
protobuf::SpotifyMessage message;
protobuf::ImageResponse* msg = message.mutable_image_response();
msg->set_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<SpotifyClient*>(userdata);
me->TryImageAgain(image);
}

View File

@ -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<PendingLoadPlaylist> pending_load_playlists_;
QList<PendingPlaybackRequest> pending_playback_requests_;
QList<PendingImageRequest> pending_image_requests_;
QMap<sp_image*, int> image_callbacks_registered_;
QMap<sp_search*, protobuf::SearchRequest> pending_searches_;
int media_length_msec_;

View File

@ -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;
}

View File

@ -34,7 +34,7 @@ static Level sDefaultLevel = Level_Debug;
static QMap<QString, Level>* 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);

View File

@ -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 <QPainter>
#include <QDir>
@ -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<SpotifyService>();
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<QNetworkReply*>(sender());
if (!reply)

View File

@ -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<Task> tasks_;
QMap<QNetworkReply*, Task> remote_tasks_;
QMap<QUrl, Task> remote_spotify_tasks_;
quint64 next_id_;
NetworkAccessManager* network_;
bool connected_spotify_;
static const int kMaxRedirects = 3;
};

View File

@ -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);
}

View File

@ -20,6 +20,7 @@
#include "spotifyblob/spotifymessages.pb.h"
#include <QImage>
#include <QObject>
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();

View File

@ -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 ; i<track.artist_size() ; ++i) {
@ -389,3 +392,22 @@ void SpotifyService::ItemDoubleClicked(QStandardItem* item) {
OpenSearchTab();
}
}
void SpotifyService::LoadImage(const QUrl& url) {
if (url.scheme() != "spotify" || url.host() != "image") {
return;
}
QString image_id = url.path();
if (image_id.startsWith('/')) {
image_id.remove(0, 1);
}
EnsureServerCreated();
server_->LoadImage(image_id);
}
void SpotifyService::ImageLoaded(const QString& id, const QImage& image) {
qLog(Debug) << "Image loaded:" << id;
emit ImageLoaded(QUrl("spotify://image/" + id), image);
}

View File

@ -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();