Clementine-audio-player-Mac.../ext/clementine-spotifyblob/spotifyclient.cpp

1150 lines
38 KiB
C++

/* This file is part of Clementine.
Copyright 2011, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Note: this file is licensed under the Apache License instead of GPL because
// it is used by the Spotify blob which links against libspotify and is not GPL
// compatible.
#include "spotifyclient.h"
#include <QCoreApplication>
#include <QDir>
#include <QHostAddress>
#include <QTcpSocket>
#include <QTimer>
#include <algorithm>
#include <memory>
#include "core/arraysize.h"
#include "core/logging.h"
#include "core/timeconstants.h"
#include "mediapipeline.h"
#include "spotify_utilities.h"
#include "spotifykey.h"
#include "spotifymessages.pb.h"
const int SpotifyClient::kSpotifyImageIDSize = 20;
const int SpotifyClient::kWaveHeaderSize = 44;
SpotifyClient::SpotifyClient(QObject* parent)
: AbstractMessageHandler<cpb::spotify::Message>(nullptr, parent),
api_key_(QByteArray::fromBase64(kSpotifyApiKey)),
protocol_socket_(new QTcpSocket(this)),
session_(nullptr),
events_timer_(new QTimer(this)) {
SetDevice(protocol_socket_);
memset(&spotify_callbacks_, 0, sizeof(spotify_callbacks_));
memset(&spotify_config_, 0, sizeof(spotify_config_));
memset(&playlistcontainer_callbacks_, 0,
sizeof(playlistcontainer_callbacks_));
memset(&get_playlists_callbacks_, 0, sizeof(get_playlists_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;
spotify_callbacks_.music_delivery = &MusicDeliveryCallback;
spotify_callbacks_.end_of_track = &EndOfTrackCallback;
spotify_callbacks_.streaming_error = &StreamingErrorCallback;
spotify_callbacks_.offline_status_updated = &OfflineStatusUpdatedCallback;
spotify_callbacks_.connection_error = &ConnectionErrorCallback;
spotify_callbacks_.message_to_user = &UserMessageCallback;
spotify_callbacks_.start_playback = &StartPlaybackCallback;
spotify_callbacks_.stop_playback = &StopPlaybackCallback;
playlistcontainer_callbacks_.container_loaded =
&PlaylistContainerLoadedCallback;
playlistcontainer_callbacks_.playlist_added = &PlaylistAddedCallback;
playlistcontainer_callbacks_.playlist_moved = &PlaylistMovedCallback;
playlistcontainer_callbacks_.playlist_removed = &PlaylistRemovedCallback;
get_playlists_callbacks_.playlist_state_changed =
&PlaylistStateChangedForGetPlaylists;
load_playlist_callbacks_.playlist_state_changed =
&PlaylistStateChangedForLoadPlaylist;
QString cache = utilities::GetCacheDirectory();
qLog(Debug) << "Using:" << cache << "for Spotify cache";
QString settings_dir = utilities::GetSettingsDirectory();
qLog(Debug) << "Using:" << settings_dir << "for Spotify settings";
spotify_config_.api_version = SPOTIFY_API_VERSION; // From libspotify/api.h
spotify_config_.cache_location = strdup(cache.toUtf8().constData());
spotify_config_.settings_location = strdup(settings_dir.toUtf8().constData());
spotify_config_.application_key = api_key_.constData();
spotify_config_.application_key_size = api_key_.size();
spotify_config_.callbacks = &spotify_callbacks_;
spotify_config_.userdata = this;
spotify_config_.user_agent = "Clementine Player";
events_timer_->setSingleShot(true);
connect(events_timer_, SIGNAL(timeout()), SLOT(ProcessEvents()));
connect(protocol_socket_, SIGNAL(disconnected()),
QCoreApplication::instance(), SLOT(quit()));
}
SpotifyClient::~SpotifyClient() {
if (session_) {
sp_session_release(session_);
}
free(const_cast<char*>(spotify_config_.cache_location));
free(const_cast<char*>(spotify_config_.settings_location));
}
void SpotifyClient::Init(quint16 port) {
qLog(Debug) << "Connecting to port" << port;
protocol_socket_->connectToHost(QHostAddress::LocalHost, port);
}
void SpotifyClient::LoggedInCallback(sp_session* session, sp_error error) {
SpotifyClient* me =
reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
const bool success = error == SP_ERROR_OK;
cpb::spotify::LoginResponse_Error error_code =
cpb::spotify::LoginResponse_Error_Other;
if (!success) {
qLog(Warning) << "Failed to login" << sp_error_message(error);
}
switch (error) {
case SP_ERROR_BAD_USERNAME_OR_PASSWORD:
error_code = cpb::spotify::LoginResponse_Error_BadUsernameOrPassword;
break;
case SP_ERROR_USER_BANNED:
error_code = cpb::spotify::LoginResponse_Error_UserBanned;
break;
case SP_ERROR_USER_NEEDS_PREMIUM:
error_code = cpb::spotify::LoginResponse_Error_UserNeedsPremium;
break;
default:
error_code = cpb::spotify::LoginResponse_Error_Other;
break;
}
me->SendLoginCompleted(success, sp_error_message(error), error_code);
if (success) {
sp_playlistcontainer_add_callbacks(sp_session_playlistcontainer(session),
&me->playlistcontainer_callbacks_, me);
sp_session_flush_caches(me->session_);
}
}
void SpotifyClient::NotifyMainThreadCallback(sp_session* session) {
SpotifyClient* me =
reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
QMetaObject::invokeMethod(me, "ProcessEvents", Qt::QueuedConnection);
}
void SpotifyClient::ProcessEvents() {
int next_timeout_ms;
sp_session_process_events(session_, &next_timeout_ms);
events_timer_->start(next_timeout_ms);
}
void SpotifyClient::LogMessageCallback(sp_session* session, const char* data) {
qLog(Debug) << "libspotify:" << QString::fromUtf8(data).trimmed();
}
void SpotifyClient::Search(const cpb::spotify::SearchRequest& req) {
sp_search* search =
sp_search_create(session_, req.query().c_str(), 0, req.limit(), 0,
req.limit_album(), 0, 0, // artists
0, 0, // playlists
SP_SEARCH_STANDARD, &SearchCompleteCallback, this);
pending_searches_[search] = req;
}
void SpotifyClient::SearchCompleteCallback(sp_search* result, void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);
if (!me->pending_searches_.contains(result)) {
qLog(Warning) << "SearchComplete called with unknown search";
return;
}
// If there were any album results then we need to resolve those before
// we can send our response.
const int count = sp_search_num_albums(result);
if (count != 0) {
for (int i = 0; i < count; ++i) {
sp_album* album = sp_search_album(result, i);
sp_albumbrowse* browse = sp_albumbrowse_create(
me->session_, album, &SearchAlbumBrowseComplete, me);
me->pending_search_album_browse_responses_[browse] = result;
}
return;
}
me->SendSearchResponse(result);
}
void SpotifyClient::SearchAlbumBrowseComplete(sp_albumbrowse* result,
void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);
if (!me->pending_search_album_browse_responses_.contains(result)) {
qLog(Warning) << "SearchAlbumBrowseComplete called with unknown result";
return;
}
sp_search* search = me->pending_search_album_browse_responses_.take(result);
me->pending_search_album_browses_[search].append(result);
if (me->pending_search_album_browses_[search].count() >=
sp_search_num_albums(search)) {
me->SendSearchResponse(search);
}
}
void SpotifyClient::SendSearchResponse(sp_search* result) {
// Take the request out of the queue
cpb::spotify::SearchRequest req = pending_searches_.take(result);
// Prepare the response
cpb::spotify::Message message;
cpb::spotify::SearchResponse* response = message.mutable_search_response();
*response->mutable_request() = req;
// Check for errors
sp_error error = sp_search_error(result);
if (error != SP_ERROR_OK) {
response->set_error(sp_error_message(error));
SendMessage(message);
sp_search_release(result);
return;
}
// Get the list of tracks from the search
int count = sp_search_num_tracks(result);
for (int i = 0; i < count; ++i) {
sp_track* track = sp_search_track(result, i);
ConvertTrack(track, response->add_result());
}
// Get the albums from the search. All these should be resolved by now.
QList<sp_albumbrowse*> browses = pending_search_album_browses_.take(result);
for (sp_albumbrowse* browse : browses) {
sp_album* album = sp_albumbrowse_album(browse);
cpb::spotify::Album* msg = response->add_album();
ConvertAlbum(album, msg->mutable_metadata());
ConvertAlbumBrowse(browse, msg->mutable_metadata());
// Add all tracks
const int tracks = sp_albumbrowse_num_tracks(browse);
for (int i = 0; i < tracks; ++i) {
ConvertTrack(sp_albumbrowse_track(browse, i), msg->add_track());
}
sp_albumbrowse_release(browse);
}
// Add other data to the response
response->set_total_tracks(sp_search_total_tracks(result));
response->set_did_you_mean(sp_search_did_you_mean(result));
SendMessage(message);
sp_search_release(result);
}
void SpotifyClient::MessageArrived(const cpb::spotify::Message& message) {
if (message.has_login_request()) {
Login(message.login_request());
} else if (message.has_load_playlist_request()) {
LoadPlaylist(message.load_playlist_request());
} else if (message.has_playback_request()) {
StartPlayback(message.playback_request());
} else if (message.has_seek_request()) {
Seek(message.seek_request().offset_nsec());
} else if (message.has_search_request()) {
Search(message.search_request());
} else if (message.has_image_request()) {
LoadImage(QStringFromStdString(message.image_request().id()));
} else if (message.has_sync_playlist_request()) {
SyncPlaylist(message.sync_playlist_request());
} else if (message.has_browse_album_request()) {
BrowseAlbum(QStringFromStdString(message.browse_album_request().uri()));
} else if (message.has_set_playback_settings_request()) {
SetPlaybackSettings(message.set_playback_settings_request());
} else if (message.has_browse_toplist_request()) {
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());
} else if (message.has_remove_tracks_from_playlist()) {
RemoveTracksFromPlaylist(message.remove_tracks_from_playlist());
}
}
void SpotifyClient::SetPlaybackSettings(
const cpb::spotify::PlaybackSettings& req) {
sp_bitrate bitrate = SP_BITRATE_320k;
switch (req.bitrate()) {
case cpb::spotify::Bitrate96k:
bitrate = SP_BITRATE_96k;
break;
case cpb::spotify::Bitrate160k:
bitrate = SP_BITRATE_160k;
break;
case cpb::spotify::Bitrate320k:
bitrate = SP_BITRATE_320k;
break;
}
qLog(Debug) << "Setting playback settings: bitrate" << bitrate
<< "normalisation" << req.volume_normalisation();
sp_session_preferred_bitrate(session_, bitrate);
sp_session_preferred_offline_bitrate(session_, bitrate, false);
sp_session_set_volume_normalization(session_, req.volume_normalisation());
}
void SpotifyClient::Login(const cpb::spotify::LoginRequest& req) {
sp_error error = sp_session_create(&spotify_config_, &session_);
if (error != SP_ERROR_OK) {
qLog(Warning) << "Failed to create session" << sp_error_message(error);
SendLoginCompleted(false, sp_error_message(error),
cpb::spotify::LoginResponse_Error_Other);
return;
}
SetPlaybackSettings(req.playback_settings());
if (req.password().empty()) {
sp_error error = sp_session_relogin(session_);
if (error != SP_ERROR_OK) {
qLog(Warning) << "Tried to relogin but no stored credentials";
SendLoginCompleted(false, sp_error_message(error),
cpb::spotify::LoginResponse_Error_ReloginFailed);
}
} else {
sp_session_login(session_, req.username().c_str(), req.password().c_str(),
true, // Remember the password.
nullptr);
}
}
void SpotifyClient::SendLoginCompleted(
bool success, const QString& error,
cpb::spotify::LoginResponse_Error error_code) {
cpb::spotify::Message message;
cpb::spotify::LoginResponse* response = message.mutable_login_response();
response->set_success(success);
response->set_error(DataCommaSizeFromQString(error));
if (!success) {
response->set_error_code(error_code);
}
SendMessage(message);
}
void SpotifyClient::PlaylistContainerLoadedCallback(sp_playlistcontainer* pc,
void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);
// Install callbacks on all the playlists
sp_playlist_add_callbacks(sp_session_starred_create(me->session_),
&me->get_playlists_callbacks_, me);
const int count = sp_playlistcontainer_num_playlists(pc);
for (int i = 0; i < count; ++i) {
sp_playlist* playlist = sp_playlistcontainer_playlist(pc, i);
sp_playlist_add_callbacks(playlist, &me->get_playlists_callbacks_, me);
}
me->SendPlaylistList();
}
void SpotifyClient::PlaylistAddedCallback(sp_playlistcontainer* pc,
sp_playlist* playlist, int position,
void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);
// Install callbacks on this playlist
sp_playlist_add_callbacks(playlist, &me->get_playlists_callbacks_, me);
me->SendPlaylistList();
}
void SpotifyClient::PlaylistMovedCallback(sp_playlistcontainer* pc,
sp_playlist* playlist, int position,
int new_position, void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);
me->SendPlaylistList();
}
void SpotifyClient::PlaylistRemovedCallback(sp_playlistcontainer* pc,
sp_playlist* playlist, int position,
void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);
// Remove callbacks from this playlist
sp_playlist_remove_callbacks(playlist, &me->get_playlists_callbacks_, me);
me->SendPlaylistList();
}
void SpotifyClient::SendPlaylistList() {
cpb::spotify::Message message;
cpb::spotify::Playlists* response = message.mutable_playlists_updated();
sp_playlistcontainer* container = sp_session_playlistcontainer(session_);
if (!container) {
qLog(Warning) << "sp_session_playlistcontainer returned nullptr";
return;
}
const int count = sp_playlistcontainer_num_playlists(container);
for (int i = 0; i < count; ++i) {
const int type = sp_playlistcontainer_playlist_type(container, i);
sp_playlist* playlist = sp_playlistcontainer_playlist(container, i);
const bool is_loaded = sp_playlist_is_loaded(playlist);
qLog(Debug) << "Got playlist" << i << is_loaded << type
<< sp_playlist_name(playlist);
if (!is_loaded) {
qLog(Info) << "Playlist is not loaded yet, jump to the next one...";
continue;
}
if (type != SP_PLAYLIST_TYPE_PLAYLIST) {
// Just ignore folders for now
continue;
}
cpb::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);
const bool is_offline = offline_status == SP_PLAYLIST_OFFLINE_STATUS_YES;
msg->set_is_offline(is_offline);
if (offline_status == SP_PLAYLIST_OFFLINE_STATUS_DOWNLOADING) {
msg->set_download_progress(
sp_playlist_get_offline_download_completed(session_, playlist));
} else if (offline_status == SP_PLAYLIST_OFFLINE_STATUS_WAITING) {
msg->set_download_progress(0);
}
msg->set_nb_tracks(sp_playlist_num_tracks(playlist));
// URI - Blugh
char uri[256];
sp_link* link = sp_link_create_from_playlist(playlist);
sp_link_as_string(link, uri, arraysize(uri));
sp_link_release(link);
msg->set_uri(uri);
}
SendMessage(message);
}
sp_playlist* SpotifyClient::GetPlaylist(cpb::spotify::PlaylistType type,
int user_index) {
sp_playlist* playlist = nullptr;
switch (type) {
case cpb::spotify::Inbox:
playlist = sp_session_inbox_create(session_);
break;
case cpb::spotify::Starred:
playlist = sp_session_starred_create(session_);
break;
case cpb::spotify::UserPlaylist: {
sp_playlistcontainer* pc = sp_session_playlistcontainer(session_);
if (pc && user_index <= sp_playlistcontainer_num_playlists(pc)) {
if (sp_playlistcontainer_playlist_type(pc, user_index) ==
SP_PLAYLIST_TYPE_PLAYLIST) {
playlist = sp_playlistcontainer_playlist(pc, user_index);
sp_playlist_add_ref(playlist);
}
}
break;
}
}
return playlist;
}
void SpotifyClient::LoadPlaylist(const cpb::spotify::LoadPlaylistRequest& req) {
PendingLoadPlaylist pending_load;
pending_load.request_ = req;
pending_load.playlist_ = GetPlaylist(req.type(), req.user_playlist_index());
// 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";
cpb::spotify::Message message;
cpb::spotify::LoadPlaylistResponse* response =
message.mutable_load_playlist_response();
*response->mutable_request() = req;
SendMessage(message);
return;
}
sp_playlist_add_callbacks(pending_load.playlist_, &load_playlist_callbacks_,
this);
pending_load_playlists_ << pending_load;
PlaylistStateChangedForLoadPlaylist(pending_load.playlist_, this);
}
void SpotifyClient::SyncPlaylist(const cpb::spotify::SyncPlaylistRequest& req) {
sp_playlist* playlist =
GetPlaylist(req.request().type(), req.request().user_playlist_index());
// The playlist should already be loaded.
sp_playlist_set_offline_mode(session_, playlist, req.offline_sync());
}
void SpotifyClient::PlaylistStateChangedForLoadPlaylist(sp_playlist* pl,
void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(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 = nullptr;
for (int i = 0; i < me->pending_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; i < count; ++i) {
sp_track* track = sp_playlist_track(pl, i);
sp_track_add_ref(track);
pending_load->tracks_ << track;
}
}
// If any of the tracks aren't loaded yet we have to wait
for (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.
cpb::spotify::Message message;
cpb::spotify::LoadPlaylistResponse* response =
message.mutable_load_playlist_response();
// For some reason, we receive the starred tracks in reverse order but not
// other playlists.
if (pending_load->request_.type() == cpb::spotify::Starred) {
std::reverse(pending_load->tracks_.begin(), pending_load->tracks_.end());
}
*response->mutable_request() = pending_load->request_;
for (sp_track* track : pending_load->tracks_) {
me->ConvertTrack(track, response->add_track());
sp_track_release(track);
}
me->SendMessage(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::PlaylistStateChangedForGetPlaylists(sp_playlist* pl,
void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);
me->SendPlaylistList();
}
void SpotifyClient::AddTracksToPlaylist(
const cpb::spotify::AddTracksToPlaylistRequest& req) {
// Get the playlist we want to update
sp_playlist* playlist =
GetPlaylist(req.playlist_type(), req.playlist_index());
if (!playlist) {
qLog(Error) << "Playlist " << req.playlist_type() << ","
<< req.playlist_index() << "not found";
return;
}
// Get the tracks we want to add
std::unique_ptr<sp_track*[]> 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::RemoveTracksFromPlaylist(
const cpb::spotify::RemoveTracksFromPlaylistRequest& req) {
// Get the playlist we want to update
sp_playlist* playlist =
GetPlaylist(req.playlist_type(), req.playlist_index());
if (!playlist) {
qLog(Error) << "Playlist " << req.playlist_type() << ","
<< req.playlist_index() << "not found";
return;
}
// Get the position of the tracks we want to remove
std::unique_ptr<int[]> tracks_indices_array(new int[req.track_index_size()]);
for (int i = 0; i < req.track_index_size(); ++i) {
tracks_indices_array[i] = req.track_index(i);
}
// WTF: sp_playlist_remove_tracks indexes start from the end for starred
// playlist, not from the beginning like other playlists: reverse them
if (req.playlist_type() == cpb::spotify::Starred) {
int num_tracks = sp_playlist_num_tracks(playlist);
for (int i = 0; i < req.track_index_size(); i++) {
tracks_indices_array[i] = num_tracks - tracks_indices_array[i] - 1;
}
}
if (sp_playlist_remove_tracks(playlist, tracks_indices_array.get(),
req.track_index_size()) != SP_ERROR_OK) {
qLog(Error) << "Error when removing tracks!";
}
}
void SpotifyClient::ConvertTrack(sp_track* track, cpb::spotify::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));
// Album art
const QByteArray art_id(reinterpret_cast<const char*>(sp_album_cover(
sp_track_album(track), SP_IMAGE_SIZE_LARGE)),
kSpotifyImageIDSize);
const QString art_id_b64 = QString::fromLatin1(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)));
}
// URI - Blugh
char uri[256];
sp_link* link = sp_link_create_from_track(track, 0);
sp_link_as_string(link, uri, arraysize(uri));
sp_link_release(link);
pb->set_uri(uri);
}
void SpotifyClient::ConvertAlbum(sp_album* album, cpb::spotify::Track* pb) {
pb->set_album(sp_album_name(album));
pb->set_year(sp_album_year(album));
pb->add_artist(sp_artist_name(sp_album_artist(album)));
// These fields were required in a previous version so need to set them again
// now.
pb->mutable_title();
pb->set_duration_msec(-1);
pb->set_popularity(-1);
pb->set_disc(-1);
pb->set_track(-1);
pb->set_starred(false);
// Album art
const QByteArray art_id(
reinterpret_cast<const char*>(sp_album_cover(album, SP_IMAGE_SIZE_LARGE)),
kSpotifyImageIDSize);
const QString art_id_b64 = QString::fromLatin1(art_id.toBase64());
pb->set_album_art_id(DataCommaSizeFromQString(art_id_b64));
// URI - Blugh
char uri[256];
sp_link* link = sp_link_create_from_album(album);
sp_link_as_string(link, uri, arraysize(uri));
sp_link_release(link);
pb->set_uri(uri);
}
void SpotifyClient::ConvertAlbumBrowse(sp_albumbrowse* browse,
cpb::spotify::Track* pb) {
pb->set_track(sp_albumbrowse_num_tracks(browse));
}
void SpotifyClient::MetadataUpdatedCallback(sp_session* session) {
SpotifyClient* me =
reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
for (const PendingLoadPlaylist& load : me->pending_load_playlists_) {
PlaylistStateChangedForLoadPlaylist(load.playlist_, me);
}
for (const PendingPlaybackRequest& playback :
me->pending_playback_requests_) {
me->TryPlaybackAgain(playback);
}
}
int SpotifyClient::MusicDeliveryCallback(sp_session* session,
const sp_audioformat* format,
const void* frames, int num_frames) {
SpotifyClient* me =
reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
if (!me->media_pipeline_) {
return 0;
}
if (num_frames == 0) {
return 0;
}
if (!me->media_pipeline_->is_initialised()) {
if (!me->media_pipeline_->Init(format->sample_rate, format->channels)) {
qLog(Warning) << "Failed to intitialise media pipeline";
sp_session_player_unload(me->session_);
me->media_pipeline_.reset();
return 0;
}
}
if (!me->media_pipeline_->is_accepting_data()) {
return 0;
}
me->media_pipeline_->WriteData(reinterpret_cast<const char*>(frames),
num_frames * format->channels * 2);
return num_frames;
}
void SpotifyClient::EndOfTrackCallback(sp_session* session) {
SpotifyClient* me =
reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
me->media_pipeline_.reset();
}
void SpotifyClient::StreamingErrorCallback(sp_session* session,
sp_error error) {
SpotifyClient* me =
reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
me->media_pipeline_.reset();
// Send the error
me->SendPlaybackError(QString::fromUtf8(sp_error_message(error)));
}
void SpotifyClient::ConnectionErrorCallback(sp_session* session,
sp_error error) {
qLog(Debug) << Q_FUNC_INFO << sp_error_message(error);
}
void SpotifyClient::UserMessageCallback(sp_session* session,
const char* message) {
qLog(Debug) << Q_FUNC_INFO << message;
}
void SpotifyClient::StartPlaybackCallback(sp_session* session) {
qLog(Debug) << Q_FUNC_INFO;
}
void SpotifyClient::StopPlaybackCallback(sp_session* session) {
qLog(Debug) << Q_FUNC_INFO;
}
void SpotifyClient::OfflineStatusUpdatedCallback(sp_session* session) {
SpotifyClient* me =
reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
sp_playlistcontainer* container = sp_session_playlistcontainer(session);
if (!container) {
qLog(Warning) << "sp_session_playlistcontainer returned nullptr";
return;
}
const int count = sp_playlistcontainer_num_playlists(container);
for (int i = 0; i < count; ++i) {
const sp_playlist_type type =
sp_playlistcontainer_playlist_type(container, i);
sp_playlist* playlist = sp_playlistcontainer_playlist(container, i);
if (type != SP_PLAYLIST_TYPE_PLAYLIST) {
// Just ignore folders for now
continue;
}
int download_progress = me->GetDownloadProgress(playlist);
if (download_progress != -1) {
me->SendDownloadProgress(cpb::spotify::UserPlaylist, i, download_progress);
}
}
sp_playlist* inbox = sp_session_inbox_create(session);
int download_progress = me->GetDownloadProgress(inbox);
sp_playlist_release(inbox);
if (download_progress != -1) {
me->SendDownloadProgress(cpb::spotify::Inbox, -1, download_progress);
}
sp_playlist* starred = sp_session_starred_create(session);
download_progress = me->GetDownloadProgress(starred);
sp_playlist_release(starred);
if (download_progress != -1) {
me->SendDownloadProgress(cpb::spotify::Starred, -1, download_progress);
}
}
void SpotifyClient::SendDownloadProgress(cpb::spotify::PlaylistType type,
int index, int download_progress) {
cpb::spotify::Message message;
cpb::spotify::SyncPlaylistProgress* progress =
message.mutable_sync_playlist_progress();
progress->mutable_request()->set_type(type);
if (index != -1) {
progress->mutable_request()->set_user_playlist_index(index);
}
progress->set_sync_progress(download_progress);
SendMessage(message);
}
int SpotifyClient::GetDownloadProgress(sp_playlist* playlist) {
sp_playlist_offline_status status =
sp_playlist_get_offline_status(session_, playlist);
switch (status) {
case SP_PLAYLIST_OFFLINE_STATUS_NO:
return -1;
case SP_PLAYLIST_OFFLINE_STATUS_YES:
return 100;
case SP_PLAYLIST_OFFLINE_STATUS_DOWNLOADING:
return sp_playlist_get_offline_download_completed(session_, playlist);
case SP_PLAYLIST_OFFLINE_STATUS_WAITING:
return 0;
}
return -1;
}
void SpotifyClient::StartPlayback(const cpb::spotify::PlaybackRequest& req) {
// Get a link object from the URI
sp_link* link = sp_link_create_from_string(req.track_uri().c_str());
if (!link) {
SendPlaybackError("Invalid Spotify URI");
return;
}
// Get the track from the link
sp_track* track = sp_link_as_track(link);
if (!track) {
SendPlaybackError("Spotify URI was not a track");
sp_link_release(link);
return;
}
PendingPlaybackRequest pending_playback;
pending_playback.request_ = req;
pending_playback.link_ = link;
pending_playback.track_ = track;
pending_playback_requests_ << pending_playback;
TryPlaybackAgain(pending_playback);
}
void SpotifyClient::Seek(qint64 offset_nsec) {
// TODO
qLog(Error) << "TODO seeking";
}
void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) {
// If the track was not loaded then we have to come back later
if (!sp_track_is_loaded(req.track_)) {
qLog(Debug) << "Playback track not loaded yet, will try again later";
return;
}
// Load the track
sp_error error = sp_session_player_load(session_, req.track_);
if (error != SP_ERROR_OK) {
SendPlaybackError("Spotify playback error: " +
QString::fromUtf8(sp_error_message(error)));
sp_link_release(req.link_);
// Remove this from the pending list now
pending_playback_requests_.removeAll(req);
return;
}
// Create the media socket
media_pipeline_.reset(new MediaPipeline(req.request_.media_port(),
sp_track_duration(req.track_)));
qLog(Info) << "Starting playback of uri" << req.request_.track_uri().c_str()
<< "to port" << req.request_.media_port();
// Start playback
sp_session_player_play(session_, true);
sp_link_release(req.link_);
// Remove this from the pending list now
pending_playback_requests_.removeAll(req);
}
void SpotifyClient::SendPlaybackError(const QString& error) {
cpb::spotify::Message message;
cpb::spotify::PlaybackError* msg = message.mutable_playback_error();
msg->set_error(DataCommaSizeFromQString(error));
SendMessage(message);
}
void SpotifyClient::LoadImage(const QString& id_b64) {
QByteArray id = QByteArray::fromBase64(id_b64.toLatin1());
if (id.length() != kSpotifyImageIDSize) {
qLog(Warning) << "Invalid image ID (did not decode to"
<< kSpotifyImageIDSize << "bytes):" << id_b64;
// Send an error response straight away
cpb::spotify::Message message;
cpb::spotify::ImageResponse* msg = message.mutable_image_response();
msg->set_id(DataCommaSizeFromQString(id_b64));
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 = nullptr;
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
cpb::spotify::Message message;
cpb::spotify::ImageResponse* msg = message.mutable_image_response();
msg->set_id(DataCommaSizeFromQString(req->id_b64_));
if (data && size) {
msg->set_data(data, size);
}
SendMessage(message);
// Free stuff
image_callbacks_registered_[image]--;
// TODO: memory leak?
// 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);
}
void SpotifyClient::BrowseAlbum(const QString& uri) {
// Get a link object from the URI
sp_link* link = sp_link_create_from_string(uri.toStdString().c_str());
if (!link) {
SendPlaybackError("Invalid Album URI");
return;
}
// Get the album from the link
sp_album* album = sp_link_as_album(link);
if (!album) {
SendPlaybackError("Spotify URI was not an album");
sp_link_release(link);
return;
}
sp_albumbrowse* browse =
sp_albumbrowse_create(session_, album, &AlbumBrowseComplete, this);
pending_album_browses_[browse] = uri;
}
void SpotifyClient::AlbumBrowseComplete(sp_albumbrowse* result,
void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);
if (!me->pending_album_browses_.contains(result)) return;
QString uri = me->pending_album_browses_.take(result);
cpb::spotify::Message message;
cpb::spotify::BrowseAlbumResponse* msg =
message.mutable_browse_album_response();
msg->set_uri(DataCommaSizeFromQString(uri));
const int count = sp_albumbrowse_num_tracks(result);
for (int i = 0; i < count; ++i) {
me->ConvertTrack(sp_albumbrowse_track(result, i), msg->add_track());
}
me->SendMessage(message);
sp_albumbrowse_release(result);
}
void SpotifyClient::BrowseToplist(
const cpb::spotify::BrowseToplistRequest& req) {
sp_toplistbrowse* browse = sp_toplistbrowse_create(
session_, SP_TOPLIST_TYPE_TRACKS, // TODO: Support albums and artists.
SP_TOPLIST_REGION_EVERYWHERE, // TODO: Support other regions.
nullptr, &ToplistBrowseComplete, this);
pending_toplist_browses_[browse] = req;
}
void SpotifyClient::SetPaused(const cpb::spotify::PauseRequest& req) {
sp_session_player_play(session_, !req.paused());
}
void SpotifyClient::ToplistBrowseComplete(sp_toplistbrowse* result,
void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);
qLog(Debug) << "Toplist browse request took:"
<< sp_toplistbrowse_backend_request_duration(result) << "ms";
if (!me->pending_toplist_browses_.contains(result)) {
return;
}
const cpb::spotify::BrowseToplistRequest& request =
me->pending_toplist_browses_.take(result);
cpb::spotify::Message message;
cpb::spotify::BrowseToplistResponse* msg =
message.mutable_browse_toplist_response();
msg->mutable_request()->CopyFrom(request);
const int count = sp_toplistbrowse_num_tracks(result);
for (int i = 0; i < count; ++i) {
me->ConvertTrack(sp_toplistbrowse_track(result, i), msg->add_track());
}
me->SendMessage(message);
sp_toplistbrowse_release(result);
}
void SpotifyClient::DeviceClosed() {
AbstractMessageHandler<cpb::spotify::Message>::DeviceClosed();
qApp->exit();
}