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:
parent
705bbce1e5
commit
5efe63462c
@ -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);
|
||||
}
|
||||
|
@ -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_;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user