|
a |
|
b/src/mediaserver/cdplugins/spotify/spotiproxy.cpp |
|
|
1 |
/* Copyright (C) 2017-2018 J.F.Dockes
|
|
|
2 |
* This program is free software; you can redistribute it and/or modify
|
|
|
3 |
* it under the terms of the GNU General Public License as published by
|
|
|
4 |
* the Free Software Foundation; either version 2 of the License, or
|
|
|
5 |
* (at your option) any later version.
|
|
|
6 |
*
|
|
|
7 |
* This program is distributed in the hope that it will be useful,
|
|
|
8 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
9 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
10 |
* GNU General Public License for more details.
|
|
|
11 |
*
|
|
|
12 |
* You should have received a copy of the GNU General Public License
|
|
|
13 |
* along with this program; if not, write to the
|
|
|
14 |
* Free Software Foundation, Inc.,
|
|
|
15 |
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
|
16 |
*/
|
|
|
17 |
#include "spotiproxy.h"
|
|
|
18 |
|
|
|
19 |
#include <stdint.h>
|
|
|
20 |
#include <stdlib.h>
|
|
|
21 |
#include <unistd.h>
|
|
|
22 |
#include <dlfcn.h>
|
|
|
23 |
|
|
|
24 |
#include <mutex>
|
|
|
25 |
|
|
|
26 |
#include "../spotify/libspotify/api.h"
|
|
|
27 |
|
|
|
28 |
#include "log.h"
|
|
|
29 |
#include "smallut.h"
|
|
|
30 |
|
|
|
31 |
using namespace std;
|
|
|
32 |
using namespace std::placeholders;
|
|
|
33 |
|
|
|
34 |
/* mopidy appkey from mopidy_spotify/spotify_appkey.key */
|
|
|
35 |
/* No idea how to get a new one now that the lib is deprecated, sorry */
|
|
|
36 |
const vector<uint8_t> g_appkey {
|
|
|
37 |
0x01,0xCF,0x89,0x0F,0xDE,0x9F,0xD6,0x21,0x50,0x16,0x8E,0xD4,0x33,0x7F,0x73,0x82,
|
|
|
38 |
0xC1,0x52,0xC7,0x4E,0x85,0x47,0x20,0x8D,0x53,0xB9,0x22,0x5E,0x3D,0xC5,0x2B,0x09,
|
|
|
39 |
0xE9,0xCF,0x64,0x2F,0x64,0x85,0xCF,0xC3,0x4B,0x7E,0xEB,0x38,0x06,0x28,0x25,0x6E,
|
|
|
40 |
0xD1,0xD5,0xFE,0x47,0xF7,0x7E,0x4C,0x90,0x0E,0x9F,0xB8,0x0B,0x98,0x1A,0x14,0x2E,
|
|
|
41 |
0x24,0xBF,0xDD,0x71,0x73,0x6D,0xC5,0xBD,0xF3,0xB2,0x81,0x9E,0x10,0x79,0x7C,0x33,
|
|
|
42 |
0x13,0xAC,0x30,0x03,0x97,0x3E,0x74,0x87,0xB6,0x95,0x7C,0xC1,0xEA,0x64,0x89,0xE2,
|
|
|
43 |
0x0D,0xDE,0xA2,0xDA,0xB7,0xBC,0xF9,0x2B,0xBB,0xDF,0xB2,0x97,0x34,0xCE,0xBB,0x79,
|
|
|
44 |
0xEC,0x2F,0xA2,0xEE,0xF1,0x21,0xF7,0xCC,0xF3,0xC9,0x75,0x90,0x15,0x3F,0xBB,0xAA,
|
|
|
45 |
0xC2,0xC9,0x64,0x39,0x07,0xD8,0x57,0x0F,0x09,0x28,0x71,0x47,0x04,0x48,0xF0,0x54,
|
|
|
46 |
0x8E,0x4D,0xD3,0x2B,0xC3,0xA3,0xF8,0x2B,0x22,0xC1,0xC2,0x86,0xB3,0x67,0xB9,0xBE,
|
|
|
47 |
0x16,0x70,0xE2,0xAB,0x17,0x76,0xE9,0xAD,0x08,0x50,0xCF,0xD8,0x0B,0x32,0xC6,0x34,
|
|
|
48 |
0x64,0x4B,0x6F,0xC4,0x20,0x62,0xBD,0x48,0xD1,0xFB,0x57,0x5D,0x29,0xBC,0x10,0x89,
|
|
|
49 |
0xC3,0xB5,0x9F,0x57,0xFB,0x74,0x4E,0x01,0x59,0xEB,0xAC,0x99,0xB7,0x95,0x70,0x2C,
|
|
|
50 |
0x12,0xE8,0x60,0xE0,0x5F,0x3E,0x56,0xEB,0x74,0x28,0xC0,0x5D,0x2C,0x45,0x09,0x0F,
|
|
|
51 |
0x1F,0x96,0x6F,0x99,0x60,0x25,0x08,0x89,0xD0,0xB3,0xFA,0xAD,0x86,0x17,0xE7,0x30,
|
|
|
52 |
0xA9,0x5B,0xE7,0x61,0xAC,0x3A,0xFB,0xCD,0xC6,0xFB,0x8A,0xD0,0x19,0xC8,0xBE,0xD8,
|
|
|
53 |
0xD5,0xA7,0xBB,0x04,0xE5,0x1D,0xA4,0x00,0x45,0xBD,0x84,0x7B,0xE2,0x7B,0x26,0x5D,
|
|
|
54 |
0x6E,0x4C,0x42,0xEF,0xC2,0x72,0x49,0x69,0x9F,0x7D,0x66,0x9E,0x95,0xAA,0x94,0xCF,
|
|
|
55 |
0x89,0xC8,0x4C,0xFD,0xD5,0x41,0xE7,0x64,0xA1,0xE8,0xEE,0xA7,0x98,0xD6,0xCF,0x1A,
|
|
|
56 |
0x9B,0x03,0x9D,0x93,0xB7,0x5F,0x3C,0xA4,0x36,0xE1,0xF3,0x07,0x4D,0xEA,0x01,0x1D,
|
|
|
57 |
0x3D};
|
|
|
58 |
|
|
|
59 |
|
|
|
60 |
// We dlopen libspotify to avoid a hard link dependancy. The entry
|
|
|
61 |
// points are resolved into the following struct, which just exists
|
|
|
62 |
// for tidyness.
|
|
|
63 |
struct SpotifyAPI {
|
|
|
64 |
const char* (*sp_error_message)(sp_error error);
|
|
|
65 |
sp_track * (*sp_link_as_track)(sp_link *link);
|
|
|
66 |
sp_link * (*sp_link_create_from_string)(const char *link);
|
|
|
67 |
sp_error (*sp_link_release)(sp_link *link);
|
|
|
68 |
sp_error (*sp_session_create)(const sp_session_config *, sp_session **sess);
|
|
|
69 |
sp_error (*sp_session_login)(sp_session *, const char *, const char *,
|
|
|
70 |
bool, const char *);
|
|
|
71 |
sp_error (*sp_session_logout)(sp_session *session);
|
|
|
72 |
sp_error (*sp_session_player_load)(sp_session *session, sp_track *track);
|
|
|
73 |
sp_error (*sp_session_player_play)(sp_session *session, bool play);
|
|
|
74 |
sp_error (*sp_session_player_seek)(sp_session *session, int offset);
|
|
|
75 |
sp_error (*sp_session_player_unload)(sp_session *session);
|
|
|
76 |
sp_error (*sp_session_process_events)(sp_session *session, int *next_timeo);
|
|
|
77 |
sp_error (*sp_session_set_cache_size)(sp_session *session, size_t size);
|
|
|
78 |
sp_error (*sp_link_add_ref)(sp_link *link);
|
|
|
79 |
int (*sp_track_duration)(sp_track *track);
|
|
|
80 |
sp_error (*sp_track_add_ref)(sp_track *track);
|
|
|
81 |
sp_error (*sp_track_error)(sp_track *track);
|
|
|
82 |
const char * (*sp_track_name)(sp_track *track);
|
|
|
83 |
sp_error (*sp_track_release)(sp_track *track);
|
|
|
84 |
};
|
|
|
85 |
static SpotifyAPI api;
|
|
|
86 |
|
|
|
87 |
|
|
|
88 |
static SpotiProxy *theSpotiProxy;
|
|
|
89 |
static SpotiProxy::Internal *theSPP;
|
|
|
90 |
// Lock for the spotiproxy object itself. Because the libspotify
|
|
|
91 |
// methods are not reentrant, most SpotiProxy methods take this
|
|
|
92 |
// exclusive lock.
|
|
|
93 |
static mutex objmutex;
|
|
|
94 |
|
|
|
95 |
static int g_notify_do;
|
|
|
96 |
static sp_session_callbacks session_callbacks;
|
|
|
97 |
static sp_session_config spconfig;
|
|
|
98 |
|
|
|
99 |
// Forward decls
|
|
|
100 |
static void login_cb(sp_session *sess, sp_error error);
|
|
|
101 |
static void log_message(sp_session *session, const char *msg);
|
|
|
102 |
static void notify_main_thread(sp_session *sess);
|
|
|
103 |
static void metadata_updated(sp_session *sess);
|
|
|
104 |
static int music_delivery(sp_session *sess, const sp_audioformat *format,
|
|
|
105 |
const void *frames, int num_frames);
|
|
|
106 |
static void end_of_track(sp_session *sess);
|
|
|
107 |
static void play_token_lost(sp_session *sess);
|
|
|
108 |
|
|
|
109 |
class SpotiProxy::Internal {
|
|
|
110 |
public:
|
|
|
111 |
|
|
|
112 |
/* The constructor logs us in, so that "logged_in" is also a general
|
|
|
113 |
* health test. */
|
|
|
114 |
Internal(const string& u, const string& p,
|
|
|
115 |
const string& cd, const string& sd)
|
|
|
116 |
: user(u), pass(p), cachedir(cd), confdir(sd) {
|
|
|
117 |
theSPP = this;
|
|
|
118 |
session_callbacks.logged_in = login_cb;
|
|
|
119 |
session_callbacks.log_message = log_message;
|
|
|
120 |
session_callbacks.notify_main_thread = notify_main_thread;
|
|
|
121 |
session_callbacks.metadata_updated = metadata_updated;
|
|
|
122 |
session_callbacks.music_delivery = music_delivery;
|
|
|
123 |
session_callbacks.play_token_lost = play_token_lost;
|
|
|
124 |
session_callbacks.end_of_track = end_of_track,
|
|
|
125 |
|
|
|
126 |
spconfig.api_version = SPOTIFY_API_VERSION;
|
|
|
127 |
spconfig.application_key = &g_appkey[0];
|
|
|
128 |
spconfig.application_key_size = g_appkey.size();
|
|
|
129 |
spconfig.user_agent = "upmpdcli-spotiproxy";
|
|
|
130 |
spconfig.callbacks = &session_callbacks;
|
|
|
131 |
spconfig.cache_location = cachedir.c_str();
|
|
|
132 |
spconfig.settings_location = confdir.c_str();
|
|
|
133 |
|
|
|
134 |
if (!init_spotify_api()) {
|
|
|
135 |
cerr << "Error loading spotify library: " << reason << endl;
|
|
|
136 |
LOGERR("Error loading spotify library: " << reason << endl);
|
|
|
137 |
return;
|
|
|
138 |
}
|
|
|
139 |
sperror = api.sp_session_create(&spconfig, &sp);
|
|
|
140 |
if (SP_ERROR_OK != sperror) {
|
|
|
141 |
registerError(sperror);
|
|
|
142 |
return;
|
|
|
143 |
}
|
|
|
144 |
api.sp_session_login(sp, user.c_str(), pass.c_str(), 1, NULL);
|
|
|
145 |
wait_for("Login", [] (SpotiProxy::Internal *o) {return o->logged_in;});
|
|
|
146 |
if (logged_in) {
|
|
|
147 |
LOGDEB("Spotify: " << user << " logged in ok\n");
|
|
|
148 |
} else {
|
|
|
149 |
LOGERR("Spotify: " << user << " log in failed\n");
|
|
|
150 |
}
|
|
|
151 |
// Max cache size 50 MB
|
|
|
152 |
api.sp_session_set_cache_size(sp, 50);
|
|
|
153 |
}
|
|
|
154 |
|
|
|
155 |
// Wait for a state change, tested by a function parameter.
|
|
|
156 |
bool wait_for(const string& who,
|
|
|
157 |
std::function<bool(SpotiProxy::Internal *)> testit) {
|
|
|
158 |
int next_timeout = 0;
|
|
|
159 |
for (;;) {
|
|
|
160 |
if (!g_notify_do) {
|
|
|
161 |
unique_lock<mutex> lock(spmutex);
|
|
|
162 |
if (testit(this) || sperror != SP_ERROR_OK) {
|
|
|
163 |
return sperror == SP_ERROR_OK;
|
|
|
164 |
}
|
|
|
165 |
if (next_timeout == 0) {
|
|
|
166 |
LOGDEB1(who << " Waiting\n");
|
|
|
167 |
spcv.wait(lock);
|
|
|
168 |
} else {
|
|
|
169 |
LOGDEB1(who << " waiting " << next_timeout << " mS\n");
|
|
|
170 |
spcv.wait_for(
|
|
|
171 |
lock, std::chrono::milliseconds(next_timeout));
|
|
|
172 |
}
|
|
|
173 |
}
|
|
|
174 |
do {
|
|
|
175 |
g_notify_do = 0;
|
|
|
176 |
LOGDEB1(who << " Calling process_events\n");
|
|
|
177 |
api.sp_session_process_events(sp, &next_timeout);
|
|
|
178 |
LOGDEB1(who << " After process_event, next_timeout " <<
|
|
|
179 |
next_timeout << " notify_do " << g_notify_do << endl);
|
|
|
180 |
} while (next_timeout == 0);
|
|
|
181 |
}
|
|
|
182 |
}
|
|
|
183 |
|
|
|
184 |
void unloadTrack() {
|
|
|
185 |
LOGDEB0("unloadTrack\n");
|
|
|
186 |
unique_lock<mutex> lock(spmutex);
|
|
|
187 |
LOGDEB1("unloadTrack: got lock\n");
|
|
|
188 |
reason.clear();
|
|
|
189 |
sperror = SP_ERROR_OK;
|
|
|
190 |
track_playing = false;
|
|
|
191 |
track_duration = 0;
|
|
|
192 |
if (sp && curtrack) {
|
|
|
193 |
api.sp_track_release(curtrack);
|
|
|
194 |
api.sp_session_player_unload(sp);
|
|
|
195 |
}
|
|
|
196 |
curtrack = nullptr;
|
|
|
197 |
spcv.notify_all();
|
|
|
198 |
LOGDEB1("unloadTrack: done\n");
|
|
|
199 |
}
|
|
|
200 |
|
|
|
201 |
~Internal() {
|
|
|
202 |
if (libhandle) {
|
|
|
203 |
unloadTrack();
|
|
|
204 |
if (sp && logged_in) {
|
|
|
205 |
LOGDEB("Logging out\n");
|
|
|
206 |
api.sp_session_logout(sp);
|
|
|
207 |
}
|
|
|
208 |
dlclose(libhandle);
|
|
|
209 |
}
|
|
|
210 |
}
|
|
|
211 |
|
|
|
212 |
void registerError(sp_error error) {
|
|
|
213 |
reason += string(api.sp_error_message(error)) + " ";
|
|
|
214 |
sperror = error;
|
|
|
215 |
}
|
|
|
216 |
bool init_spotify_api();
|
|
|
217 |
|
|
|
218 |
void *libhandle{nullptr};
|
|
|
219 |
|
|
|
220 |
string user;
|
|
|
221 |
string pass;
|
|
|
222 |
string cachedir;
|
|
|
223 |
string confdir;
|
|
|
224 |
sp_session *sp{nullptr};
|
|
|
225 |
bool logged_in{false};
|
|
|
226 |
|
|
|
227 |
// sync for waiting for libspotify events.
|
|
|
228 |
condition_variable spcv;
|
|
|
229 |
mutex spmutex;
|
|
|
230 |
|
|
|
231 |
|
|
|
232 |
string reason;
|
|
|
233 |
sp_error sperror{SP_ERROR_OK};
|
|
|
234 |
sp_track *curtrack{nullptr};
|
|
|
235 |
bool track_playing{false};
|
|
|
236 |
bool sent_0buf{false};
|
|
|
237 |
int track_duration{0};
|
|
|
238 |
AudioSink sink{nullptr};
|
|
|
239 |
};
|
|
|
240 |
|
|
|
241 |
#define NMTOPTR(NM, TP) \
|
|
|
242 |
if ((api.NM = TP dlsym(libhandle, #NM)) == 0) { \
|
|
|
243 |
badnames += #NM + string(" "); \
|
|
|
244 |
}
|
|
|
245 |
|
|
|
246 |
static vector<string> lib_suffixes{".so.12", ".so"};
|
|
|
247 |
|
|
|
248 |
bool SpotiProxy::Internal::init_spotify_api()
|
|
|
249 |
{
|
|
|
250 |
reason = "Could not open shared library ";
|
|
|
251 |
string libbase("libspotify");
|
|
|
252 |
for (const auto suff : lib_suffixes) {
|
|
|
253 |
string lib = libbase + suff;
|
|
|
254 |
reason += string("[") + lib + "] ";
|
|
|
255 |
if ((libhandle = dlopen(lib.c_str(), RTLD_LAZY)) != 0) {
|
|
|
256 |
reason.erase();
|
|
|
257 |
goto found;
|
|
|
258 |
}
|
|
|
259 |
}
|
|
|
260 |
|
|
|
261 |
found:
|
|
|
262 |
if (nullptr == libhandle) {
|
|
|
263 |
reason += string(" : ") + dlerror();
|
|
|
264 |
return false;
|
|
|
265 |
}
|
|
|
266 |
|
|
|
267 |
string badnames;
|
|
|
268 |
|
|
|
269 |
NMTOPTR(sp_error_message, (const char* (*)(sp_error error)));
|
|
|
270 |
NMTOPTR(sp_link_as_track, (sp_track * (*)(sp_link *link)));
|
|
|
271 |
NMTOPTR(sp_link_create_from_string, (sp_link * (*)(const char *link)));
|
|
|
272 |
NMTOPTR(sp_link_release, (sp_error (*)(sp_link *link)));
|
|
|
273 |
NMTOPTR(sp_session_create, (sp_error (*)(const sp_session_config *config,
|
|
|
274 |
sp_session **sess)));;
|
|
|
275 |
NMTOPTR(sp_session_login, (sp_error (*)(
|
|
|
276 |
sp_session *session, const char *username, const char *password,
|
|
|
277 |
bool remember_me, const char *blob)));
|
|
|
278 |
NMTOPTR(sp_session_logout, (sp_error (*)(sp_session *session)));
|
|
|
279 |
NMTOPTR(sp_session_player_load, (sp_error (*)(sp_session *session,
|
|
|
280 |
sp_track *track)));
|
|
|
281 |
NMTOPTR(sp_session_player_play, (sp_error (*)(sp_session *session,
|
|
|
282 |
bool play)));
|
|
|
283 |
NMTOPTR(sp_session_player_seek, (sp_error (*)(sp_session *session,
|
|
|
284 |
int offset)));
|
|
|
285 |
NMTOPTR(sp_session_player_unload, (sp_error (*)(sp_session *session)));
|
|
|
286 |
NMTOPTR(sp_session_process_events, (sp_error (*)(sp_session *session,
|
|
|
287 |
int *next_timeout)));
|
|
|
288 |
NMTOPTR(sp_session_set_cache_size, (sp_error (*)(sp_session *session,
|
|
|
289 |
size_t size)));
|
|
|
290 |
NMTOPTR(sp_link_add_ref, (sp_error (*)(sp_link *link)));
|
|
|
291 |
NMTOPTR(sp_track_duration, (int (*)(sp_track *track)));
|
|
|
292 |
NMTOPTR(sp_track_add_ref, (sp_error (*)(sp_track *track)));
|
|
|
293 |
NMTOPTR(sp_track_error, (sp_error (*)(sp_track *track)));
|
|
|
294 |
NMTOPTR(sp_track_name, (const char * (*)(sp_track *track)));
|
|
|
295 |
NMTOPTR(sp_track_release, (sp_error (*)(sp_track *track)));
|
|
|
296 |
if (!badnames.empty()) {
|
|
|
297 |
reason = string("init_libspotify: symbols not found:") + badnames;
|
|
|
298 |
return false;
|
|
|
299 |
}
|
|
|
300 |
return true;
|
|
|
301 |
}
|
|
|
302 |
|
|
|
303 |
class Cleaner {
|
|
|
304 |
public:
|
|
|
305 |
~Cleaner() {
|
|
|
306 |
delete theSpotiProxy;
|
|
|
307 |
}
|
|
|
308 |
};
|
|
|
309 |
static Cleaner cleaner;
|
|
|
310 |
|
|
|
311 |
static void login_cb(sp_session *sess, sp_error error)
|
|
|
312 |
{
|
|
|
313 |
const char *me = "login_cb";
|
|
|
314 |
LOGDEB1(me << " error " << error << "\n");
|
|
|
315 |
if (nullptr == theSPP) {
|
|
|
316 |
LOGERR(me << " no SPP ??\n");
|
|
|
317 |
// ??
|
|
|
318 |
return;
|
|
|
319 |
}
|
|
|
320 |
unique_lock<mutex> lock(theSPP->spmutex);
|
|
|
321 |
|
|
|
322 |
if (SP_ERROR_OK == error) {
|
|
|
323 |
theSPP->logged_in = true;
|
|
|
324 |
} else {
|
|
|
325 |
theSPP->registerError(error);
|
|
|
326 |
}
|
|
|
327 |
theSPP->spcv.notify_all();
|
|
|
328 |
}
|
|
|
329 |
|
|
|
330 |
static void log_message(sp_session *session, const char *msg)
|
|
|
331 |
{
|
|
|
332 |
LOGDEB(msg);
|
|
|
333 |
}
|
|
|
334 |
|
|
|
335 |
static void metadata_updated(sp_session *sess)
|
|
|
336 |
{
|
|
|
337 |
const char *me = "metadata_updated";
|
|
|
338 |
LOGDEB1(me << "\n");
|
|
|
339 |
if (nullptr == theSPP) {
|
|
|
340 |
LOGERR(me << " no SPP ??\n");
|
|
|
341 |
return;
|
|
|
342 |
}
|
|
|
343 |
unique_lock<mutex> lock(theSPP->spmutex);
|
|
|
344 |
theSPP->spcv.notify_all();
|
|
|
345 |
}
|
|
|
346 |
|
|
|
347 |
static int music_delivery(sp_session *sess, const sp_audioformat *format,
|
|
|
348 |
const void *frames, int num_frames)
|
|
|
349 |
{
|
|
|
350 |
const char *me = "music_delivery";
|
|
|
351 |
static int counter;
|
|
|
352 |
if ((counter++ %100) == 0) {
|
|
|
353 |
LOGDEB1(me << ": " << num_frames << " frames " <<
|
|
|
354 |
" samplerate " << format->sample_rate << " channels " <<
|
|
|
355 |
format->channels << endl);
|
|
|
356 |
}
|
|
|
357 |
|
|
|
358 |
if (num_frames == 0) {
|
|
|
359 |
LOGDEB("music_delivery: called with 0 frames\n");
|
|
|
360 |
return 0;
|
|
|
361 |
}
|
|
|
362 |
if (nullptr == theSPP) {
|
|
|
363 |
return -1;
|
|
|
364 |
}
|
|
|
365 |
if (num_frames > 4096) {
|
|
|
366 |
// Declare eot when we see a silence buffer Not too sure why
|
|
|
367 |
// these silence buffers are generated before the
|
|
|
368 |
// notify_main_thread/end_of_track is called. At some point,
|
|
|
369 |
// we called unload from here (see the git hist). It happens
|
|
|
370 |
// that notify_main thread is not called at all after we get a
|
|
|
371 |
// silence buffer (probable libspotify bug ? Just do it here
|
|
|
372 |
// for safety.
|
|
|
373 |
LOGDEB(me << ": got silence buffer\n");
|
|
|
374 |
// Silence buffer: end of real track
|
|
|
375 |
if (!theSPP->sent_0buf) {
|
|
|
376 |
theSPP->sent_0buf = true;
|
|
|
377 |
theSPP->sink(frames, 0, format->channels, format->sample_rate);
|
|
|
378 |
}
|
|
|
379 |
g_notify_do = true;
|
|
|
380 |
theSPP->spcv.notify_all();
|
|
|
381 |
return num_frames;
|
|
|
382 |
}
|
|
|
383 |
|
|
|
384 |
return theSPP->sink(frames, num_frames, format->channels,
|
|
|
385 |
format->sample_rate);
|
|
|
386 |
}
|
|
|
387 |
|
|
|
388 |
#if 0
|
|
|
389 |
// Spotify probably does this to adjust its send rate, but it's not
|
|
|
390 |
// clear how to implement it in general, nor does it seems to be
|
|
|
391 |
// needed. Would need another function callback to work (in addition
|
|
|
392 |
// to sink())
|
|
|
393 |
static void get_audio_buffer_stats(sp_session *sess,
|
|
|
394 |
sp_audio_buffer_stats *stats)
|
|
|
395 |
{
|
|
|
396 |
const char *me = "get_audio_buffer_stats";
|
|
|
397 |
LOGDEB(me << "\n");
|
|
|
398 |
if (nullptr == theSPP) {
|
|
|
399 |
LOGERR(me << " no SPP ??\n");
|
|
|
400 |
// ??
|
|
|
401 |
return;
|
|
|
402 |
}
|
|
|
403 |
|
|
|
404 |
stats->samples = 0;
|
|
|
405 |
stats->stutter = 0;
|
|
|
406 |
}
|
|
|
407 |
#endif
|
|
|
408 |
|
|
|
409 |
static void end_of_track(sp_session *sess)
|
|
|
410 |
{
|
|
|
411 |
const char *me = "end_of_track";
|
|
|
412 |
LOGDEB(me << "\n");
|
|
|
413 |
if (nullptr == theSPP) {
|
|
|
414 |
LOGERR(me << " no SPP ??\n");
|
|
|
415 |
// ??
|
|
|
416 |
return;
|
|
|
417 |
}
|
|
|
418 |
unique_lock<mutex> lock(theSPP->spmutex);
|
|
|
419 |
|
|
|
420 |
theSPP->track_playing = false;
|
|
|
421 |
theSPP->track_duration = 0;
|
|
|
422 |
theSPP->spcv.notify_all();
|
|
|
423 |
}
|
|
|
424 |
|
|
|
425 |
static void play_token_lost(sp_session *sess)
|
|
|
426 |
{
|
|
|
427 |
const char *me = "play_token_lost";
|
|
|
428 |
LOGDEB(me << "\n");
|
|
|
429 |
if (nullptr == theSPP) {
|
|
|
430 |
LOGERR(me << " no SPP ??\n");
|
|
|
431 |
// ??
|
|
|
432 |
return;
|
|
|
433 |
}
|
|
|
434 |
api.sp_session_player_play(theSPP->sp, 0);
|
|
|
435 |
}
|
|
|
436 |
|
|
|
437 |
static void notify_main_thread(sp_session *sess)
|
|
|
438 |
{
|
|
|
439 |
const char *me = "notify_main_thread";
|
|
|
440 |
LOGDEB(me << "\n");
|
|
|
441 |
if (nullptr == theSPP) {
|
|
|
442 |
LOGERR(me << " no SPP ??\n");
|
|
|
443 |
// ??
|
|
|
444 |
return;
|
|
|
445 |
}
|
|
|
446 |
unique_lock<mutex> lock(theSPP->spmutex);
|
|
|
447 |
g_notify_do = 1;
|
|
|
448 |
theSPP->spcv.notify_all();
|
|
|
449 |
}
|
|
|
450 |
|
|
|
451 |
static string o_user, o_password, o_cachedir, o_settingsdir;
|
|
|
452 |
void SpotiProxy::setParams(
|
|
|
453 |
const std::string& user, const std::string& pass,
|
|
|
454 |
const std::string& cachedir, const std::string& settingsdir)
|
|
|
455 |
{
|
|
|
456 |
o_user = user;
|
|
|
457 |
o_password = pass;
|
|
|
458 |
o_cachedir = cachedir;
|
|
|
459 |
o_settingsdir = settingsdir;
|
|
|
460 |
}
|
|
|
461 |
|
|
|
462 |
SpotiProxy *SpotiProxy::getSpotiProxy(
|
|
|
463 |
const string& u, const string& p, const string& cached, const string& confd)
|
|
|
464 |
{
|
|
|
465 |
LOGDEB1("getSpotiProxy\n");
|
|
|
466 |
unique_lock<mutex> lock(objmutex);
|
|
|
467 |
if (theSpotiProxy) {
|
|
|
468 |
LOGDEB1("getSpotiProxy: already created\n");
|
|
|
469 |
if ((u.empty() && p.empty()) ||
|
|
|
470 |
(theSpotiProxy->m->user == u && theSpotiProxy->m->pass == p)) {
|
|
|
471 |
return theSpotiProxy;
|
|
|
472 |
} else {
|
|
|
473 |
return nullptr;
|
|
|
474 |
}
|
|
|
475 |
} else {
|
|
|
476 |
string user(u.empty() ? o_user : u);
|
|
|
477 |
string pass(p.empty() ? o_password : p);
|
|
|
478 |
LOGDEB("getSpotiProxy: creating for user " << user <<"\n");
|
|
|
479 |
theSpotiProxy = new SpotiProxy(user, pass,
|
|
|
480 |
cached.empty() ? o_cachedir : cached,
|
|
|
481 |
confd.empty() ? o_settingsdir : confd);
|
|
|
482 |
return theSpotiProxy;
|
|
|
483 |
}
|
|
|
484 |
}
|
|
|
485 |
|
|
|
486 |
|
|
|
487 |
SpotiProxy::SpotiProxy(const string& user, const string& password,
|
|
|
488 |
const string& cd, const string& sd)
|
|
|
489 |
: m(std::unique_ptr<Internal>(new Internal(user, password, cd, sd)))
|
|
|
490 |
{
|
|
|
491 |
}
|
|
|
492 |
|
|
|
493 |
SpotiProxy::~SpotiProxy() {}
|
|
|
494 |
|
|
|
495 |
bool SpotiProxy::playTrack(const string& trackid, AudioSink sink,
|
|
|
496 |
int seekmsecs)
|
|
|
497 |
{
|
|
|
498 |
if (!startPlay(trackid, sink, seekmsecs)) {
|
|
|
499 |
return false;
|
|
|
500 |
}
|
|
|
501 |
return waitForEndOfPlay();
|
|
|
502 |
}
|
|
|
503 |
|
|
|
504 |
bool SpotiProxy::startPlay(const string& trackid, AudioSink sink,
|
|
|
505 |
int seekmsecs)
|
|
|
506 |
{
|
|
|
507 |
LOGDEB("SpotiProxy::startPlay: id " << trackid << " at " <<
|
|
|
508 |
seekmsecs / 1000 << " S\n");
|
|
|
509 |
unique_lock<mutex> lock(objmutex);
|
|
|
510 |
if (!m || !m->logged_in) {
|
|
|
511 |
LOGERR("SpotiProxy::startPlay: init failed.\n");
|
|
|
512 |
return false;
|
|
|
513 |
}
|
|
|
514 |
string trackref("spotify:track:");
|
|
|
515 |
trackref += trackid;
|
|
|
516 |
sp_link *link = api.sp_link_create_from_string(trackref.c_str());
|
|
|
517 |
if (!link) {
|
|
|
518 |
LOGERR("SpotiProxy:startPlay: link creation failed\n");
|
|
|
519 |
return false;
|
|
|
520 |
}
|
|
|
521 |
m->curtrack = api.sp_link_as_track(link);
|
|
|
522 |
api.sp_track_add_ref(m->curtrack);
|
|
|
523 |
api.sp_link_release(link);
|
|
|
524 |
|
|
|
525 |
m->sink = sink;
|
|
|
526 |
|
|
|
527 |
if (!m->wait_for("startPlay", [](SpotiProxy::Internal *o) {
|
|
|
528 |
return api.sp_track_error(o->curtrack) == SP_ERROR_OK;})) {
|
|
|
529 |
LOGERR("playTrackId: error waiting for track metadata ready\n");
|
|
|
530 |
return false;
|
|
|
531 |
}
|
|
|
532 |
|
|
|
533 |
theSPP->track_duration = api.sp_track_duration(m->curtrack);
|
|
|
534 |
api.sp_session_player_load(m->sp, m->curtrack);
|
|
|
535 |
if (seekmsecs) {
|
|
|
536 |
api.sp_session_player_seek(m->sp, seekmsecs);
|
|
|
537 |
}
|
|
|
538 |
api.sp_session_player_play(m->sp, 1);
|
|
|
539 |
m->track_playing = true;
|
|
|
540 |
m->sent_0buf = false;
|
|
|
541 |
LOGDEB("SpotiProxy::startPlay: NOW PLAYING "<<
|
|
|
542 |
api.sp_track_name(m->curtrack) <<
|
|
|
543 |
". Duration: " << theSPP->track_duration << endl);
|
|
|
544 |
return true;
|
|
|
545 |
}
|
|
|
546 |
|
|
|
547 |
bool SpotiProxy::waitForEndOfPlay()
|
|
|
548 |
{
|
|
|
549 |
LOGDEB("SpotiProxy::waitForEndOfPlay\n");
|
|
|
550 |
unique_lock<mutex> lock(objmutex);
|
|
|
551 |
if (!m || !m->logged_in) {
|
|
|
552 |
LOGERR("SpotiProxy::waitForEndOfPlay: init failed.\n");
|
|
|
553 |
return false;
|
|
|
554 |
}
|
|
|
555 |
if (!m->wait_for("waitForEndOfPlay", [](SpotiProxy::Internal *o) {
|
|
|
556 |
return o->track_playing == false;})) {
|
|
|
557 |
LOGERR("playTrackId: error waiting for end of track play\n");
|
|
|
558 |
return false;
|
|
|
559 |
}
|
|
|
560 |
return true;
|
|
|
561 |
}
|
|
|
562 |
|
|
|
563 |
bool SpotiProxy::isPlaying()
|
|
|
564 |
{
|
|
|
565 |
if (!m || !m->logged_in) {
|
|
|
566 |
LOGERR("SpotiProxy::isPlaying: init failed.\n");
|
|
|
567 |
return false;
|
|
|
568 |
}
|
|
|
569 |
return m->track_playing;
|
|
|
570 |
}
|
|
|
571 |
|
|
|
572 |
int SpotiProxy::durationMs()
|
|
|
573 |
{
|
|
|
574 |
if (!m || !m->logged_in) {
|
|
|
575 |
LOGERR("SpotiProxy::durationMs: init failed.\n");
|
|
|
576 |
return 0;
|
|
|
577 |
}
|
|
|
578 |
return m->track_duration;
|
|
|
579 |
}
|
|
|
580 |
|
|
|
581 |
void SpotiProxy::stop()
|
|
|
582 |
{
|
|
|
583 |
LOGDEB("SpotiProxy:stop()\n");
|
|
|
584 |
unique_lock<mutex> lock(objmutex);
|
|
|
585 |
if (!m || !m->logged_in) {
|
|
|
586 |
LOGERR("SpotiProxy::stop: init failed.\n");
|
|
|
587 |
return;
|
|
|
588 |
}
|
|
|
589 |
m->unloadTrack();
|
|
|
590 |
}
|
|
|
591 |
|
|
|
592 |
bool SpotiProxy::loginOk()
|
|
|
593 |
{
|
|
|
594 |
return m && m->logged_in;
|
|
|
595 |
}
|
|
|
596 |
|
|
|
597 |
const string& SpotiProxy::getReason()
|
|
|
598 |
{
|
|
|
599 |
static string nobuild("Constructor failed");
|
|
|
600 |
return m ? m->reason : nobuild;
|
|
|
601 |
}
|
|
|
602 |
|
|
|
603 |
|
|
|
604 |
////////// NetFetch wrapper ////////////////////////////////////////////
|
|
|
605 |
|
|
|
606 |
inline int inttoichar4(unsigned char *cdb, unsigned int addr)
|
|
|
607 |
{
|
|
|
608 |
cdb[3] = (addr & 0xff000000) >> 24;
|
|
|
609 |
cdb[2] = (addr & 0x00ff0000) >> 16;
|
|
|
610 |
cdb[1] = (addr & 0x0000ff00) >> 8;
|
|
|
611 |
cdb[0] = addr & 0x000000ff;
|
|
|
612 |
return 4;
|
|
|
613 |
}
|
|
|
614 |
|
|
|
615 |
inline int inttoichar2(unsigned char *cdb, unsigned int cnt)
|
|
|
616 |
{
|
|
|
617 |
cdb[1] = (cnt & 0x0000ff00) >> 8;
|
|
|
618 |
cdb[0] = cnt & 0x000000ff;
|
|
|
619 |
return 2;
|
|
|
620 |
}
|
|
|
621 |
|
|
|
622 |
|
|
|
623 |
#if 0
|
|
|
624 |
// For reference: definition of a wav header
|
|
|
625 |
// Les valeurs en commentaires sont donnees pour du son 44100/16/2
|
|
|
626 |
struct wav_header {
|
|
|
627 |
/*0 */char riff[4]; /* = 'RIFF' */
|
|
|
628 |
/*4 */int32 rifflen; /* longueur des infos qui suivent= datalen+36 */
|
|
|
629 |
/*8 */char wave[4]; /* = 'WAVE' */
|
|
|
630 |
|
|
|
631 |
/*12*/char fmt[4]; /* = 'fmt ' */
|
|
|
632 |
/*16*/int32 fmtlen; /* = 16 */
|
|
|
633 |
/*20*/int16 formtag; /* = 1 : PCM */
|
|
|
634 |
/*22*/int16 nchan; /* = 2 : nombre de canaux */
|
|
|
635 |
/*24*/int32 sampspersec; /* = 44100 : Nbr d'echantillons par seconde */
|
|
|
636 |
/*28*/int32 avgbytpersec;/* = 176400 : Nbr moyen octets par seconde */
|
|
|
637 |
/*32*/int16 blockalign; /* = 4 : nombre d'octets par echantillon */
|
|
|
638 |
/*34*/int16 bitspersamp; /* = 16 : bits par echantillon */
|
|
|
639 |
|
|
|
640 |
/*36*/char data[4]; /* = 'data' */
|
|
|
641 |
/*40*/int32 datalen; /* Nombre d'octets de son qui suivent */
|
|
|
642 |
/*44*/char data[];
|
|
|
643 |
};
|
|
|
644 |
#endif /* if 0 */
|
|
|
645 |
|
|
|
646 |
#define WAVHSIZE 44
|
|
|
647 |
#define RIFFTOWAVCNT 36
|
|
|
648 |
|
|
|
649 |
// Format header. Note the use of intel format integers. Input buffer must
|
|
|
650 |
// be of size >= 44
|
|
|
651 |
int makewavheader(char *buf, int maxsize, int freq, int bits,
|
|
|
652 |
int chans, unsigned int databytecnt)
|
|
|
653 |
{
|
|
|
654 |
if (maxsize < WAVHSIZE)
|
|
|
655 |
return -1;
|
|
|
656 |
|
|
|
657 |
unsigned char *cp = (unsigned char *)buf;
|
|
|
658 |
memcpy(cp, "RIFF", 4);
|
|
|
659 |
cp += 4;
|
|
|
660 |
inttoichar4(cp, databytecnt + RIFFTOWAVCNT);
|
|
|
661 |
cp += 4;
|
|
|
662 |
memcpy(cp, "WAVE", 4);
|
|
|
663 |
cp += 4;
|
|
|
664 |
|
|
|
665 |
memcpy(cp, "fmt ", 4);
|
|
|
666 |
cp += 4;
|
|
|
667 |
inttoichar4(cp, 16);
|
|
|
668 |
cp += 4;
|
|
|
669 |
inttoichar2(cp, 1);
|
|
|
670 |
cp += 2;
|
|
|
671 |
inttoichar2(cp, chans);
|
|
|
672 |
cp += 2;
|
|
|
673 |
inttoichar4(cp, freq);
|
|
|
674 |
cp += 4;
|
|
|
675 |
inttoichar4(cp, freq * chans * (bits / 8));
|
|
|
676 |
cp += 4;
|
|
|
677 |
inttoichar2(cp, chans * bits / 8);
|
|
|
678 |
cp += 2;
|
|
|
679 |
inttoichar2(cp, bits);
|
|
|
680 |
cp += 2;
|
|
|
681 |
|
|
|
682 |
memcpy(cp, "data", 4);
|
|
|
683 |
cp += 4;
|
|
|
684 |
inttoichar4(cp, databytecnt);
|
|
|
685 |
cp += 4;
|
|
|
686 |
|
|
|
687 |
return WAVHSIZE;
|
|
|
688 |
}
|
|
|
689 |
|
|
|
690 |
class SpotiFetch::Internal {
|
|
|
691 |
static const int SAMPLES_BUF_SIZE = 16 * 1024;
|
|
|
692 |
public:
|
|
|
693 |
|
|
|
694 |
Internal(SpotiFetch *parent)
|
|
|
695 |
: p(parent) {
|
|
|
696 |
LOGDEB("SpotiFetch::SpotiFetch:\n");
|
|
|
697 |
spp = SpotiProxy::getSpotiProxy();
|
|
|
698 |
if (nullptr == spp) {
|
|
|
699 |
LOGERR("SpotiFetch::start: getSpotiProxy returned null\n");
|
|
|
700 |
}
|
|
|
701 |
_sink = std::bind(&Internal::framesink, this, _1, _2, _3, _4);
|
|
|
702 |
|
|
|
703 |
}
|
|
|
704 |
|
|
|
705 |
~Internal() {
|
|
|
706 |
LOGDEB("SpotiFetch::~SpotiFetch: clen " << _contentlen <<
|
|
|
707 |
" total sent " << _totalsent << endl);
|
|
|
708 |
if (spp) {
|
|
|
709 |
spp->stop();
|
|
|
710 |
}
|
|
|
711 |
}
|
|
|
712 |
|
|
|
713 |
// Write callback receiving data from Spotify.
|
|
|
714 |
int framesink(const void *frames, int num_frames, int chans, int rate) {
|
|
|
715 |
LOGDEB1("SpotiFefch::framesink. dryrun " << _dryrun << " num_frames " <<
|
|
|
716 |
num_frames<< " channels " << chans << " rate " << rate << endl);
|
|
|
717 |
|
|
|
718 |
|
|
|
719 |
// Need samplerate, so can only be done on first data call
|
|
|
720 |
if (_streamneedinit) {
|
|
|
721 |
{unique_lock<mutex> lock(_mutex);
|
|
|
722 |
// First pass, compute what's needed, discard data
|
|
|
723 |
LOGDEB("SpotiFetch: sample rate " << rate << " chans " <<
|
|
|
724 |
chans << endl);
|
|
|
725 |
_samplerate = rate;
|
|
|
726 |
_channels = chans;
|
|
|
727 |
_streamneedinit = false;
|
|
|
728 |
// We fake a slightly longer song. This is ok with
|
|
|
729 |
// mpd, but the actual transfer will be shorter than
|
|
|
730 |
// what the wav header and content-length say, which
|
|
|
731 |
// may be an issue with some renderers, and will
|
|
|
732 |
// not work at all with, e.g. wget or curl.
|
|
|
733 |
//
|
|
|
734 |
// Adding silence in this case, would break gapless
|
|
|
735 |
// with mpd, so we don't do it. This might be a
|
|
|
736 |
// settable option.
|
|
|
737 |
//
|
|
|
738 |
// In the case where 200 mS is < actual diff, the long
|
|
|
739 |
// transfer will be truncated to content-length by mhd
|
|
|
740 |
// anyway, only the header will be wrong in this case.
|
|
|
741 |
_durationms = spp->durationMs() + 300;
|
|
|
742 |
_contentlen = (_noheader? 0 : 44) +
|
|
|
743 |
((_durationms - _initseekmsecs) / 10) *
|
|
|
744 |
(rate/100) * 2 * chans;
|
|
|
745 |
LOGDEB0("framesink: contentlen: " << _contentlen << endl);
|
|
|
746 |
_dryruncv.notify_all();
|
|
|
747 |
if (!_dryrun) {
|
|
|
748 |
_cv.notify_all();
|
|
|
749 |
}
|
|
|
750 |
}
|
|
|
751 |
if (!_dryrun && !_noheader) {
|
|
|
752 |
char buf[100];
|
|
|
753 |
LOGDEB("Sending wav header. content-length " << _contentlen <<
|
|
|
754 |
"\n");
|
|
|
755 |
int cnt = makewavheader(
|
|
|
756 |
buf, 100, rate, 16, chans, _contentlen - 44);
|
|
|
757 |
_totalsent += cnt;
|
|
|
758 |
p->databufToQ(buf, cnt);
|
|
|
759 |
}
|
|
|
760 |
}
|
|
|
761 |
|
|
|
762 |
if (_dryrun) {
|
|
|
763 |
return num_frames;
|
|
|
764 |
}
|
|
|
765 |
|
|
|
766 |
// A call with num_frames == 0 signals the end of stream
|
|
|
767 |
if (num_frames == 0) {
|
|
|
768 |
LOGDEB("SpotiFetch: empty buf: EOS. clen: " << _contentlen <<
|
|
|
769 |
" total sent: " << _totalsent << endl);
|
|
|
770 |
|
|
|
771 |
// Padding with a silence buffer avoids curl errors, but
|
|
|
772 |
// it creates a gap. OTOH curl errors often cause the last
|
|
|
773 |
// buffer to be dropped so that gapless is broken too (in
|
|
|
774 |
// a different way). No good solution here. Avoiding curl
|
|
|
775 |
// (and wav header) errors is probably better all in all).
|
|
|
776 |
size_t resid = _contentlen - _totalsent;
|
|
|
777 |
if (resid > 0 && resid < 5000000) {
|
|
|
778 |
LOGDEB("SpotiFetch: padding track with " << resid <<
|
|
|
779 |
" bytes (" << (resid*10)/(2*chans*rate/100) << " mS)\n");
|
|
|
780 |
char *buf = (char *)malloc(resid);
|
|
|
781 |
if (buf) {
|
|
|
782 |
memset(buf, 0, resid);
|
|
|
783 |
p->databufToQ(buf, resid);
|
|
|
784 |
}
|
|
|
785 |
}
|
|
|
786 |
|
|
|
787 |
// Enqueue empty buffer.
|
|
|
788 |
p->databufToQ(frames, 0);
|
|
|
789 |
return 0;
|
|
|
790 |
}
|
|
|
791 |
|
|
|
792 |
int bytes = num_frames * chans * 2;
|
|
|
793 |
if (_totalsent + bytes > _contentlen) {
|
|
|
794 |
bytes = _contentlen - _totalsent;
|
|
|
795 |
if (bytes <= 0) {
|
|
|
796 |
return num_frames;
|
|
|
797 |
}
|
|
|
798 |
}
|
|
|
799 |
_totalsent += bytes;
|
|
|
800 |
p->databufToQ(frames, bytes);
|
|
|
801 |
return num_frames;
|
|
|
802 |
}
|
|
|
803 |
|
|
|
804 |
bool dodryrun(const string& url) {
|
|
|
805 |
_dryrun = true;
|
|
|
806 |
if (!spp->startPlay(url, _sink, 0)) {
|
|
|
807 |
LOGERR("dodryrun: startplay failed\n");
|
|
|
808 |
_dryrun = false;
|
|
|
809 |
_streamneedinit = true;
|
|
|
810 |
return false;
|
|
|
811 |
}
|
|
|
812 |
bool ret = waitForHeadersInternal(0, true);
|
|
|
813 |
spp->stop();
|
|
|
814 |
_streamneedinit = true;
|
|
|
815 |
return ret;
|
|
|
816 |
}
|
|
|
817 |
|
|
|
818 |
bool waitForHeadersInternal(int maxSecs, bool isfordry) {
|
|
|
819 |
unique_lock<mutex> lock(_mutex);
|
|
|
820 |
LOGDEB("waitForHeaders: rate " << _samplerate << " isfordry " <<
|
|
|
821 |
isfordry << " dryrun " << _dryrun << "\n");
|
|
|
822 |
while (_samplerate == 0 || (!isfordry && _dryrun)) {
|
|
|
823 |
LOGDEB1("waitForHeaders: waiting for sample rate. rate " <<
|
|
|
824 |
_samplerate << " isfordry " << isfordry << " dryrun " <<
|
|
|
825 |
_dryrun << "\n");
|
|
|
826 |
if (isfordry) {
|
|
|
827 |
_dryruncv.wait(lock);
|
|
|
828 |
} else {
|
|
|
829 |
_cv.wait(lock);
|
|
|
830 |
}
|
|
|
831 |
}
|
|
|
832 |
LOGDEB("SpotiFetch::waitForHeaders: isfordry " << isfordry <<
|
|
|
833 |
" dryrun " << _dryrun<<" returning "<< spp->isPlaying() << endl);
|
|
|
834 |
return spp->isPlaying();
|
|
|
835 |
}
|
|
|
836 |
void resetStreamFields() {
|
|
|
837 |
_dryrun = false;
|
|
|
838 |
_streamneedinit = true;
|
|
|
839 |
_durationms = 0;
|
|
|
840 |
_initseekmsecs = 0;
|
|
|
841 |
_noheader = false;
|
|
|
842 |
_samplerate = 0;
|
|
|
843 |
_channels = 0;
|
|
|
844 |
_contentlen = 0;
|
|
|
845 |
_totalsent = 0;
|
|
|
846 |
}
|
|
|
847 |
SpotiFetch *p;
|
|
|
848 |
SpotiProxy *spp{nullptr};
|
|
|
849 |
SpotiProxy::AudioSink _sink;
|
|
|
850 |
|
|
|
851 |
bool _dryrun{false};
|
|
|
852 |
bool _streamneedinit{true};
|
|
|
853 |
int _initseekmsecs{0};
|
|
|
854 |
// This is for the case where the offset is non-zero (most often
|
|
|
855 |
// 44 in practise), but small enough that _initseekmsecs is 0.
|
|
|
856 |
bool _noheader{false};
|
|
|
857 |
int _samplerate{0};
|
|
|
858 |
int _channels{0};
|
|
|
859 |
int _durationms{0};
|
|
|
860 |
uint64_t _contentlen{0};
|
|
|
861 |
uint64_t _totalsent{0};
|
|
|
862 |
|
|
|
863 |
condition_variable _cv;
|
|
|
864 |
condition_variable _dryruncv;
|
|
|
865 |
mutex _mutex;
|
|
|
866 |
};
|
|
|
867 |
|
|
|
868 |
bool SpotiFetch::reset()
|
|
|
869 |
{
|
|
|
870 |
LOGDEB("SpotiFetch::reset\n");
|
|
|
871 |
m->spp->stop();
|
|
|
872 |
m->spp->waitForEndOfPlay();
|
|
|
873 |
|
|
|
874 |
m->resetStreamFields();
|
|
|
875 |
return true;
|
|
|
876 |
}
|
|
|
877 |
|
|
|
878 |
SpotiFetch:: SpotiFetch(const std::string& url)
|
|
|
879 |
: NetFetch(url), m(new Internal(this)) {}
|
|
|
880 |
|
|
|
881 |
SpotiFetch::~SpotiFetch() {}
|
|
|
882 |
|
|
|
883 |
bool SpotiFetch::start(BufXChange<ABuffer*> *queue, uint64_t offset)
|
|
|
884 |
{
|
|
|
885 |
LOGDEB("SpotiFetch::start: Offset: " << offset << " queue " <<
|
|
|
886 |
(queue? queue->getname() : "null") << endl);
|
|
|
887 |
|
|
|
888 |
// Flush current queue if any
|
|
|
889 |
if (outqueue) {
|
|
|
890 |
outqueue->waitIdle();
|
|
|
891 |
}
|
|
|
892 |
outqueue = queue;
|
|
|
893 |
|
|
|
894 |
reset();
|
|
|
895 |
|
|
|
896 |
uint64_t v = 0;
|
|
|
897 |
if (offset) {
|
|
|
898 |
m->dodryrun(_url);
|
|
|
899 |
if (m->_samplerate == 0 || m->_channels == 0) {
|
|
|
900 |
LOGERR("SpotiFetch::start: rate or chans 0 after dryrun\n");
|
|
|
901 |
return false;
|
|
|
902 |
} else {
|
|
|
903 |
LOGDEB("SpotiFetch::start: after dryrun rate " << m->_samplerate <<
|
|
|
904 |
" chans " << m->_channels << endl);
|
|
|
905 |
}
|
|
|
906 |
v = (10 * offset) / (m->_channels * 2 * (m->_samplerate/100));
|
|
|
907 |
LOGDEB("SpotiFetch::start: computed seek ms: " << v << " duration " <<
|
|
|
908 |
m->_durationms << endl);
|
|
|
909 |
if (v > uint64_t(m->_durationms)) {
|
|
|
910 |
v = m->_durationms;
|
|
|
911 |
}
|
|
|
912 |
}
|
|
|
913 |
LOGDEB("SpotiFetch::start: seek msecs: " << v << endl);
|
|
|
914 |
m->_initseekmsecs = v;
|
|
|
915 |
|
|
|
916 |
m->_dryrun = false;
|
|
|
917 |
// Reset samplerate so that the external waitForHeaders will only
|
|
|
918 |
// return after we get the first frame and the actual contentlen
|
|
|
919 |
// is computed (and samplerate set again).
|
|
|
920 |
m->_samplerate = 0;
|
|
|
921 |
if (offset) {
|
|
|
922 |
m->_noheader = true;
|
|
|
923 |
}
|
|
|
924 |
return m->spp->startPlay(_url, m->_sink, m->_initseekmsecs);
|
|
|
925 |
}
|
|
|
926 |
|
|
|
927 |
bool SpotiFetch::waitForHeaders(int maxSecs)
|
|
|
928 |
{
|
|
|
929 |
return m->waitForHeadersInternal(maxSecs, false);
|
|
|
930 |
}
|
|
|
931 |
|
|
|
932 |
bool SpotiFetch::headerValue(const std::string& nm, std::string& val)
|
|
|
933 |
{
|
|
|
934 |
if (!stringlowercmp("content-type", nm)) {
|
|
|
935 |
val = "audio/wav";
|
|
|
936 |
LOGDEB1("SpotiFetch::headerValue: content-type: " << val << "\n");
|
|
|
937 |
return true;
|
|
|
938 |
} else if (!stringlowercmp("content-length", nm)) {
|
|
|
939 |
ulltodecstr(m->_contentlen, val);
|
|
|
940 |
LOGDEB("SpotiFetch::headerValue: content-length: " << val << "\n");
|
|
|
941 |
return true;
|
|
|
942 |
}
|
|
|
943 |
return false;
|
|
|
944 |
}
|
|
|
945 |
|
|
|
946 |
bool SpotiFetch::fetchDone(FetchStatus *code, int *http_code)
|
|
|
947 |
{
|
|
|
948 |
bool ret= !m->spp->isPlaying();
|
|
|
949 |
if (ret && code) {
|
|
|
950 |
*code = m->spp->getReason().empty() ? FETCH_OK : FETCH_FATAL;
|
|
|
951 |
}
|
|
|
952 |
if (http_code) {
|
|
|
953 |
*http_code = 0;
|
|
|
954 |
}
|
|
|
955 |
LOGDEB0("SpotiFetch::fetchDone: returning " << ret << endl);
|
|
|
956 |
return ret;
|
|
|
957 |
}
|