--- a/src/mediaserver/cdplugins/spotify/spotiproxy.cpp
+++ b/src/mediaserver/cdplugins/spotify/spotiproxy.cpp
@@ -19,6 +19,7 @@
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
+#include <dlfcn.h>
#include <mutex>
@@ -55,6 +56,35 @@
0x9B,0x03,0x9D,0x93,0xB7,0x5F,0x3C,0xA4,0x36,0xE1,0xF3,0x07,0x4D,0xEA,0x01,0x1D,
0x3D};
+
+// We dlopen libspotify to avoid a hard link dependancy. The entry
+// points are resolved into the following struct, which just exists
+// for tidyness.
+struct SpotifyAPI {
+ const char* (*sp_error_message)(sp_error error);
+ sp_track * (*sp_link_as_track)(sp_link *link);
+ sp_link * (*sp_link_create_from_string)(const char *link);
+ sp_error (*sp_link_release)(sp_link *link);
+ sp_error (*sp_session_create)(const sp_session_config *, sp_session **sess);
+ sp_error (*sp_session_login)(sp_session *, const char *, const char *,
+ bool, const char *);
+ sp_error (*sp_session_logout)(sp_session *session);
+ sp_error (*sp_session_player_load)(sp_session *session, sp_track *track);
+ sp_error (*sp_session_player_play)(sp_session *session, bool play);
+ sp_error (*sp_session_player_seek)(sp_session *session, int offset);
+ sp_error (*sp_session_player_unload)(sp_session *session);
+ sp_error (*sp_session_process_events)(sp_session *session, int *next_timeo);
+ sp_error (*sp_session_set_cache_size)(sp_session *session, size_t size);
+ sp_error (*sp_link_add_ref)(sp_link *link);
+ int (*sp_track_duration)(sp_track *track);
+ sp_error (*sp_track_add_ref)(sp_track *track);
+ sp_error (*sp_track_error)(sp_track *track);
+ const char * (*sp_track_name)(sp_track *track);
+ sp_error (*sp_track_release)(sp_track *track);
+};
+static SpotifyAPI api;
+
+
static SpotiProxy *theSpotiProxy;
static SpotiProxy::Internal *theSPP;
// Lock for the spotiproxy object itself. Because the libspotify
@@ -79,7 +109,8 @@
class SpotiProxy::Internal {
public:
- /* The constructor logs us in */
+ /* The constructor logs us in, so that "logged_in" is also a general
+ * health test. */
Internal(const string& u, const string& p,
const string& cd, const string& sd)
: user(u), pass(p), cachedir(cd), confdir(sd) {
@@ -100,12 +131,17 @@
spconfig.cache_location = cachedir.c_str();
spconfig.settings_location = confdir.c_str();
- sperror = sp_session_create(&spconfig, &sp);
+ if (!init_spotify_api()) {
+ cerr << "Error loading spotify library: " << reason << endl;
+ LOGERR("Error loading spotify library: " << reason << endl);
+ return;
+ }
+ sperror = api.sp_session_create(&spconfig, &sp);
if (SP_ERROR_OK != sperror) {
registerError(sperror);
return;
}
- sp_session_login(sp, user.c_str(), pass.c_str(), 1, NULL);
+ api.sp_session_login(sp, user.c_str(), pass.c_str(), 1, NULL);
wait_for("Login", [] (SpotiProxy::Internal *o) {return o->logged_in;});
if (logged_in) {
LOGDEB("Spotify: " << user << " logged in ok\n");
@@ -113,7 +149,7 @@
LOGERR("Spotify: " << user << " log in failed\n");
}
// Max cache size 50 MB
- sp_session_set_cache_size(sp, 50);
+ api.sp_session_set_cache_size(sp, 50);
}
// Wait for a state change, tested by a function parameter.
@@ -138,7 +174,7 @@
do {
g_notify_do = 0;
LOGDEB1(who << " Calling process_events\n");
- sp_session_process_events(sp, &next_timeout);
+ api.sp_session_process_events(sp, &next_timeout);
LOGDEB1(who << " After process_event, next_timeout " <<
next_timeout << " notify_do " << g_notify_do << endl);
} while (next_timeout == 0);
@@ -154,8 +190,8 @@
track_playing = false;
track_duration = 0;
if (sp && curtrack) {
- sp_track_release(curtrack);
- sp_session_player_unload(sp);
+ api.sp_track_release(curtrack);
+ api.sp_session_player_unload(sp);
}
curtrack = nullptr;
spcv.notify_all();
@@ -163,17 +199,23 @@
}
~Internal() {
- unloadTrack();
- if (sp && logged_in) {
- LOGDEB("Logging out\n");
- sp_session_logout(sp);
+ if (libhandle) {
+ unloadTrack();
+ if (sp && logged_in) {
+ LOGDEB("Logging out\n");
+ api.sp_session_logout(sp);
+ }
+ dlclose(libhandle);
}
}
void registerError(sp_error error) {
- reason += string(sp_error_message(error)) + " ";
+ reason += string(api.sp_error_message(error)) + " ";
sperror = error;
}
+ bool init_spotify_api();
+
+ void *libhandle{nullptr};
string user;
string pass;
@@ -195,6 +237,68 @@
int track_duration{0};
AudioSink sink{nullptr};
};
+
+#define NMTOPTR(NM, TP) \
+ if ((api.NM = TP dlsym(libhandle, #NM)) == 0) { \
+ badnames += #NM + string(" "); \
+ }
+
+static vector<string> lib_suffixes{".so", ".so.12", ".so.11"};
+
+bool SpotiProxy::Internal::init_spotify_api()
+{
+ reason = "Could not open shared library ";
+ string libbase("libspotify");
+ for (const auto suff : lib_suffixes) {
+ string lib = libbase + suff;
+ reason += string("[") + lib + "] ";
+ if ((libhandle = dlopen(lib.c_str(), RTLD_LAZY)) != 0) {
+ reason.erase();
+ goto found;
+ }
+ }
+
+ found:
+ if (nullptr == libhandle) {
+ reason += string(" : ") + dlerror();
+ return false;
+ }
+
+ string badnames;
+
+ NMTOPTR(sp_error_message, (const char* (*)(sp_error error)));
+ NMTOPTR(sp_link_as_track, (sp_track * (*)(sp_link *link)));
+ NMTOPTR(sp_link_create_from_string, (sp_link * (*)(const char *link)));
+ NMTOPTR(sp_link_release, (sp_error (*)(sp_link *link)));
+ NMTOPTR(sp_session_create, (sp_error (*)(const sp_session_config *config,
+ sp_session **sess)));;
+ NMTOPTR(sp_session_login, (sp_error (*)(
+ sp_session *session, const char *username, const char *password,
+ bool remember_me, const char *blob)));
+ NMTOPTR(sp_session_logout, (sp_error (*)(sp_session *session)));
+ NMTOPTR(sp_session_player_load, (sp_error (*)(sp_session *session,
+ sp_track *track)));
+ NMTOPTR(sp_session_player_play, (sp_error (*)(sp_session *session,
+ bool play)));
+ NMTOPTR(sp_session_player_seek, (sp_error (*)(sp_session *session,
+ int offset)));
+ NMTOPTR(sp_session_player_unload, (sp_error (*)(sp_session *session)));
+ NMTOPTR(sp_session_process_events, (sp_error (*)(sp_session *session,
+ int *next_timeout)));
+ NMTOPTR(sp_session_set_cache_size, (sp_error (*)(sp_session *session,
+ size_t size)));
+ NMTOPTR(sp_link_add_ref, (sp_error (*)(sp_link *link)));
+ NMTOPTR(sp_track_duration, (int (*)(sp_track *track)));
+ NMTOPTR(sp_track_add_ref, (sp_error (*)(sp_track *track)));
+ NMTOPTR(sp_track_error, (sp_error (*)(sp_track *track)));
+ NMTOPTR(sp_track_name, (const char * (*)(sp_track *track)));
+ NMTOPTR(sp_track_release, (sp_error (*)(sp_track *track)));
+ if (!badnames.empty()) {
+ reason = string("init_libspotify: symbols not found:") + badnames;
+ return false;
+ }
+ return true;
+}
class Cleaner {
public:
@@ -327,7 +431,7 @@
// ??
return;
}
- sp_session_player_play(theSPP->sp, 0);
+ api.sp_session_player_play(theSPP->sp, 0);
}
static void notify_main_thread(sp_session *sess)
@@ -389,7 +493,7 @@
SpotiProxy::~SpotiProxy() {}
bool SpotiProxy::playTrack(const string& trackid, AudioSink sink,
- int seekmsecs)
+ int seekmsecs)
{
if (!startPlay(trackid, sink, seekmsecs)) {
return false;
@@ -403,34 +507,39 @@
LOGDEB("SpotiProxy::startPlay: id " << trackid << " at " <<
seekmsecs / 1000 << " S\n");
unique_lock<mutex> lock(objmutex);
+ if (!m || !m->logged_in) {
+ LOGERR("SpotiProxy::startPlay: init failed.\n");
+ return false;
+ }
string trackref("spotify:track:");
trackref += trackid;
- sp_link *link = sp_link_create_from_string(trackref.c_str());
+ sp_link *link = api.sp_link_create_from_string(trackref.c_str());
if (!link) {
LOGERR("SpotiProxy:startPlay: link creation failed\n");
return false;
}
- m->curtrack = sp_link_as_track(link);
- sp_track_add_ref(m->curtrack);
- sp_link_release(link);
+ m->curtrack = api.sp_link_as_track(link);
+ api.sp_track_add_ref(m->curtrack);
+ api.sp_link_release(link);
m->sink = sink;
if (!m->wait_for("startPlay", [](SpotiProxy::Internal *o) {
- return sp_track_error(o->curtrack) == SP_ERROR_OK;})) {
+ return api.sp_track_error(o->curtrack) == SP_ERROR_OK;})) {
LOGERR("playTrackId: error waiting for track metadata ready\n");
return false;
}
- theSPP->track_duration = sp_track_duration(m->curtrack);
- sp_session_player_load(m->sp, m->curtrack);
+ theSPP->track_duration = api.sp_track_duration(m->curtrack);
+ api.sp_session_player_load(m->sp, m->curtrack);
if (seekmsecs) {
- sp_session_player_seek(m->sp, seekmsecs);
- }
- sp_session_player_play(m->sp, 1);
+ api.sp_session_player_seek(m->sp, seekmsecs);
+ }
+ api.sp_session_player_play(m->sp, 1);
m->track_playing = true;
m->sent_0buf = false;
- LOGDEB("SpotiProxy::startPlay: NOW PLAYING "<< sp_track_name(m->curtrack) <<
+ LOGDEB("SpotiProxy::startPlay: NOW PLAYING "<<
+ api.sp_track_name(m->curtrack) <<
". Duration: " << theSPP->track_duration << endl);
return true;
}
@@ -439,6 +548,10 @@
{
LOGDEB("SpotiProxy::waitForEndOfPlay\n");
unique_lock<mutex> lock(objmutex);
+ if (!m || !m->logged_in) {
+ LOGERR("SpotiProxy::waitForEndOfPlay: init failed.\n");
+ return false;
+ }
if (!m->wait_for("waitForEndOfPlay", [](SpotiProxy::Internal *o) {
return o->track_playing == false;})) {
LOGERR("playTrackId: error waiting for end of track play\n");
@@ -449,11 +562,19 @@
bool SpotiProxy::isPlaying()
{
+ if (!m || !m->logged_in) {
+ LOGERR("SpotiProxy::isPlaying: init failed.\n");
+ return false;
+ }
return m->track_playing;
}
int SpotiProxy::durationMs()
{
+ if (!m || !m->logged_in) {
+ LOGERR("SpotiProxy::durationMs: init failed.\n");
+ return 0;
+ }
return m->track_duration;
}
@@ -461,17 +582,22 @@
{
LOGDEB("SpotiProxy:stop()\n");
unique_lock<mutex> lock(objmutex);
+ if (!m || !m->logged_in) {
+ LOGERR("SpotiProxy::stop: init failed.\n");
+ return;
+ }
m->unloadTrack();
}
bool SpotiProxy::loginOk()
{
- return m->logged_in;
+ return m && m->logged_in;
}
const string& SpotiProxy::getReason()
{
- return m->reason;
+ static string nobuild("Constructor failed");
+ return m ? m->reason : nobuild;
}
@@ -613,7 +739,7 @@
// transfer will be truncated to content-length by mhd
// anyway, only the header will be wrong in this case.
_durationms = spp->durationMs() + 300;
- _contentlen = 44 +
+ _contentlen = (_noheader? 0 : 44) +
((_durationms - _initseekmsecs) / 10) *
(rate/100) * 2 * chans;
LOGDEB0("framesink: contentlen: " << _contentlen << endl);
@@ -622,7 +748,7 @@
_cv.notify_all();
}
}
- if (!_dryrun) {
+ if (!_dryrun && !_noheader) {
char buf[100];
LOGDEB("Sending wav header. content-length " << _contentlen <<
"\n");
@@ -712,6 +838,7 @@
_streamneedinit = true;
_durationms = 0;
_initseekmsecs = 0;
+ _noheader = false;
_samplerate = 0;
_channels = 0;
_contentlen = 0;
@@ -724,6 +851,9 @@
bool _dryrun{false};
bool _streamneedinit{true};
int _initseekmsecs{0};
+ // This is for the case where the offset is non-zero (most often
+ // 44 in practise), but small enough that _initseekmsecs is 0.
+ bool _noheader{false};
int _samplerate{0};
int _channels{0};
int _durationms{0};
@@ -788,6 +918,9 @@
// return after we get the first frame and the actual contentlen
// is computed (and samplerate set again).
m->_samplerate = 0;
+ if (offset) {
+ m->_noheader = true;
+ }
return m->spp->startPlay(_url, m->_sink, m->_initseekmsecs);
}