owntone-server/src/inputs/spotify_librespotc.c

794 lines
17 KiB
C

/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <pthread.h>
#include <event2/event.h>
#include "input.h"
#include "misc.h"
#include "logger.h"
#include "conffile.h"
#include "listener.h"
#include "http.h"
#include "db.h"
#include "transcode.h"
#include "spotify.h"
#include "librespot-c/librespot-c.h"
// Haven't actually studied ffmpeg's probe size requirements, this is just a
// guess
#define SPOTIFY_PROBE_SIZE_MIN 16384
// The transcoder will say EOF if too little data is provided to it
#define SPOTIFY_BUF_MIN 4096
// Limits how much of the Spotify Ogg file we fetch and buffer (in read_buf).
// This will also in effect throttle in librespot-c.
#define SPOTIFY_BUF_MAX (512 * 1024)
struct global_ctx
{
bool is_initialized;
struct spotify_status status;
struct sp_session *session;
enum sp_bitrates bitrate_preferred;
};
struct download_ctx
{
bool is_started;
bool is_ended;
struct transcode_ctx *xcode;
struct evbuffer *read_buf;
int read_fd;
uint32_t len_ms;
size_t len_bytes;
};
static struct global_ctx spotify_ctx;
// Must be initialized statically since we don't have anywhere to do it at
// runtime. We are in the special situation that multiple threads can result in
// calls to initialize(), e.g. input_init() and library init scan, thus it must
// have the lock ready to use to be thread safe.
static pthread_mutex_t spotify_ctx_lock = PTHREAD_MUTEX_INITIALIZER;
static struct media_quality spotify_quality = { 44100, 16, 2, 0 };
/* ------------------------------ Utility funcs ----------------------------- */
static void
hextobin(uint8_t *data, size_t data_len, const char *hexstr, size_t hexstr_len)
{
char hex[] = { 0, 0, 0 };
const char *ptr;
int i;
if (2 * data_len < hexstr_len)
{
memset(data, 0, data_len);
return;
}
ptr = hexstr;
for (i = 0; i < data_len; i++, ptr+=2)
{
memcpy(hex, ptr, 2);
data[i] = strtol(hex, NULL, 16);
}
}
static int
postlogin(struct global_ctx *ctx)
{
struct sp_credentials credentials;
char *db_stored_cred;
char *ptr;
int i;
int ret;
ret = librespotc_credentials_get(&credentials, ctx->session);
if (ret < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Error getting Spotify credentials: %s\n", librespotc_last_errmsg());
return -1;
}
CHECK_NULL(L_SPOTIFY, db_stored_cred = malloc(2 * credentials.stored_cred_len + 1));
for (i = 0, ptr = db_stored_cred; i < credentials.stored_cred_len; i++)
ptr += sprintf(ptr, "%02x", credentials.stored_cred[i]);
db_admin_set("spotify_username", credentials.username);
db_admin_set("spotify_stored_cred", db_stored_cred);
free(db_stored_cred);
ctx->status.logged_in = true;
snprintf(ctx->status.username, sizeof(ctx->status.username), "%s", credentials.username);
librespotc_bitrate_set(ctx->session, ctx->bitrate_preferred);
DPRINTF(E_LOG, L_SPOTIFY, "Logged into Spotify succesfully with username %s\n", credentials.username);
listener_notify(LISTENER_SPOTIFY);
return 0;
}
static int
login_stored_cred(struct global_ctx *ctx, const char *username, const char *db_stored_cred)
{
size_t db_stored_cred_len;
uint8_t *stored_cred = NULL;
size_t stored_cred_len;
int ret;
db_stored_cred_len = strlen(db_stored_cred);
stored_cred_len = db_stored_cred_len / 2;
CHECK_NULL(L_SPOTIFY, stored_cred = malloc(stored_cred_len));
hextobin(stored_cred, stored_cred_len, db_stored_cred, db_stored_cred_len);
ctx->session = librespotc_login_stored_cred(username, stored_cred, stored_cred_len);
if (!ctx->session)
{
DPRINTF(E_LOG, L_SPOTIFY, "Error logging into Spotify: %s\n", librespotc_last_errmsg());
goto error;
}
ret = postlogin(ctx);
if (ret < 0)
goto error;
free(stored_cred);
return 0;
error:
free(stored_cred);
if (ctx->session)
librespotc_logout(ctx->session);
ctx->session = NULL;
return -1;
}
// If there is evbuf size is below max, reads from a non-blocking fd until error,
// EAGAIN or evbuf full
static int
fd_read(bool *eofptr, struct evbuffer *evbuf, int fd)
{
size_t len = evbuffer_get_length(evbuf);
bool eof = false;
int total = 0;
int ret = 0;
while (len + total < SPOTIFY_BUF_MAX && !eof)
{
ret = evbuffer_read(evbuf, fd, -1); // Each read is 4096 bytes (EVBUFFER_READ_MAX)
if (ret == 0)
eof = true;
else if (ret < 0)
break;
total += ret;
}
if (eofptr)
*eofptr = eof;
if (ret < 0 && errno != EAGAIN)
return ret;
return total;
}
/* ------------------ Callbacks from librespot-c thread --------------------- */
static void
progress_cb(int fd, void *cb_arg, size_t received, size_t len)
{
DPRINTF(E_SPAM, L_SPOTIFY, "Progress %zu/%zu\n", received, len);
}
static int
https_get_cb(char **out, const char *url)
{
struct http_client_ctx ctx = { 0 };
char *body;
size_t len;
int ret;
ctx.url = url;
ctx.input_body = evbuffer_new();
ret = http_client_request(&ctx, NULL);
if (ret < 0 || ctx.response_code != HTTP_OK)
{
DPRINTF(E_LOG, L_SPOTIFY, "Failed to AP list from '%s' (return %d, error code %d)\n", ctx.url, ret, ctx.response_code);
goto error;
}
len = evbuffer_get_length(ctx.input_body);
body = malloc(len + 1);
evbuffer_remove(ctx.input_body, body, len);
body[len] = '\0'; // For safety
*out = body;
evbuffer_free(ctx.input_body);
return 0;
error:
evbuffer_free(ctx.input_body);
return -1;
}
static int
tcp_connect(const char *address, unsigned short port)
{
return net_connect(address, port, SOCK_STREAM, "spotify");
}
static void
tcp_disconnect(int fd)
{
close(fd);
}
static void
thread_name_set(pthread_t thread)
{
thread_setname(thread, "spotify");
}
static void
logmsg_cb(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
DVPRINTF(E_DBG, L_SPOTIFY, fmt, ap);
va_end(ap);
}
static void
hexdump_cb(const char *msg, uint8_t *data, size_t data_len)
{
// DHEXDUMP(E_DBG, L_SPOTIFY, data, data_len, msg);
}
/* ------------------------ librespot-c initialization ---------------------- */
struct sp_callbacks callbacks = {
.https_get = https_get_cb,
.tcp_connect = tcp_connect,
.tcp_disconnect = tcp_disconnect,
.thread_name_set = thread_name_set,
.hexdump = hexdump_cb,
.logmsg = logmsg_cb,
};
// Called from main thread as part of player_init, or from library thread as
// part of relogin. Caller must use mutex for thread safety.
static int
initialize(struct global_ctx *ctx)
{
struct sp_sysinfo sysinfo = { 0 };
cfg_t *spotify_cfg;
int ret;
spotify_cfg = cfg_getsec(cfg, "spotify");
if (ctx->is_initialized)
return 0;
snprintf(sysinfo.device_id, sizeof(sysinfo.device_id), "%" PRIx64, libhash); // TODO use a UUID instead
ret = librespotc_init(&sysinfo, &callbacks);
if (ret < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Error initializing Spotify: %s\n", librespotc_last_errmsg());
goto error;
}
switch (cfg_getint(spotify_cfg, "bitrate"))
{
case 1:
ctx->bitrate_preferred = SP_BITRATE_96;
break;
case 2:
ctx->bitrate_preferred = SP_BITRATE_160;
break;
case 3:
ctx->bitrate_preferred = SP_BITRATE_320;
break;
default:
ctx->bitrate_preferred = SP_BITRATE_ANY;
}
ctx->is_initialized = true;
return 0;
error:
ctx->is_initialized = false;
return -1;
}
/* --------------------- Implementation (input thread) ---------------------- */
static int64_t
download_seek(void *arg, int64_t offset, enum transcode_seek_type type)
{
struct download_ctx *download = arg;
int64_t out;
int ret;
switch (type)
{
case XCODE_SEEK_SIZE:
out = download->len_bytes;
break;
case XCODE_SEEK_SET:
// Flush read buffer
evbuffer_drain(download->read_buf, -1);
ret = librespotc_seek(download->read_fd, offset);
if (ret < 0)
goto error;
fd_read(NULL, download->read_buf, download->read_fd);
out = offset;
break;
default:
goto error;
}
DPRINTF(E_DBG, L_SPOTIFY, "Seek to offset %" PRIi64 " requested, type %d, returning %" PRIi64 "\n", offset, type, out);
return out;
error:
DPRINTF(E_WARN, L_SPOTIFY, "Seek error\n");
return -1;
}
// Has to be called after we have started receiving data, since ffmpeg needs to
// probe the data to find the audio streams
static int
download_xcode_setup(struct download_ctx *download)
{
struct transcode_ctx *xcode;
struct transcode_evbuf_io xcode_evbuf_io = { 0 };
CHECK_NULL(L_SPOTIFY, xcode = malloc(sizeof(struct transcode_ctx)));
xcode_evbuf_io.evbuf = download->read_buf;
xcode_evbuf_io.seekfn = download_seek;
xcode_evbuf_io.seekfn_arg = download;
xcode->decode_ctx = transcode_decode_setup(XCODE_OGG, NULL, DATA_KIND_SPOTIFY, NULL, &xcode_evbuf_io, download->len_ms);
if (!xcode->decode_ctx)
goto error;
xcode->encode_ctx = transcode_encode_setup(XCODE_PCM16, NULL, xcode->decode_ctx, NULL, 0, 0);
if (!xcode->encode_ctx)
goto error;
download->xcode = xcode;
return 0;
error:
transcode_cleanup(&xcode);
return -1;
}
static void
download_free(struct download_ctx *download)
{
if (!download)
return;
if (download->read_fd >= 0)
librespotc_close(download->read_fd);
if (download->read_buf)
evbuffer_free(download->read_buf);
transcode_cleanup(&download->xcode);
free(download);
}
static struct download_ctx *
download_new(int fd, uint32_t len_ms, size_t len_bytes)
{
struct download_ctx *download;
CHECK_NULL(L_SPOTIFY, download = calloc(1, sizeof(struct download_ctx)));
CHECK_NULL(L_SPOTIFY, download->read_buf = evbuffer_new());
download->read_fd = fd;
download->len_ms = len_ms;
download->len_bytes = len_bytes;
return download;
}
static int
stop(struct input_source *source)
{
struct download_ctx *download = source->input_ctx;
DPRINTF(E_DBG, L_SPOTIFY, "stop()\n");
pthread_mutex_lock(&spotify_ctx_lock);
download_free(download);
if (source->evbuf)
evbuffer_free(source->evbuf);
source->input_ctx = NULL;
source->evbuf = NULL;
pthread_mutex_unlock(&spotify_ctx_lock);
return 0;
}
static int
setup(struct input_source *source)
{
struct global_ctx *ctx = &spotify_ctx;
struct download_ctx *download;
struct sp_metadata metadata;
int probe_bytes;
int fd;
int ret;
DPRINTF(E_DBG, L_SPOTIFY, "setup()\n");
pthread_mutex_lock(&spotify_ctx_lock);
fd = librespotc_open(source->path, ctx->session);
if (fd < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Error opening source: %s\n", librespotc_last_errmsg());
goto error;
}
ret = librespotc_metadata_get(&metadata, fd);
if (ret < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Error getting track metadata: %s\n", librespotc_last_errmsg());
goto error;
}
// Seems we have a valid source, now setup a read + decoding context. The
// closing of the fd is from now on part of closing the download_ctx, which is
// done in stop().
download = download_new(fd, source->len_ms, metadata.file_len);
CHECK_NULL(L_SPOTIFY, source->evbuf = evbuffer_new());
CHECK_NULL(L_SPOTIFY, source->input_ctx = download);
source->quality = spotify_quality;
// At this point enough bytes should be ready for transcode setup (ffmpeg probing)
probe_bytes = fd_read(NULL, download->read_buf, fd);
if (probe_bytes < SPOTIFY_PROBE_SIZE_MIN)
{
DPRINTF(E_LOG, L_SPOTIFY, "Not enough audio data for ffmpeg probing (%d)\n", probe_bytes);
goto error;
}
ret = download_xcode_setup(download);
if (ret < 0)
goto error;
pthread_mutex_unlock(&spotify_ctx_lock);
return 0;
error:
pthread_mutex_unlock(&spotify_ctx_lock);
stop(source);
return -1;
}
static int
play(struct input_source *source)
{
struct download_ctx *download = source->input_ctx;
size_t buflen;
int ret;
// Starts the download. We don't do that in setup because the player/input
// might run seek() before starting download.
if (!download->is_started)
{
librespotc_write(download->read_fd, progress_cb, download);
download->is_started = true;
}
if (!download->is_ended)
{
ret = fd_read(&download->is_ended, download->read_buf, download->read_fd);
if (ret < 0)
goto error;
buflen = evbuffer_get_length(download->read_buf);
if (buflen < SPOTIFY_BUF_MIN)
goto wait;
}
// Decode the Ogg Vorbis to PCM in chunks of 16 packets, which is pretty much
// a randomly chosen chunk size
ret = transcode(source->evbuf, NULL, download->xcode, 16);
if (ret == 0)
{
input_write(source->evbuf, &source->quality, INPUT_FLAG_EOF);
stop(source);
return -1;
}
else if (ret < 0)
goto error;
ret = input_write(source->evbuf, &source->quality, 0);
if (ret == EAGAIN)
goto wait;
return 0;
error:
input_write(NULL, NULL, INPUT_FLAG_ERROR);
stop(source);
return -1;
wait:
DPRINTF(E_DBG, L_SPOTIFY, "Waiting for data\n");
input_wait();
return 0;
}
static int
seek(struct input_source *source, int seek_ms)
{
struct download_ctx *download = source->input_ctx;
int ret;
pthread_mutex_lock(&spotify_ctx_lock);
// This will make transcode call back to download_seek(), but with a byte
// offset instead of a ms position, which is what librespot-c requires
ret = transcode_seek(download->xcode, seek_ms);
pthread_mutex_unlock(&spotify_ctx_lock);
return ret;
}
static int
init(void)
{
int ret;
pthread_mutex_lock(&spotify_ctx_lock);
ret = initialize(&spotify_ctx);
pthread_mutex_unlock(&spotify_ctx_lock);
return ret;
}
static void
deinit(void)
{
pthread_mutex_lock(&spotify_ctx_lock);
librespotc_deinit();
pthread_mutex_unlock(&spotify_ctx_lock);
}
struct input_definition input_spotify =
{
.name = "Spotify",
.type = INPUT_TYPE_SPOTIFY,
.disabled = 0,
.setup = setup,
.stop = stop,
.play = play,
.seek = seek,
.init = init,
.deinit = deinit,
};
/* -------------------- Functions exposed via spotify.h --------------------- */
/* Called from other threads than the input thread */
static int
login(const char *username, const char *password, const char **errmsg)
{
struct global_ctx *ctx = &spotify_ctx;
int ret;
pthread_mutex_lock(&spotify_ctx_lock);
ctx->session = librespotc_login_password(username, password);
if (!ctx->session)
goto error;
ret = postlogin(ctx);
if (ret < 0)
goto error;
pthread_mutex_unlock(&spotify_ctx_lock);
return 0;
error:
if (ctx->session)
librespotc_logout(ctx->session);
ctx->session = NULL;
if (errmsg)
*errmsg = librespotc_last_errmsg();
pthread_mutex_unlock(&spotify_ctx_lock);
return -1;
}
static int
login_token(const char *username, const char *token, const char **errmsg)
{
struct global_ctx *ctx = &spotify_ctx;
int ret;
pthread_mutex_lock(&spotify_ctx_lock);
ctx->session = librespotc_login_token(username, token);
if (!ctx->session)
goto error;
ret = postlogin(ctx);
if (ret < 0)
goto error;
pthread_mutex_unlock(&spotify_ctx_lock);
return 0;
error:
if (ctx->session)
librespotc_logout(ctx->session);
ctx->session = NULL;
if (errmsg)
*errmsg = librespotc_last_errmsg();
pthread_mutex_unlock(&spotify_ctx_lock);
return -1;
}
static void
logout(void)
{
struct global_ctx *ctx = &spotify_ctx;
db_admin_delete("spotify_username");
db_admin_delete("spotify_stored_cred");
pthread_mutex_lock(&spotify_ctx_lock);
librespotc_logout(ctx->session);
ctx->session = NULL;
memset(&ctx->status, 0, sizeof(ctx->status));
pthread_mutex_unlock(&spotify_ctx_lock);
listener_notify(LISTENER_SPOTIFY);
}
static int
relogin(void)
{
struct global_ctx *ctx = &spotify_ctx;
char *username = NULL;
char *db_stored_cred = NULL;
int ret;
pthread_mutex_lock(&spotify_ctx_lock);
ret = initialize(ctx);
if (ret < 0)
goto error;
// Re-login if we have stored credentials
db_admin_get(&username, "spotify_username");
db_admin_get(&db_stored_cred, "spotify_stored_cred");
if (username && db_stored_cred)
{
ret = login_stored_cred(ctx, username, db_stored_cred);
if (ret < 0)
goto error;
}
free(username);
free(db_stored_cred);
pthread_mutex_unlock(&spotify_ctx_lock);
return 0;
error:
free(username);
free(db_stored_cred);
pthread_mutex_unlock(&spotify_ctx_lock);
return -1;
}
static void
status_get(struct spotify_status *status)
{
struct global_ctx *ctx = &spotify_ctx;
pthread_mutex_lock(&spotify_ctx_lock);
memcpy(status->username, ctx->status.username, sizeof(status->username));
status->logged_in = ctx->status.logged_in;
status->installed = true;
status->has_podcast_support = true;
pthread_mutex_unlock(&spotify_ctx_lock);
}
struct spotify_backend spotify_librespotc =
{
.login = login,
.login_token = login_token,
.logout = logout,
.relogin = relogin,
.status_get = status_get,
};