Switch to unified view

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
}