[spotify] Reintroduce libspotify support, change spotifyc to librespot-c

Select use of either libspotify or librespot-c as streaming backend via config
option.

librespot-c (renamed/improved spotifyc) impl has the following:
- sync interface
- seek support
- honor bitrate config, set client and thread name
- use web access token with "streaming" scope for login
- fix issue with podcast playback

Also say goodbye to file-based Spotify login.
This commit is contained in:
ejurgensen 2021-04-08 22:53:19 +02:00
parent 2bbc5f16c5
commit 6f0fef6179
77 changed files with 6007 additions and 3755 deletions

View File

@ -1,3 +1,7 @@
if COND_LIBRESPOTC
LIBRESPOTC_SUBDIR=src/inputs/librespot-c
endif
ACLOCAL_AMFLAGS = -I m4
RPM_SPEC_FILE = owntone.spec
@ -8,7 +12,7 @@ sysconf_DATA = $(CONF_FILE)
BUILT_SOURCES = $(CONF_FILE) $(SYSTEMD_SERVICE_FILE)
SUBDIRS = sqlext src htdocs
SUBDIRS = $(LIBRESPOTC_SUBDIR) sqlext src htdocs
dist_man_MANS = owntone.8

View File

@ -287,23 +287,33 @@ AS_IF([[test "x$with_avahi" = "xno"]],
AM_CONDITIONAL([COND_AVAHI], [[test "x$with_avahi" = "xyes"]])
dnl Spotify support
OWNTONE_ARG_ENABLE([Spotify support], [spotify], [SPOTIFY],
[AS_IF([[test "x$with_libevent_pthreads" = "xno"]],
[AC_MSG_ERROR([[Spotify support requires libevent_pthreads]])])
OWNTONE_MODULES_CHECK([OWNTONE_OPTS], [LIBPROTOBUF_C],
OWNTONE_ARG_DISABLE([Spotify support], [spotify], [SPOTIFY_LIBRESPOTC],
[OWNTONE_MODULES_CHECK([OWNTONE_OPTS], [LIBPROTOBUF_C],
[libprotobuf-c >= 1.0.0], [protobuf_c_message_pack],
[protobuf-c/protobuf-c.h], [],
[OWNTONE_FUNC_REQUIRE([OWNTONE_OPTS], [v0 libprotobuf-c],
[LIBPROTOBUF_OLD], [protobuf-c],
[protobuf_c_message_pack],
[google/protobuf-c/protobuf-c.h],
[AC_DEFINE([HAVE_PROTOBUF_OLD], 1,
[Define to 1 if you have libprotobuf < 1.0.0])
[protobuf_old=yes]],
[AC_MSG_ERROR([[Spotify support requires protobuf-c]])])
])
[protobuf-c/protobuf-c.h])
OWNTONE_VAR_PREPEND([OWNTONE_OPTS_LIBS], [inputs/librespot-c/librespot-c.a])
AC_DEFINE([SPOTIFY], 1,
[Define to 1 to enable Spotify])
])
AM_CONDITIONAL([COND_SPOTIFY], [[test "x$enable_spotify" = "xyes"]])
AM_CONDITIONAL([COND_LIBRESPOTC], [[test "x$enable_spotify" = "xyes"]])
dnl Spotify with dynamic linking to libspotify (legacy)
OWNTONE_ARG_ENABLE([legacy libspotify support], [libspotify], [SPOTIFY_LIBSPOTIFY],
[AS_IF([[test "x$with_libevent_pthreads" = "xno"]],
[AC_MSG_ERROR([[libspotify support requires libevent_pthreads]])])
OWNTONE_MODULES_CHECK([SPOTIFY_LIBSPOTIFY], [LIBSPOTIFY], [libspotify],
[], [libspotify/api.h])
dnl Don't link with libspotify, use dynamic linking
AC_SEARCH_LIBS([dlopen], [dl], [],
[AC_MSG_ERROR([[libspotify support requires dlopen]])])
OWNTONE_VAR_PREPEND([OWNTONE_OPTS_CPPFLAGS], [$SPOTIFY_LIBSPOTIFY_CPPFLAGS])
OWNTONE_VAR_PREPEND([OWNTONE_OPTS_LIBS], [-rdynamic])
AC_DEFINE([SPOTIFY], 1,
[Define to 1 to enable Spotify])
])
AM_CONDITIONAL([COND_LIBSPOTIFY], [[test "x$enable_libspotify" = "xyes"]])
AM_CONDITIONAL([COND_SPOTIFY], [[test "x$enable_spotify" = "xyes" -o "x$enable_libspotify" = "xyes"]])
dnl LastFM support
OWNTONE_ARG_DISABLE([LastFM support], [lastfm], [LASTFM])
@ -362,6 +372,11 @@ OWNTONE_GROUP=${withval:-$OWNTONE_USER}
AC_SUBST([OWNTONE_GROUP])
dnl --- End options ---
dnl Unconditional since we always want to produce Makefiles for dist targets
AC_CONFIG_SUBDIRS([
src/inputs/librespot-c
])
AC_CONFIG_FILES([
src/Makefile
sqlext/Makefile

View File

@ -346,10 +346,16 @@ audio {
# Spotify settings (only have effect if Spotify enabled - see README/INSTALL)
spotify {
# The server can stream from Spotify using either its own implementation
# or using Spotify's libspotify (which was deprecated many years ago)
# use_libspotify = false
# Directory where user settings should be stored (credentials)
# (only has effect with libspotify)
# settings_dir = "@localstatedir@/cache/@PACKAGE@/libspotify"
# Cache directory
# (only has effect with libspotify)
# cache_dir = "/tmp"
# Set preferred bitrate for music streaming

View File

@ -3,15 +3,16 @@ sbin_PROGRAMS = owntone
if COND_SPOTIFY
SPOTIFY_SRC = \
library/spotify_webapi.c library/spotify_webapi.h \
inputs/spotify.c inputs/spotify.h \
inputs/spotifyc/spotifyc.c inputs/spotifyc/spotifyc.h \
inputs/spotifyc/shannon/ShannonFast.c \
inputs/spotifyc/shannon/ShannonInternal.h inputs/spotifyc/shannon/Shannon.h \
inputs/spotifyc/proto/keyexchange.pb-c.c inputs/spotifyc/proto/keyexchange.pb-c.h \
inputs/spotifyc/proto/authentication.pb-c.c inputs/spotifyc/proto/authentication.pb-c.h \
inputs/spotifyc/proto/mercury.pb-c.c inputs/spotifyc/proto/mercury.pb-c.h \
inputs/spotifyc/proto/metadata.pb-c.c inputs/spotifyc/proto/metadata.pb-c.h
library/spotify_webapi.c library/spotify_webapi.h inputs/spotify.c inputs/spotify.h
endif
if COND_LIBRESPOTC
LIBRESPOTC_SRC = \
inputs/spotify_librespotc.c
endif
if COND_LIBSPOTIFY
LIBSPOTIFY_SRC = \
inputs/spotify_libspotify.c \
inputs/libspotify/libspotify.c inputs/libspotify/libspotify.h
endif
if COND_LASTFM
@ -142,7 +143,7 @@ owntone_SOURCES = main.c \
outputs/streaming.c outputs/dummy.c outputs/fifo.c \
$(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \
evrtsp/rtsp.c evrtsp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h \
$(SPOTIFY_SRC) \
$(SPOTIFY_SRC) $(LIBRESPOTC_SRC) $(LIBSPOTIFY_SRC) \
$(LASTFM_SRC) \
$(MPD_SRC) \
listener.c listener.h \

View File

@ -188,6 +188,7 @@ static cfg_opt_t sec_fifo[] =
/* Spotify section structure */
static cfg_opt_t sec_spotify[] =
{
CFG_BOOL("use_libspotify", cfg_false, CFGF_NONE),
CFG_STR("settings_dir", STATEDIR "/cache/" PACKAGE "/libspotify", CFGF_NONE),
CFG_STR("cache_dir", "/tmp", CFGF_NONE),
CFG_INT("bitrate", 0, CFGF_NONE),

View File

@ -1282,7 +1282,7 @@ jsonapi_reply_spotify_login(struct httpd_request *hreq)
password = jparse_str_from_obj(request, "password");
if (user && strlen(user) > 0 && password && strlen(password) > 0)
{
ret = spotify_login_user(user, password, &errmsg);
ret = spotify_login(user, password, &errmsg);
if (ret < 0)
{
json_object_object_add(jreply, "success", json_object_new_boolean(false));
@ -1323,6 +1323,7 @@ static int
jsonapi_reply_spotify_logout(struct httpd_request *hreq)
{
#ifdef SPOTIFY
spotifywebapi_purge();
spotify_logout();
#endif
return HTTP_NOCONTENT;

View File

@ -44,7 +44,7 @@ static int
oauth_reply_spotify(struct httpd_request *hreq)
{
char redirect_uri[256];
char *errmsg;
const char *errmsg;
int httpd_port;
int ret;
@ -56,7 +56,6 @@ oauth_reply_spotify(struct httpd_request *hreq)
{
DPRINTF(E_LOG, L_WEB, "Could not parse Spotify OAuth callback: '%s'\n", hreq->uri_parsed->uri);
httpd_send_error(hreq->req, HTTP_INTERNAL, errmsg);
free(errmsg);
return -1;
}

View File

@ -58,9 +58,12 @@ extern struct input_definition input_file;
extern struct input_definition input_http;
extern struct input_definition input_pipe;
extern struct input_definition input_timer;
#ifdef SPOTIFY
#ifdef SPOTIFY_LIBRESPOTC
extern struct input_definition input_spotify;
#endif
#ifdef SPOTIFY_LIBSPOTIFY
extern struct input_definition input_libspotify;
#endif
// Must be in sync with enum input_types
static struct input_definition *inputs[] = {
@ -68,8 +71,11 @@ static struct input_definition *inputs[] = {
&input_http,
&input_pipe,
&input_timer,
#ifdef SPOTIFY
#ifdef SPOTIFY_LIBRESPOTC
&input_spotify,
#endif
#ifdef SPOTIFY_LIBSPOTIFY
&input_libspotify,
#endif
NULL
};
@ -171,10 +177,16 @@ map_data_kind(int data_kind)
case DATA_KIND_PIPE:
return INPUT_TYPE_PIPE;
#ifdef SPOTIFY
case DATA_KIND_SPOTIFY:
return INPUT_TYPE_SPOTIFY;
#ifdef SPOTIFY_LIBRESPOTC
if (!inputs[INPUT_TYPE_SPOTIFY]->disabled)
return INPUT_TYPE_SPOTIFY;
#endif
#ifdef SPOTIFY_LIBSPOTIFY
if (!inputs[INPUT_TYPE_LIBSPOTIFY]->disabled)
return INPUT_TYPE_LIBSPOTIFY;
#endif
return -1;
default:
return -1;
@ -458,6 +470,8 @@ start(void *arg, int *retval)
struct db_queue_item *queue_item;
int ret;
DPRINTF(E_WARN, L_PLAYER, "now %d, item_id %d, now item_id %d\n", input_now_reading.open, cmdarg->item_id, input_now_reading.item_id);
// If we are asked to start the item that is currently open we can just seek
if (input_now_reading.open && cmdarg->item_id == input_now_reading.item_id)
{

View File

@ -16,9 +16,12 @@ enum input_types
INPUT_TYPE_HTTP,
INPUT_TYPE_PIPE,
INPUT_TYPE_TIMER,
#ifdef SPOTIFY
#ifdef SPOTIFY_LIBRESPOTC
INPUT_TYPE_SPOTIFY,
#endif
#ifdef SPOTIFY_LIBSPOTIFY
INPUT_TYPE_LIBSPOTIFY,
#endif
};
enum input_flags

38
src/inputs/librespot-c/.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
*~
*.swp
Makefile.in
Makefile
*.o
*.lo
*.a
*.la
.dirstamp
.deps/
.libs/
# autofoo stuff
autom4te.cache
aclocal.m4
compile
config.guess
config.h
config.h.in
config.log
config.status
config.sub
configure
depcomp
install-sh
libtool
ltmain.sh
missing
stamp-h1
autotools-stamp
build-stamp
ar-lib
/.settings
/.cproject
/.project
/.autotools
/.vscode

View File

@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,26 @@
SUBDIRS = tests
noinst_LIBRARIES = librespot-c.a
SHANNON_SRC = \
src/shannon/ShannonFast.c src/shannon/Shannon.h src/shannon/ShannonInternal.h
PROTO_SRC = \
src/proto/keyexchange.pb-c.c src/proto/keyexchange.pb-c.h \
src/proto/authentication.pb-c.c src/proto/authentication.pb-c.h \
src/proto/mercury.pb-c.c src/proto/mercury.pb-c.h \
src/proto/metadata.pb-c.c src/proto/metadata.pb-c.h
CORE_SRC = \
src/librespot-c.c src/connection.c src/channel.c src/crypto.c src/commands.c
librespot_c_a_SOURCES = \
$(CORE_SRC) \
$(SHANNON_SRC) \
$(PROTO_SRC)
noinst_HEADERS = \
librespot-c.h src/librespot-c-internal.h src/connection.h \
src/channel.h src/crypto.h src/commands.h
EXTRA_DIST = README.md LICENSE

View File

@ -0,0 +1,13 @@
Build:
- autoreconf -i && ./configure && make
Test:
- make check
- ./tests/test1
Dependencies:
- libevent-dev libgcrypt20-dev libcurl4-gnutls-dev libjson-c-dev libprotobuf-c-dev
Credits:
- librespot (https://github.com/librespot-org/librespot)
- timniederhausen for Shannon cipher (https://github.com/timniederhausen/shannon)

View File

@ -0,0 +1,22 @@
AC_INIT([librespot-c], [0.1])
AC_CONFIG_AUX_DIR([.])
AM_INIT_AUTOMAKE([foreign subdir-objects])
AC_PROG_CC
AM_PROG_AR
AC_PROG_RANLIB
AC_CHECK_HEADERS_ONCE([endian.h sys/endian.h])
AC_CHECK_DECL([htobe16], [],
[AC_CHECK_HEADERS([libkern/OSByteOrder.h], [], [AC_MSG_ERROR([[Missing functions to swap byte order]])])],
)
AC_SEARCH_LIBS([pthread_exit], [pthread], [], [AC_MSG_ERROR([[pthreads library is required]])])
PKG_CHECK_MODULES([LIBEVENT], [libevent])
PKG_CHECK_MODULES([JSON_C], [json-c])
PKG_CHECK_MODULES([LIBGCRYPT], [libgcrypt])
PKG_CHECK_MODULES([LIBCURL], [libcurl])
PKG_CHECK_MODULES([LIBPROTOBUF_C], [libprotobuf-c])
AC_CONFIG_FILES([Makefile tests/Makefile])
AC_OUTPUT

View File

@ -0,0 +1,116 @@
#ifndef __LIBRESPOT_C_H__
#define __LIBRESPOT_C_H__
#include <inttypes.h>
#include <stddef.h>
#include <pthread.h>
#define LIBRESPOT_C_VERSION_MAJOR 0
#define LIBRESPOT_C_VERSION_MINOR 1
struct sp_session;
enum sp_bitrates
{
SP_BITRATE_ANY,
SP_BITRATE_96,
SP_BITRATE_160,
SP_BITRATE_320,
};
typedef void (*sp_progress_cb)(int fd, void *arg, size_t received, size_t len);
struct sp_credentials
{
char username[64];
char password[32];
uint8_t stored_cred[256]; // Actual size is 146, but leave room for some more
size_t stored_cred_len;
uint8_t token[256]; // Actual size is ?
size_t token_len;
};
struct sp_metadata
{
size_t file_len;
};
struct sp_sysinfo
{
char client_name[16];
char client_version[16];
char client_build_id[16];
char device_id[41]; // librespot gives a 20 byte id (so 40 char hex + 1 zero term)
};
struct sp_callbacks
{
// Bring your own https client and tcp connector
int (*https_get)(char **body, const char *url);
int (*tcp_connect)(const char *address, unsigned short port);
void (*tcp_disconnect)(int fd);
// Optional - set name of thread
void (*thread_name_set)(pthread_t thread);
// Debugging
void (*hexdump)(const char *msg, uint8_t *data, size_t data_len);
void (*logmsg)(const char *fmt, ...);
};
struct sp_session *
librespotc_login_password(const char *username, const char *password);
struct sp_session *
librespotc_login_stored_cred(const char *username, uint8_t *stored_cred, size_t stored_cred_len);
struct sp_session *
librespotc_login_token(const char *username, const char *token);
int
librespotc_logout(struct sp_session *session);
int
librespotc_bitrate_set(struct sp_session *session, enum sp_bitrates bitrate);
int
librespotc_credentials_get(struct sp_credentials *credentials, struct sp_session *session);
// Returns a file descriptor (in non-blocking mode) from which caller can read
// one chunk of data. To get more data written/start playback loop, call
// librespotc_play().
int
librespotc_open(const char *path, struct sp_session *session);
// Continues writing data to the file descriptor until error or end of track.
// A read of the fd that returns 0 means end of track, and a negative read
// return value means error. progress_cb and cb_arg optional.
void
librespotc_write(int fd, sp_progress_cb progress_cb, void *cb_arg);
// Seeks to pos (measured in bytes, so must not exceed file_len), flushes old
// data from the fd and prepares one chunk of data for reading.
int
librespotc_seek(int fd, size_t pos);
// Closes a track download, incl. the fd.
int
librespotc_close(int fd);
int
librespotc_metadata_get(struct sp_metadata *metadata, int fd);
const char *
librespotc_last_errmsg(void);
int
librespotc_init(struct sp_sysinfo *sysinfo, struct sp_callbacks *callbacks);
void
librespotc_deinit(void);
#endif /* !__LIBRESPOT_C_H__ */

View File

@ -0,0 +1,446 @@
#include <fcntl.h>
#include "librespot-c-internal.h"
/* -------------------------------- Channels -------------------------------- */
/*
Here is my current understanding of the channel concept:
1. A channel is established for retrieving chunks of audio. A channel is not a
separate connection, all the traffic goes via the same Shannon-encrypted tcp
connection as the rest.
2. It depends on the cmd whether a channel is used. CmdStreamChunk,
CmdStreamChunkRes, CmdChannelError, CmdChannelAbort use channels. A channel
is identified with a uint16_t, which is the first 2 bytes of these packets.
3. A channel is established with CmdStreamChunk where receiver picks channel id.
Spotify responds with CmdStreamChunkRes that initially has some headers after
the channel id. The headers are "reverse tlv": uint16_t header length,
uint8_t header id, uint8_t header_data[]. The length includes the id length.
4. After the headers are sent the channel switches to data mode. This is
signalled by a header length of 0. In data mode Spotify sends the requested
chunks of audio (CmdStreamChunkRes) which have the audio right after the
channel id prefix. The audio is AES encrypted with a per-file key. An empty
CmdStreamChunkRes indicates the end. The caller can then make a new
CmdStreamChunk requesting the next data.
5. For Ogg, the first 167 bytes of audio is a special Spotify header.
6. The channel can presumably be reset with CmdChannelAbort (?)
*/
static int
path_to_media_id_and_type(struct sp_file *file)
{
char *ptr;
file->media_type = SP_MEDIA_UNKNOWN;
if (strstr(file->path, ":track:"))
file->media_type = SP_MEDIA_TRACK;
else if (strstr(file->path, ":episode:"))
file->media_type = SP_MEDIA_EPISODE;
else
return -1;
ptr = strrchr(file->path, ':');
if (!ptr || strlen(ptr + 1) != 22)
return -1;
return crypto_base62_to_bin(file->media_id, sizeof(file->media_id), ptr + 1);
}
struct sp_channel *
channel_get(uint32_t channel_id, struct sp_session *session)
{
if (channel_id > sizeof(session->channels)/sizeof(session->channels)[0])
return NULL;
if (!session->channels[channel_id].is_allocated)
return NULL;
return &session->channels[channel_id];
}
void
channel_free(struct sp_channel *channel)
{
if (!channel || !channel->is_allocated)
return;
if (channel->audio_buf)
evbuffer_free(channel->audio_buf);
if (channel->audio_write_ev)
event_free(channel->audio_write_ev);
if (channel->audio_fd[0] >= 0)
close(channel->audio_fd[0]);
if (channel->audio_fd[1] >= 0)
close(channel->audio_fd[1]);
crypto_aes_free(&channel->file.decrypt);
free(channel->file.path);
memset(channel, 0, sizeof(struct sp_channel));
channel->audio_fd[0] = -1;
channel->audio_fd[1] = -1;
}
void
channel_free_all(struct sp_session *session)
{
int i;
for (i = 0; i < sizeof(session->channels)/sizeof(session->channels)[0]; i++)
channel_free(&session->channels[i]);
}
int
channel_new(struct sp_channel **new_channel, struct sp_session *session, const char *path, struct event_base *evbase, event_callback_fn write_cb)
{
struct sp_channel *channel;
uint16_t i = SP_DEFAULT_CHANNEL;
int ret;
channel = &session->channels[i];
channel_free(channel);
channel->id = i;
channel->is_allocated = true;
channel->file.path = strdup(path);
path_to_media_id_and_type(&channel->file);
// Set up the audio I/O
ret = pipe(channel->audio_fd);
if (ret < 0)
goto error;
if (fcntl(channel->audio_fd[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0)
goto error;
if (fcntl(channel->audio_fd[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0)
goto error;
channel->audio_write_ev = event_new(evbase, channel->audio_fd[1], EV_WRITE, write_cb, session);
if (!channel->audio_write_ev)
goto error;
channel->audio_buf = evbuffer_new();
if (!channel->audio_buf)
goto error;
*new_channel = channel;
return 0;
error:
channel_free(channel);
return -1;
}
// Set the fd to non-blocking in case the caller changed that, and then read
// until empty
static int
channel_flush(int fd)
{
uint8_t buf[4096];
int flags;
int got;
flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
return -1;
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
do
got = read(fd, buf, sizeof(buf));
while (got > 0);
fcntl(fd, F_SETFL, flags);
return 0;
}
void
channel_play(struct sp_channel *channel)
{
channel->is_writing = true;
}
void
channel_stop(struct sp_channel *channel)
{
channel->is_writing = false;
// This will tell the reader that there is no more to read. He should then
// call librespotc_close(), which will clean up the rest of the channel via
// channel_free().
close(channel->audio_fd[1]);
channel->audio_fd[1] = -1;
}
int
channel_seek(struct sp_channel *channel, size_t pos)
{
uint32_t seek_words;
int ret;
ret = channel_flush(channel->audio_fd[0]);
if (ret < 0)
RETURN_ERROR(SP_ERR_INVALID, "Could not flush read fd before seeking");
channel->seek_pos = pos;
// If seek + header isn't word aligned we will get up to 3 bytes before the
// actual seek position. We will remove those when they are received.
channel->seek_align = (pos + SP_OGG_HEADER_LEN) % 4;
seek_words = (pos + SP_OGG_HEADER_LEN) / 4;
ret = crypto_aes_seek(&channel->file.decrypt, 4 * seek_words, &sp_errmsg);
if (ret < 0)
RETURN_ERROR(SP_ERR_DECRYPTION, sp_errmsg);
// Set the offset and received counter to match the seek
channel->file.offset_words = seek_words;
channel->file.received_words = seek_words;
return 0;
error:
return ret;
}
void
channel_pause(struct sp_channel *channel)
{
channel_flush(channel->audio_fd[0]);
channel->is_writing = false;
}
// Always returns number of byte read so caller can advance read pointer. If
// header->len == 0 is returned it means that there are no more headers, and
// caller should switch the channel to data mode.
static ssize_t
channel_header_parse(struct sp_channel_header *header, uint8_t *data, size_t data_len)
{
uint8_t *ptr;
uint16_t be;
if (data_len < sizeof(be))
return -1;
ptr = data;
memset(header, 0, sizeof(struct sp_channel_header));
memcpy(&be, ptr, sizeof(be));
header->len = be16toh(be);
ptr += sizeof(be);
if (header->len == 0)
goto done; // No more headers
else if (data_len < header->len + sizeof(be))
return -1;
header->id = ptr[0];
ptr += 1;
header->data = ptr;
header->data_len = header->len - 1;
ptr += header->data_len;
assert(ptr - data == header->len + sizeof(be));
done:
return header->len + sizeof(be);
}
static void
channel_header_handle(struct sp_channel *channel, struct sp_channel_header *header)
{
uint32_t be32;
sp_cb.hexdump("Received header\n", header->data, header->data_len);
// The only header that librespot seems to use is 0x3, which is the audio file
// size in words (incl. headers?)
if (header->id == 0x3)
{
if (header->data_len != sizeof(be32))
{
sp_cb.logmsg("Unexpected header length for header id 0x3\n");
return;
}
memcpy(&be32, header->data, sizeof(be32));
channel->file.len_words = be32toh(be32);
}
}
static ssize_t
channel_header_trailer_read(struct sp_channel *channel, uint8_t *msg, size_t msg_len, struct sp_session *session)
{
ssize_t parsed_len;
ssize_t consumed_len;
int ret;
channel->file.end_of_chunk = false;
channel->file.end_of_file = false;
if (msg_len == 0)
{
channel->file.end_of_chunk = true;
channel->file.end_of_file = (channel->file.received_words >= channel->file.len_words);
// In preparation for next chunk
channel->file.offset_words += SP_CHUNK_LEN_WORDS;
channel->is_data_mode = false;
return 0;
}
else if (channel->is_data_mode)
{
return 0;
}
for (consumed_len = 0; msg_len > 0; msg += parsed_len, msg_len -= parsed_len)
{
parsed_len = channel_header_parse(&channel->header, msg, msg_len);
if (parsed_len < 0)
RETURN_ERROR(SP_ERR_INVALID, "Invalid channel header");
consumed_len += parsed_len;
if (channel->header.len == 0)
{
channel->is_data_mode = true;
break; // All headers read
}
channel_header_handle(channel, &channel->header);
}
return consumed_len;
error:
return ret;
}
static ssize_t
channel_data_read(struct sp_channel *channel, uint8_t *msg, size_t msg_len, struct sp_session *session)
{
const char *errmsg;
int ret;
assert (msg_len % 4 == 0);
channel->file.received_words += msg_len / 4;
ret = crypto_aes_decrypt(msg, msg_len, &channel->file.decrypt, &errmsg);
if (ret < 0)
RETURN_ERROR(SP_ERR_DECRYPTION, errmsg);
// Skip Spotify header
// TODO What to do here when seeking
if (!channel->is_spotify_header_received)
{
if (msg_len < SP_OGG_HEADER_LEN)
RETURN_ERROR(SP_ERR_INVALID, "Invalid data received");
channel->is_spotify_header_received = true;
msg += SP_OGG_HEADER_LEN;
msg_len -= SP_OGG_HEADER_LEN;
}
// See explanation of this in channel_seek()
if (channel->seek_align)
{
msg += channel->seek_align;
msg_len -= channel->seek_align;
channel->seek_align = 0;
}
channel->body.data = msg;
channel->body.data_len = msg_len;
return 0;
error:
return ret;
}
int
channel_data_write(struct sp_channel *channel)
{
ssize_t wrote;
int ret;
wrote = evbuffer_write(channel->audio_buf, channel->audio_fd[1]);
if (wrote < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
return SP_OK_WAIT;
else if (wrote < 0)
RETURN_ERROR(SP_ERR_WRITE, "Error writing to audio pipe");
channel->audio_written_len += wrote;
if (evbuffer_get_length(channel->audio_buf) > 0)
return SP_OK_WAIT;
return SP_OK_DONE;
error:
return ret;
}
int
channel_msg_read(uint16_t *channel_id, uint8_t *msg, size_t msg_len, struct sp_session *session)
{
struct sp_channel *channel;
uint16_t be;
ssize_t consumed_len;
int ret;
if (msg_len < sizeof(be))
RETURN_ERROR(SP_ERR_INVALID, "Chunk response is too small");
memcpy(&be, msg, sizeof(be));
*channel_id = be16toh(be);
channel = channel_get(*channel_id, session);
if (!channel)
{
sp_cb.hexdump("Message with unknown channel\n", msg, msg_len);
RETURN_ERROR(SP_ERR_INVALID, "Could not recognize channel in chunk response");
}
msg += sizeof(be);
msg_len -= sizeof(be);
// Will set data_mode, end_of_file and end_of_chunk as appropriate
consumed_len = channel_header_trailer_read(channel, msg, msg_len, session);
if (consumed_len < 0)
RETURN_ERROR((int)consumed_len, sp_errmsg);
msg += consumed_len;
msg_len -= consumed_len;
channel->body.data = NULL;
channel->body.data_len = 0;
if (!channel->is_data_mode || !(msg_len > 0))
return 0; // Not in data mode or no data to read
consumed_len = channel_data_read(channel, msg, msg_len, session);
if (consumed_len < 0)
RETURN_ERROR((int)consumed_len, sp_errmsg);
return 0;
error:
return ret;
}

View File

@ -0,0 +1,29 @@
struct sp_channel *
channel_get(uint32_t channel_id, struct sp_session *session);
void
channel_free(struct sp_channel *channel);
void
channel_free_all(struct sp_session *session);
int
channel_new(struct sp_channel **channel, struct sp_session *session, const char *path, struct event_base *evbase, event_callback_fn write_cb);
int
channel_data_write(struct sp_channel *channel);
void
channel_play(struct sp_channel *channel);
void
channel_stop(struct sp_channel *channel);
int
channel_seek(struct sp_channel *channel, size_t pos);
void
channel_pause(struct sp_channel *channel);
int
channel_msg_read(uint16_t *channel_id, uint8_t *msg, size_t msg_len, struct sp_session *session);

View File

@ -0,0 +1,423 @@
/*
* Copyright (C) 2016 Christian Meffert <christian.meffert@googlemail.com>
*
* 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
*/
#include "commands.h"
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
struct command
{
pthread_mutex_t lck;
pthread_cond_t cond;
command_function func;
command_function func_bh;
void *arg;
int nonblock;
int ret;
int pending;
};
struct commands_base
{
struct event_base *evbase;
command_exit_cb exit_cb;
int command_pipe[2];
struct event *command_event;
struct command *current_cmd;
};
static int
mutex_init(pthread_mutex_t *mutex)
{
pthread_mutexattr_t mattr;
int err;
pthread_mutexattr_init(&mattr);
pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_ERRORCHECK);
err = pthread_mutex_init(mutex, &mattr);
pthread_mutexattr_destroy(&mattr);
return err;
}
/*
* Asynchronous execution of the command function
*/
static void
command_cb_async(struct commands_base *cmdbase, struct command *cmd)
{
enum command_state cmdstate;
// Command is executed asynchronously
cmdstate = cmd->func(cmd->arg, &cmd->ret);
// Only free arg if there are no pending events (used in worker.c)
if (cmdstate != COMMAND_PENDING && cmd->arg)
free(cmd->arg);
free(cmd);
event_add(cmdbase->command_event, NULL);
}
/*
* Synchronous execution of the command function
*/
static void
command_cb_sync(struct commands_base *cmdbase, struct command *cmd)
{
enum command_state cmdstate;
pthread_mutex_lock(&cmd->lck);
cmdstate = cmd->func(cmd->arg, &cmd->ret);
if (cmdstate == COMMAND_PENDING)
{
// Command execution is waiting for pending events before returning to the caller
cmdbase->current_cmd = cmd;
cmd->pending = cmd->ret;
}
else
{
// Command execution finished, execute the bottom half function
if (cmd->ret == 0 && cmd->func_bh)
cmd->func_bh(cmd->arg, &cmd->ret);
event_add(cmdbase->command_event, NULL);
// Signal the calling thread that the command execution finished
pthread_cond_signal(&cmd->cond);
pthread_mutex_unlock(&cmd->lck);
// Note if cmd->func was cmdloop_exit then cmdbase may be invalid now,
// because commands_base_destroy() may have freed it
}
}
/*
* Event callback function
*
* Function is triggered by libevent if there is data to read on the command pipe (writing to the command pipe happens through
* the send_command function).
*/
static void
command_cb(int fd, short what, void *arg)
{
struct commands_base *cmdbase;
struct command *cmd;
int ret;
cmdbase = arg;
// Get the command to execute from the pipe
ret = read(cmdbase->command_pipe[0], &cmd, sizeof(cmd));
if (ret != sizeof(cmd))
{
// Incorrect length, ignore
event_add(cmdbase->command_event, NULL);
return;
}
// Execute the command function
if (cmd->nonblock)
{
// Command is executed asynchronously
command_cb_async(cmdbase, cmd);
}
else
{
// Command is executed synchronously, caller is waiting until signaled that the execution finished
command_cb_sync(cmdbase, cmd);
}
}
/*
* Writes the given command to the command pipe
*/
static int
send_command(struct commands_base *cmdbase, struct command *cmd)
{
int ret;
if (!cmd->func)
{
errno = EINVAL;
return -1;
}
ret = write(cmdbase->command_pipe[1], &cmd, sizeof(cmd));
if (ret != sizeof(cmd))
{
// errno set by write()
return -1;
}
return 0;
}
/*
* Frees the command base and closes the (internally used) pipes
*/
int
commands_base_free(struct commands_base *cmdbase)
{
if (cmdbase->command_event)
event_free(cmdbase->command_event);
close(cmdbase->command_pipe[0]);
close(cmdbase->command_pipe[1]);
free(cmdbase);
return 0;
}
/*
* Creates a new command base, needs to be freed by commands_base_destroy or commands_base_free.
*
* @param evbase The libevent base to use for command handling
* @param exit_cb Optional callback function to be called during commands_base_destroy
*/
struct commands_base *
commands_base_new(struct event_base *evbase, command_exit_cb exit_cb)
{
struct commands_base *cmdbase;
int ret;
cmdbase = calloc(1, sizeof(struct commands_base));
#ifdef HAVE_PIPE2
ret = pipe2(cmdbase->command_pipe, O_CLOEXEC);
#else
ret = pipe(cmdbase->command_pipe);
#endif
if (ret < 0)
{
// errno set by pipe
free(cmdbase);
return NULL;
}
cmdbase->command_event = event_new(evbase, cmdbase->command_pipe[0], EV_READ, command_cb, cmdbase);
if (!cmdbase->command_event)
{
commands_base_free(cmdbase);
errno = ENOMEM;
return NULL;
}
ret = event_add(cmdbase->command_event, NULL);
if (ret != 0)
{
commands_base_free(cmdbase);
errno = ENOMEM;
return NULL;
}
cmdbase->evbase = evbase;
cmdbase->exit_cb = exit_cb;
return cmdbase;
}
/*
* Gets the current return value for the current pending command.
*
* If a command has more than one pending event, each event can access the previous set return value
* if it depends on it.
*
* @param cmdbase The command base
* @return The current return value
*/
int
commands_exec_returnvalue(struct commands_base *cmdbase)
{
if (cmdbase->current_cmd == NULL)
return 0;
return cmdbase->current_cmd->ret;
}
/*
* If a command function returned COMMAND_PENDING, each event triggered by this command needs to
* call command_exec_end, passing it the return value of the event execution.
*
* If a command function is waiting for multiple events, each event needs to call command_exec_end.
* The command base keeps track of the number of still pending events and only returns to the caller
* if there are no pending events left.
*
* @param cmdbase The command base (holds the current pending command)
* @param retvalue The return value for the calling thread
*/
void
commands_exec_end(struct commands_base *cmdbase, int retvalue)
{
struct command *current_cmd = cmdbase->current_cmd;
if (!current_cmd)
return;
// A pending event finished, decrease the number of pending events and update the return value
current_cmd->pending--;
current_cmd->ret = retvalue;
// If there are still pending events return
if (current_cmd->pending > 0)
return;
// All pending events have finished, execute the bottom half and signal the caller that the command execution finished
if (current_cmd->func_bh)
current_cmd->func_bh(current_cmd->arg, &current_cmd->ret);
cmdbase->current_cmd = NULL;
/* Process commands again */
event_add(cmdbase->command_event, NULL);
pthread_cond_signal(&current_cmd->cond);
pthread_mutex_unlock(&current_cmd->lck);
}
/*
* Execute the function 'func' with the given argument 'arg' in the event loop thread.
* Blocks the caller (thread) until the function returned.
*
* If a function 'func_bh' ("bottom half") is given, it is executed after 'func' has successfully
* finished.
*
* @param cmdbase The command base
* @param func The function to be executed
* @param func_bh The bottom half function to be executed after all pending events from func are processed
* @param arg Argument passed to func (and func_bh)
* @return Return value of func (or func_bh if func_bh is not NULL)
*/
int
commands_exec_sync(struct commands_base *cmdbase, command_function func, command_function func_bh, void *arg)
{
struct command cmd;
int errsv = 0;
int ret;
memset(&cmd, 0, sizeof(struct command));
cmd.func = func;
cmd.func_bh = func_bh;
cmd.arg = arg;
cmd.nonblock = 0;
mutex_init(&cmd.lck);
pthread_cond_init(&cmd.cond, NULL);
pthread_mutex_lock(&cmd.lck);
ret = send_command(cmdbase, &cmd);
if (ret < 0)
{
errsv = errno;
cmd.ret = -1;
}
else
{
pthread_cond_wait(&cmd.cond, &cmd.lck);
}
// May change errno, but we don't care about that
pthread_mutex_unlock(&cmd.lck);
pthread_cond_destroy(&cmd.cond);
pthread_mutex_destroy(&cmd.lck);
errno = errsv;
return cmd.ret;
}
/*
* Execute the function 'func' with the given argument 'arg' in the event loop thread.
* Triggers the function execution and immediately returns (does not wait for func to finish).
*
* The pointer passed as argument is freed in the event loop thread after func returned.
*
* @param cmdbase The command base
* @param func The function to be executed
* @param arg Argument passed to func
* @return 0 if triggering the function execution succeeded, -1 on failure.
*/
int
commands_exec_async(struct commands_base *cmdbase, command_function func, void *arg)
{
struct command *cmd;
int ret;
cmd = calloc(1, sizeof(struct command));
cmd->func = func;
cmd->func_bh = NULL;
cmd->arg = arg;
cmd->nonblock = 1;
ret = send_command(cmdbase, cmd);
if (ret < 0)
{
free(cmd);
return -1;
}
return 0;
}
/*
* Command to break the libevent loop
*
* If the command base was created with an exit_cb function, exit_cb is called before breaking the
* libevent loop.
*
* @param arg The command base
* @param retval Always set to COMMAND_END
*/
static enum command_state
cmdloop_exit(void *arg, int *retval)
{
struct commands_base *cmdbase = arg;
*retval = 0;
if (cmdbase->exit_cb)
cmdbase->exit_cb();
event_base_loopbreak(cmdbase->evbase);
return COMMAND_END;
}
/*
* Break the libevent loop for the given command base, closes the internally used pipes
* and frees the command base.
*
* @param cmdbase The command base
*/
void
commands_base_destroy(struct commands_base *cmdbase)
{
commands_exec_sync(cmdbase, cmdloop_exit, NULL, cmdbase);
commands_base_free(cmdbase);
}

View File

@ -0,0 +1,56 @@
#ifndef SRC_COMMANDS_H_
#define SRC_COMMANDS_H_
#include <event2/event.h>
enum command_state {
COMMAND_END = 0,
COMMAND_PENDING = 1,
};
/*
* Function that will be executed in the event loop thread.
*
* If the function has pending events to complete, it needs to return
* COMMAND_PENDING with 'ret' set to the number of pending events to wait for.
*
* If the function returns with COMMAND_END, command execution will proceed
* with the "bottem half" function (if passed to the command_exec function) only
* if 'ret' is 0.
*
* @param arg Opaque pointer passed by command_exec_sync or command_exec_async
* @param ret Pointer to the return value for the caller of the command
* @return COMMAND_END if there are no pending events (function execution is
* complete) or COMMAND_PENDING if there are pending events
*/
typedef enum command_state (*command_function)(void *arg, int *ret);
typedef void (*command_exit_cb)(void);
struct commands_base;
struct commands_base *
commands_base_new(struct event_base *evbase, command_exit_cb exit_cb);
int
commands_base_free(struct commands_base *cmdbase);
int
commands_exec_returnvalue(struct commands_base *cmdbase);
void
commands_exec_end(struct commands_base *cmdbase, int retvalue);
int
commands_exec_sync(struct commands_base *cmdbase, command_function func, command_function func_bh, void *arg);
int
commands_exec_async(struct commands_base *cmdbase, command_function func, void *arg);
void
commands_base_destroy(struct commands_base *cmdbase);
#endif /* SRC_COMMANDS_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
void
ap_disconnect(struct sp_connection *conn);
enum sp_error
ap_connect(enum sp_msg_type type, struct sp_conn_callbacks *cb, struct sp_session *session);
enum sp_error
response_read(struct sp_session *session);
int
msg_make(struct sp_message *msg, enum sp_msg_type type, struct sp_session *session);
int
msg_send(struct sp_message *msg, struct sp_connection *conn);
int
msg_pong(struct sp_session *session);

View File

@ -0,0 +1,464 @@
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h> // for isdigit(), isupper(), islower()
#include "librespot-c-internal.h" // For endian compat functions
#include "crypto.h"
/* ----------------------------------- Crypto ------------------------------- */
#define SHA512_DIGEST_LENGTH 64
#define bnum_new(bn) \
do { \
if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) { \
if (!gcry_check_version("1.5.4")) \
abort(); \
gcry_control(GCRYCTL_DISABLE_SECMEM, 0); \
gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); \
} \
bn = gcry_mpi_new(1); \
} while (0)
#define bnum_free(bn) gcry_mpi_release(bn)
#define bnum_num_bytes(bn) (gcry_mpi_get_nbits(bn) + 7) / 8
#define bnum_is_zero(bn) (gcry_mpi_cmp_ui(bn, (unsigned long)0) == 0)
#define bnum_bn2bin(bn, buf, len) gcry_mpi_print(GCRYMPI_FMT_USG, buf, len, NULL, bn)
#define bnum_bin2bn(bn, buf, len) gcry_mpi_scan(&bn, GCRYMPI_FMT_USG, buf, len, NULL)
#define bnum_hex2bn(bn, buf) gcry_mpi_scan(&bn, GCRYMPI_FMT_HEX, buf, 0, 0)
#define bnum_random(bn, num_bits) gcry_mpi_randomize(bn, num_bits, GCRY_WEAK_RANDOM)
#define bnum_add(bn, a, b) gcry_mpi_add(bn, a, b)
#define bnum_sub(bn, a, b) gcry_mpi_sub(bn, a, b)
#define bnum_mul(bn, a, b) gcry_mpi_mul(bn, a, b)
#define bnum_mod(bn, a, b) gcry_mpi_mod(bn, a, b)
typedef gcry_mpi_t bnum;
__attribute__((unused)) static void bnum_modexp(bnum bn, bnum y, bnum q, bnum p)
{
gcry_mpi_powm(bn, y, q, p);
}
__attribute__((unused)) static void bnum_modadd(bnum bn, bnum a, bnum b, bnum m)
{
gcry_mpi_addm(bn, a, b, m);
}
static const uint8_t generator_bytes[] = { 0x2 };
static const uint8_t prime_bytes[] =
{
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34,
0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74,
0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd,
0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37,
0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6,
0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
};
static void
crypto_log(const char *fmt, ...)
{
return;
}
/*
static void
crypto_hexdump(const char *msg, uint8_t *mem, size_t len)
{
return;
}
*/
int
crypto_keys_set(struct crypto_keys *keys)
{
bnum generator;
bnum prime;
bnum private_key;
bnum public_key;
bnum_bin2bn(generator, generator_bytes, sizeof(generator_bytes));
bnum_bin2bn(prime, prime_bytes, sizeof(prime_bytes));
bnum_new(private_key);
bnum_new(public_key);
// bnum_random(private_key, 8 * (sizeof(keys->private_key) - 1)); // Not sure why it is 95 bytes?
bnum_random(private_key, 8 * sizeof(keys->private_key));
bnum_modexp(public_key, generator, private_key, prime);
memset(keys, 0, sizeof(struct crypto_keys));
bnum_bn2bin(private_key, keys->private_key, sizeof(keys->private_key));
bnum_bn2bin(public_key, keys->public_key, sizeof(keys->public_key));
bnum_free(generator);
bnum_free(prime);
bnum_free(private_key);
bnum_free(public_key);
return 0;
}
void
crypto_shared_secret(uint8_t **shared_secret_bytes, size_t *shared_secret_bytes_len,
uint8_t *private_key_bytes, size_t private_key_bytes_len,
uint8_t *server_key_bytes, size_t server_key_bytes_len)
{
bnum private_key;
bnum server_key;
bnum prime;
bnum shared_secret;
bnum_bin2bn(private_key, private_key_bytes, private_key_bytes_len);
bnum_bin2bn(server_key, server_key_bytes, server_key_bytes_len);
bnum_bin2bn(prime, prime_bytes, sizeof(prime_bytes));
bnum_new(shared_secret);
bnum_modexp(shared_secret, server_key, private_key, prime);
*shared_secret_bytes_len = bnum_num_bytes(shared_secret);
*shared_secret_bytes = malloc(*shared_secret_bytes_len);
bnum_bn2bin(shared_secret, *shared_secret_bytes, *shared_secret_bytes_len);
bnum_free(private_key);
bnum_free(server_key);
bnum_free(prime);
bnum_free(shared_secret);
}
// Calculates challenge and send/receive keys. The challenge is allocated,
// caller must free
int
crypto_challenge(uint8_t **challenge, size_t *challenge_len,
uint8_t *send_key, size_t send_key_len,
uint8_t *recv_key, size_t recv_key_len,
uint8_t *packets, size_t packets_len,
uint8_t *shared_secret, size_t shared_secret_len)
{
gcry_mac_hd_t hd = NULL;
uint8_t data[0x64];
uint8_t i;
size_t offset;
size_t len;
if (gcry_mac_open(&hd, GCRY_MAC_HMAC_SHA1, 0, NULL) != GPG_ERR_NO_ERROR)
goto error;
if (gcry_mac_setkey(hd, shared_secret, shared_secret_len) != GPG_ERR_NO_ERROR)
goto error;
offset = 0;
for (i = 1; i <= 6; i++)
{
gcry_mac_write(hd, packets, packets_len);
gcry_mac_write(hd, &i, sizeof(i));
len = sizeof(data) - offset;
gcry_mac_read(hd, data + offset, &len);
offset += len;
gcry_mac_reset(hd);
}
gcry_mac_close(hd);
hd = NULL;
assert(send_key_len == 32);
assert(recv_key_len == 32);
memcpy(send_key, data + 20, send_key_len);
memcpy(recv_key, data + 52, recv_key_len);
// Calculate challenge
if (gcry_mac_open(&hd, GCRY_MAC_HMAC_SHA1, 0, NULL) != GPG_ERR_NO_ERROR)
goto error;
if (gcry_mac_setkey(hd, data, 20) != GPG_ERR_NO_ERROR)
goto error;
gcry_mac_write(hd, packets, packets_len);
*challenge_len = gcry_mac_get_algo_maclen(GCRY_MAC_HMAC_SHA1);
*challenge = malloc(*challenge_len);
gcry_mac_read(hd, *challenge, challenge_len);
gcry_mac_close(hd);
return 0;
error:
if (hd)
gcry_mac_close(hd);
return -1;
}
// Inplace encryption, buf_len must be larger than plain_len so that the mac
// can be added
ssize_t
crypto_encrypt(uint8_t *buf, size_t buf_len, size_t plain_len, struct crypto_cipher *cipher)
{
uint32_t nonce;
uint8_t mac[4];
size_t encrypted_len;
encrypted_len = plain_len + sizeof(mac);
if (encrypted_len > buf_len)
return -1;
shn_key(&cipher->shannon, cipher->key, sizeof(cipher->key));
nonce = htobe32(cipher->nonce);
shn_nonce(&cipher->shannon, (uint8_t *)&nonce, sizeof(nonce));
shn_encrypt(&cipher->shannon, buf, plain_len);
shn_finish(&cipher->shannon, mac, sizeof(mac));
memcpy(buf + plain_len, mac, sizeof(mac));
cipher->nonce++;
return encrypted_len;
}
static size_t
payload_len_get(uint8_t *header)
{
uint16_t be;
memcpy(&be, header + 1, sizeof(be));
return (size_t)be16toh(be);
}
// *encrypted will consist of a header (3 bytes, encrypted), payload length (2
// bytes, encrypted, BE), the encrypted payload and then the mac (4 bytes, not
// encrypted). The return will be the number of bytes decrypted (incl mac if a
// whole packet was decrypted). Zero means not enough data for a packet.
ssize_t
crypto_decrypt(uint8_t *encrypted, size_t encrypted_len, struct crypto_cipher *cipher)
{
uint32_t nonce;
uint8_t mac[4];
size_t header_len = sizeof(cipher->last_header);
size_t payload_len;
crypto_log("Decrypting %zu bytes with nonce %u\n", encrypted_len, cipher->nonce);
// crypto_hexdump("Key\n", cipher->key, sizeof(cipher->key));
// crypto_hexdump("Encrypted\n", encrypted, encrypted_len);
// In case we didn't even receive the basics, header and mac, then return.
if (encrypted_len < header_len + sizeof(mac))
{
crypto_log("Waiting for %zu header bytes, have %zu\n", header_len + sizeof(mac), encrypted_len);
return 0;
}
// Will be zero if this is the first pass
payload_len = payload_len_get(cipher->last_header);
if (!payload_len)
{
shn_key(&cipher->shannon, cipher->key, sizeof(cipher->key));
nonce = htobe32(cipher->nonce);
shn_nonce(&cipher->shannon, (uint8_t *)&nonce, sizeof(nonce));
// Decrypt header to get the size, save it in case another pass will be
// required
shn_decrypt(&cipher->shannon, encrypted, header_len);
memcpy(cipher->last_header, encrypted, header_len);
payload_len = payload_len_get(cipher->last_header);
// crypto_log("Payload len is %zu\n", payload_len);
// crypto_hexdump("Decrypted header\n", encrypted, header_len);
}
// At this point the header is already decrypted, so now decrypt the payload
encrypted += header_len;
encrypted_len -= header_len + sizeof(mac);
// Not enough data for decrypting the entire packet
if (payload_len > encrypted_len)
{
crypto_log("Waiting for %zu payload bytes, have %zu\n", payload_len, encrypted_len);
return 0;
}
shn_decrypt(&cipher->shannon, encrypted, payload_len);
// crypto_hexdump("Decrypted payload\n", encrypted, payload_len);
shn_finish(&cipher->shannon, mac, sizeof(mac));
// crypto_hexdump("mac in\n", encrypted + payload_len, sizeof(mac));
// crypto_hexdump("mac our\n", mac, sizeof(mac));
if (memcmp(mac, encrypted + payload_len, sizeof(mac)) != 0)
{
crypto_log("MAC VALIDATION FAILED\n"); // TODO
memset(cipher->last_header, 0, header_len);
return -1;
}
cipher->nonce++;
memset(cipher->last_header, 0, header_len);
return header_len + payload_len + sizeof(mac);
}
void
crypto_aes_free(struct crypto_aes_cipher *cipher)
{
if (!cipher || !cipher->aes)
return;
gcry_cipher_close(cipher->aes);
}
int
crypto_aes_new(struct crypto_aes_cipher *cipher, uint8_t *key, size_t key_len, uint8_t *iv, size_t iv_len, const char **errmsg)
{
gcry_error_t err;
err = gcry_cipher_open(&cipher->aes, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_CTR, 0);
if (err)
{
*errmsg = "Error initialising AES 128 CTR decryption";
goto error;
}
err = gcry_cipher_setkey(cipher->aes, key, key_len);
if (err)
{
*errmsg = "Could not set key for AES 128 CTR";
goto error;
}
err = gcry_cipher_setctr(cipher->aes, iv, iv_len);
if (err)
{
*errmsg = "Could not set iv for AES 128 CTR";
goto error;
}
memcpy(cipher->aes_iv, iv, iv_len);
return 0;
error:
crypto_aes_free(cipher);
return -1;
}
int
crypto_aes_seek(struct crypto_aes_cipher *cipher, size_t seek, const char **errmsg)
{
gcry_error_t err;
uint64_t be64;
uint64_t ctr;
uint8_t iv[16];
size_t iv_len;
size_t num_blocks;
size_t offset;
iv_len = gcry_cipher_get_algo_blklen(GCRY_CIPHER_AES128);
assert(iv_len == sizeof(iv));
memcpy(iv, cipher->aes_iv, iv_len);
num_blocks = seek / iv_len;
offset = seek % iv_len;
// Advance the block counter
memcpy(&be64, iv + iv_len / 2, iv_len / 2);
ctr = be64toh(be64);
ctr += num_blocks;
be64 = htobe64(ctr);
memcpy(iv + iv_len / 2, &be64, iv_len / 2);
err = gcry_cipher_setctr(cipher->aes, iv, iv_len);
if (err)
{
*errmsg = "Could not set iv for AES 128 CTR";
return -1;
}
// Advance if the seek is into a block. iv is used because we have it already,
// it could be any buffer as long as it big enough
err = gcry_cipher_decrypt(cipher->aes, iv, offset, NULL, 0);
if (err)
{
*errmsg = "Error CTR offset while seeking";
return -1;
}
return 0;
}
int
crypto_aes_decrypt(uint8_t *encrypted, size_t encrypted_len, struct crypto_aes_cipher *cipher, const char **errmsg)
{
gcry_error_t err;
err = gcry_cipher_decrypt(cipher->aes, encrypted, encrypted_len, NULL, 0);
if (err)
{
*errmsg = "Error CTR decrypting";
return -1;
}
return 0;
}
static unsigned char
crypto_base62_digit(char c)
{
if (isdigit(c))
return c - '0';
else if (islower(c))
return c - 'a' + 10;
else if (isupper(c))
return c - 'A' + 10 + 26;
else
return 0xff;
}
// base 62 to bin: 4gtj0ZuMWRw8WioT9SXsC2 -> 8c283882b29346829b8d021f52f5c2ce
// 00AdHZ94Jb7oVdHVJmJsIU -> 004f421c7e934635aaf778180a8fd068
// (note that the function prefixes with zeroes)
int
crypto_base62_to_bin(uint8_t *out, size_t out_len, const char *in)
{
uint8_t u8;
bnum n;
bnum base;
bnum digit;
const char *ptr;
size_t len;
u8 = 62;
bnum_bin2bn(base, &u8, sizeof(u8));
bnum_new(n);
for (ptr = in; *ptr; ptr++)
{
// n = 62 * n + base62_digit(*p);
bnum_mul(n, n, base);
u8 = crypto_base62_digit(*ptr);
// Heavy on alloc's, but means we can use bnum compability wrapper
bnum_bin2bn(digit, &u8, sizeof(u8));
bnum_add(n, n, digit);
bnum_free(digit);
}
len = bnum_num_bytes(n);
if (len > out_len)
goto error;
memset(out, 0, out_len - len);
bnum_bn2bin(n, out + out_len - len, len);
bnum_free(n);
bnum_free(base);
return (int)out_len;
error:
bnum_free(n);
bnum_free(base);
return -1;
}

View File

@ -0,0 +1,75 @@
#ifndef __CRYPTO_H__
#define __CRYPTO_H__
#include <inttypes.h>
#include <stddef.h>
#include <gcrypt.h>
#include "shannon/Shannon.h"
struct crypto_cipher
{
shn_ctx shannon;
uint8_t key[32];
uint32_t nonce;
uint8_t last_header[3]; // uint8 cmd and uint16 BE size
void (*logmsg)(const char *fmt, ...);
};
struct crypto_aes_cipher
{
gcry_cipher_hd_t aes;
uint8_t key[16];
uint8_t aes_iv[16];
};
struct crypto_keys
{
uint8_t private_key[96];
uint8_t public_key[96];
uint8_t *shared_secret;
size_t shared_secret_len;
};
void
crypto_shared_secret(uint8_t **shared_secret_bytes, size_t *shared_secret_bytes_len,
uint8_t *private_key_bytes, size_t private_key_bytes_len,
uint8_t *server_key_bytes, size_t server_key_bytes_len);
int
crypto_challenge(uint8_t **challenge, size_t *challenge_len,
uint8_t *send_key, size_t send_key_len,
uint8_t *recv_key, size_t recv_key_len,
uint8_t *packets, size_t packets_len,
uint8_t *shared_secret, size_t shared_secret_len);
int
crypto_keys_set(struct crypto_keys *keys);
ssize_t
crypto_encrypt(uint8_t *buf, size_t buf_len, size_t plain_len, struct crypto_cipher *cipher);
ssize_t
crypto_decrypt(uint8_t *encrypted, size_t encrypted_len, struct crypto_cipher *cipher);
void
crypto_aes_free(struct crypto_aes_cipher *cipher);
int
crypto_aes_new(struct crypto_aes_cipher *cipher, uint8_t *key, size_t key_len, uint8_t *iv, size_t iv_len, const char **errmsg);
int
crypto_aes_seek(struct crypto_aes_cipher *cipher, size_t seek, const char **errmsg);
int
crypto_aes_decrypt(uint8_t *encrypted, size_t encrypted_len, struct crypto_aes_cipher *cipher, const char **errmsg);
int
crypto_base62_to_bin(uint8_t *out, size_t out_len, const char *in);
#endif /* __CRYPTO_H__ */

View File

@ -0,0 +1,343 @@
#ifndef __LIBRESPOT_C_INTERNAL_H__
#define __LIBRESPOT_C_INTERNAL_H__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdbool.h>
#include <unistd.h>
#include <event2/event.h>
#include <event2/buffer.h>
#ifdef HAVE_ENDIAN_H
# include <endian.h>
#elif defined(HAVE_SYS_ENDIAN_H)
# include <sys/endian.h>
#elif defined(HAVE_LIBKERN_OSBYTEORDER_H)
#include <libkern/OSByteOrder.h>
#define htobe16(x) OSSwapHostToBigInt16(x)
#define be16toh(x) OSSwapBigToHostInt16(x)
#define htobe32(x) OSSwapHostToBigInt32(x)
#define be32toh(x) OSSwapBigToHostInt32(x)
#define htobe64(x) OSSwapHostToBigInt64(x)
#define be64toh(x) OSSwapBigToHostInt64(x)
#endif
#include "librespot-c.h"
#include "crypto.h"
#include "proto/keyexchange.pb-c.h"
#include "proto/authentication.pb-c.h"
#include "proto/mercury.pb-c.h"
#include "proto/metadata.pb-c.h"
#define SP_AP_RESOLVE_URL "https://APResolve.spotify.com/"
#define SP_AP_RESOLVE_KEY "ap_list"
// Disconnect from AP after this number of secs idle
#define SP_AP_DISCONNECT_SECS 60
// Max wait for AP to respond
#define SP_AP_TIMEOUT_SECS 10
// If client hasn't requested anything in particular
#define SP_BITRATE_DEFAULT SP_BITRATE_320
// A "mercury" response may contain multiple parts (e.g. multiple tracks), even
// though this implenentation currently expects just one.
#define SP_MERCURY_MAX_PARTS 32
// librespot uses /3, but -golang and -java use /4
#define SP_MERCURY_URI_TRACK "hm://metadata/4/track/"
#define SP_MERCURY_URI_EPISODE "hm://metadata/4/episode/"
// Special Spotify header that comes before the actual Ogg data
#define SP_OGG_HEADER_LEN 167
// For now we just always use channel 0, expand with more if needed
#define SP_DEFAULT_CHANNEL 0
// Download in chunks of 32768 bytes. The chunks shouldn't be too large because
// it makes seeking slow (seeking involves jumping around in the file), but
// large enough that the file can be probed from the first chunk.
#define SP_CHUNK_LEN_WORDS 1024 * 8
// Shorthand for error handling
#define RETURN_ERROR(r, m) \
do { ret = (r); sp_errmsg = (m); goto error; } while(0)
enum sp_error
{
SP_OK_OTHER = 3,
SP_OK_WAIT = 2,
SP_OK_DATA = 1,
SP_OK_DONE = 0,
SP_ERR_OOM = -1,
SP_ERR_INVALID = -2,
SP_ERR_DECRYPTION = -3,
SP_ERR_WRITE = -4,
SP_ERR_NOCONNECTION = -5,
SP_ERR_OCCUPIED = -6,
SP_ERR_NOSESSION = -7,
SP_ERR_LOGINFAILED = -8,
SP_ERR_TIMEOUT = -9,
};
enum sp_msg_type
{
MSG_TYPE_NONE,
MSG_TYPE_CLIENT_HELLO,
MSG_TYPE_CLIENT_RESPONSE_PLAINTEXT,
MSG_TYPE_CLIENT_RESPONSE_ENCRYPTED,
MSG_TYPE_PONG,
MSG_TYPE_MERCURY_TRACK_GET,
MSG_TYPE_MERCURY_EPISODE_GET,
MSG_TYPE_AUDIO_KEY_GET,
MSG_TYPE_CHUNK_REQUEST,
};
enum sp_media_type
{
SP_MEDIA_UNKNOWN,
SP_MEDIA_TRACK,
SP_MEDIA_EPISODE,
};
// From librespot-golang
enum sp_cmd_type
{
CmdNone = 0x00,
CmdSecretBlock = 0x02,
CmdPing = 0x04,
CmdStreamChunk = 0x08,
CmdStreamChunkRes = 0x09,
CmdChannelError = 0x0a,
CmdChannelAbort = 0x0b,
CmdRequestKey = 0x0c,
CmdAesKey = 0x0d,
CmdAesKeyError = 0x0e,
CmdImage = 0x19,
CmdCountryCode = 0x1b,
CmdPong = 0x49,
CmdPongAck = 0x4a,
CmdPause = 0x4b,
CmdProductInfo = 0x50,
CmdLegacyWelcome = 0x69,
CmdLicenseVersion = 0x76,
CmdLogin = 0xab,
CmdAPWelcome = 0xac,
CmdAuthFailure = 0xad,
CmdMercuryReq = 0xb2,
CmdMercurySub = 0xb3,
CmdMercuryUnsub = 0xb4,
};
struct sp_cmdargs
{
struct sp_session *session;
struct sp_credentials *credentials;
struct sp_metadata *metadata;
const char *username;
const char *password;
uint8_t *stored_cred;
size_t stored_cred_len;
const char *token;
const char *path;
int fd_read;
int fd_write;
size_t seek_pos;
enum sp_bitrates bitrate;
sp_progress_cb progress_cb;
void *cb_arg;
};
struct sp_conn_callbacks
{
struct event_base *evbase;
event_callback_fn response_cb;
event_callback_fn timeout_cb;
};
struct sp_message
{
enum sp_msg_type type;
enum sp_cmd_type cmd;
bool encrypt;
bool add_version_header;
enum sp_msg_type type_next;
enum sp_msg_type type_queued;
int (*response_handler)(uint8_t *msg, size_t msg_len, struct sp_session *session);
ssize_t len;
uint8_t data[4096];
};
struct sp_connection
{
bool is_connected;
bool is_encrypted;
// Resolved access point
char *ap_address;
unsigned short ap_port;
// Where we receive data from Spotify
int response_fd;
struct event *response_ev;
// Connection timers
struct event *idle_ev;
struct event *timeout_ev;
// Holds incoming data
struct evbuffer *incoming;
// Buffer holding client hello and ap response, since they are needed for
// MAC calculation
bool handshake_completed;
struct evbuffer *handshake_packets;
struct crypto_keys keys;
struct crypto_cipher encrypt;
struct crypto_cipher decrypt;
};
struct sp_mercury
{
char *uri;
char *method;
char *content_type;
uint64_t seq;
uint16_t parts_num;
struct sp_mercury_parts
{
uint8_t *data;
size_t len;
Track *track;
} parts[SP_MERCURY_MAX_PARTS];
};
struct sp_file
{
uint8_t id[20];
char *path; // The Spotify URI, e.g. spotify:episode:3KRjRyqv5ou5SilNMYBR4E
uint8_t media_id[16]; // Decoded value of the URIs base62
enum sp_media_type media_type; // track or episode from URI
uint8_t key[16];
uint16_t channel_id;
// Length and download progress
size_t len_words; // Length of file in words (32 bit)
size_t offset_words;
size_t received_words;
bool end_of_file;
bool end_of_chunk;
bool open;
struct crypto_aes_cipher decrypt;
};
struct sp_channel_header
{
uint16_t len;
uint8_t id;
uint8_t *data;
size_t data_len;
};
struct sp_channel_body
{
uint8_t *data;
size_t data_len;
};
struct sp_channel
{
int id;
bool is_allocated;
bool is_writing;
bool is_data_mode;
bool is_spotify_header_received;
size_t seek_pos;
size_t seek_align;
// pipe where we write audio data
int audio_fd[2];
// Triggers when fd is writable
struct event *audio_write_ev;
// Storage of audio until it can be written to the pipe
struct evbuffer *audio_buf;
// How much we have written to the fd (only used for debug)
size_t audio_written_len;
struct sp_file file;
// Latest header and body received
struct sp_channel_header header;
struct sp_channel_body body;
// Callbacks made during playback
sp_progress_cb progress_cb;
void *cb_arg;
};
// Linked list of sessions
struct sp_session
{
struct sp_connection conn;
bool is_logged_in;
struct sp_credentials credentials;
char country[3]; // Incl null term
enum sp_bitrates bitrate_preferred;
struct sp_channel channels[8];
// Points to the channel that is streaming, and via this information about
// the current track is also available
struct sp_channel *now_streaming_channel;
// Go to next step in a request sequence
struct event *continue_ev;
// Current (or last) message being processed
enum sp_msg_type msg_type_queued;
enum sp_msg_type msg_type_next;
int (*response_handler)(uint8_t *, size_t, struct sp_session *);
struct sp_session *next;
};
struct sp_err_map
{
ErrorCode errorcode;
const char *errmsg;
};
extern struct sp_callbacks sp_cb;
extern struct sp_sysinfo sp_sysinfo;
extern const char *sp_errmsg;
#endif // __LIBRESPOT_C_INTERNAL_H__

View File

@ -0,0 +1,977 @@
/*
* The MIT License (MIT)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
/*
Illustration of the general flow, where receive and writing the result are async
operations. For some commands, e.g. open and seek, the entire sequence is
encapsulated in a sync command, which doesn't return until final "done, error or
timeout". The command play is async, so all "done/error/timeout" is returned via
callbacks. Also, play will loop the flow, i.e. after writing a chunk of data it
will go back and ask for the next chunk of data from Spotify.
In some cases there is no result to write, or no reponse expected, but then the
events for proceeding are activated directly.
|---next----*------------next-------------*----------next----------*
v | | |
----------> start/send ------------------> recv ----------------> write result
^ | ^ | ^ |
|---reconnect---* |------wait------* |------wait------*
| | |
v v v
done/error done/error/timeout done/error
"next": on success, continue with next command
"wait": waiting for more data or for write to become possible
"timeout": receive or write took too long to complete
*/
#include <pthread.h>
#include "librespot-c-internal.h"
#include "commands.h"
#include "connection.h"
#include "channel.h"
/* TODO list
- protect against DOS
*/
/* -------------------------------- Globals --------------------------------- */
// Shared
struct sp_callbacks sp_cb;
struct sp_sysinfo sp_sysinfo;
const char *sp_errmsg;
static struct sp_session *sp_sessions;
static bool sp_initialized;
static pthread_t sp_tid;
static struct event_base *sp_evbase;
static struct commands_base *sp_cmdbase;
static struct timeval sp_response_timeout_tv = { SP_AP_TIMEOUT_SECS, 0 };
// Forwards
static int
request_make(enum sp_msg_type type, struct sp_session *session);
/* -------------------------------- Session --------------------------------- */
static void
session_free(struct sp_session *session)
{
if (!session)
return;
channel_free_all(session);
ap_disconnect(&session->conn);
event_free(session->continue_ev);
free(session);
}
static void
session_cleanup(struct sp_session *session)
{
struct sp_session *s;
if (!session)
return;
if (session == sp_sessions)
sp_sessions = session->next;
else
{
for (s = sp_sessions; s && (s->next != session); s = s->next)
; /* EMPTY */
if (s)
s->next = session->next;
}
session_free(session);
}
static int
session_new(struct sp_session **out, struct sp_cmdargs *cmdargs, event_callback_fn cb)
{
struct sp_session *session;
int ret;
session = calloc(1, sizeof(struct sp_session));
if (!session)
RETURN_ERROR(SP_ERR_OOM, "Out of memory creating session");
session->continue_ev = evtimer_new(sp_evbase, cb, session);
if (!session->continue_ev)
RETURN_ERROR(SP_ERR_OOM, "Out of memory creating session event");
snprintf(session->credentials.username, sizeof(session->credentials.username), "%s", cmdargs->username);
if (cmdargs->stored_cred)
{
if (cmdargs->stored_cred_len > sizeof(session->credentials.stored_cred))
RETURN_ERROR(SP_ERR_INVALID, "Invalid stored credential");
session->credentials.stored_cred_len = cmdargs->stored_cred_len;
memcpy(session->credentials.stored_cred, cmdargs->stored_cred, session->credentials.stored_cred_len);
}
else if (cmdargs->token)
{
if (strlen(cmdargs->token) > sizeof(session->credentials.token))
RETURN_ERROR(SP_ERR_INVALID, "Invalid token");
session->credentials.token_len = strlen(cmdargs->token);
memcpy(session->credentials.token, cmdargs->token, session->credentials.token_len);
}
else
{
snprintf(session->credentials.password, sizeof(session->credentials.password), "%s", cmdargs->password);
}
session->bitrate_preferred = SP_BITRATE_DEFAULT;
// Add to linked list
session->next = sp_sessions;
sp_sessions = session;
*out = session;
return 0;
error:
session_free(session);
return ret;
}
static int
session_check(struct sp_session *session)
{
struct sp_session *s;
for (s = sp_sessions; s; s = s->next)
{
if (s == session)
return 0;
}
return -1;
}
static struct sp_session *
session_find_by_fd(int fd)
{
struct sp_session *s;
for (s = sp_sessions; s; s = s->next)
{
if (s->now_streaming_channel && s->now_streaming_channel->audio_fd[0] == fd)
return s;
}
return NULL;
}
static void
session_return(struct sp_session *session, enum sp_error err)
{
struct sp_channel *channel = session->now_streaming_channel;
int ret;
ret = commands_exec_returnvalue(sp_cmdbase);
if (ret == 0) // Here we are async, i.e. no pending command
{
// track_write() completed, close the write end which means reader will
// get an EOF
if (channel && channel->is_writing && err == SP_OK_DONE)
channel_stop(channel);
return;
}
commands_exec_end(sp_cmdbase, err);
}
// Rolls back from an error situation. If it is a failed login then the session
// will be closed, but if it just a connection timeout we keep the session, but
// drop the ongoing download.
static void
session_error(struct sp_session *session, enum sp_error err)
{
struct sp_channel *channel = session->now_streaming_channel;
sp_cb.logmsg("Session error: %d\n", err);
session_return(session, err);
if (!session->is_logged_in)
{
session_cleanup(session);
return;
}
channel_free(channel);
session->now_streaming_channel = NULL;
}
/* ------------------------ Main sequence control --------------------------- */
// This callback must determine if a new request should be made, or if we are
// done and should return to caller
static void
continue_cb(int fd, short what, void *arg)
{
struct sp_session *session = arg;
enum sp_msg_type type = MSG_TYPE_NONE;
int ret;
// type_next has priority, since this is what we use to chain a sequence, e.g.
// the handshake sequence. type_queued is what comes after, e.g. first a
// handshake (type_next) and then a chunk request (type_queued)
if (session->msg_type_next != MSG_TYPE_NONE)
{
// sp_cb.logmsg(">>> msg_next >>>\n");
type = session->msg_type_next;
session->msg_type_next = MSG_TYPE_NONE;
}
else if (session->msg_type_queued != MSG_TYPE_NONE)
{
// sp_cb.logmsg(">>> msg_queued >>>\n");
type = session->msg_type_queued;
session->msg_type_queued = MSG_TYPE_NONE;
}
if (type != MSG_TYPE_NONE)
{
ret = request_make(type, session);
if (ret < 0)
session_error(session, ret);
}
else
session_return(session, SP_OK_DONE); // All done, yay!
}
// This callback is triggered by response_cb when the message response handler
// said that there was data to write. If not all data can be written in one pass
// it will re-add the event.
static void
audio_write_cb(int fd, short what, void *arg)
{
struct sp_session *session = arg;
struct sp_channel *channel = session->now_streaming_channel;
int ret;
if (!channel)
RETURN_ERROR(SP_ERR_INVALID, "Write result request, but not streaming right now");
ret = channel_data_write(channel);
switch (ret)
{
case SP_OK_WAIT:
event_add(channel->audio_write_ev, NULL);
break;
case SP_OK_DONE:
event_active(session->continue_ev, 0, 0);
break;
default:
goto error;
}
return;
error:
session_error(session, ret);
}
static void
timeout_cb(int fd, short what, void *arg)
{
struct sp_session *session = arg;
sp_errmsg = "Timeout waiting for Spotify response";
session_error(session, SP_ERR_TIMEOUT);
}
static void
response_cb(int fd, short what, void *arg)
{
struct sp_session *session = arg;
struct sp_connection *conn = &session->conn;
struct sp_channel *channel = session->now_streaming_channel;
int ret;
if (what == EV_READ)
{
ret = evbuffer_read(conn->incoming, fd, -1);
if (ret == 0)
RETURN_ERROR(SP_ERR_NOCONNECTION, "The access point disconnected");
else if (ret < 0)
RETURN_ERROR(SP_ERR_NOCONNECTION, "Connection to Spotify returned an error");
// sp_cb.logmsg("Received data len %d\n", ret);
}
ret = response_read(session);
switch (ret)
{
case SP_OK_WAIT: // Incomplete, wait for more data
break;
case SP_OK_DATA:
if (channel->is_writing && !channel->file.end_of_file)
session->msg_type_next = MSG_TYPE_CHUNK_REQUEST;
if (channel->progress_cb)
channel->progress_cb(channel->audio_fd[0], channel->cb_arg, 4 * channel->file.received_words - SP_OGG_HEADER_LEN, 4 * channel->file.len_words - SP_OGG_HEADER_LEN);
event_del(conn->timeout_ev);
event_add(channel->audio_write_ev, NULL);
break;
case SP_OK_DONE: // Got the response we expected, but possibly more to process
if (evbuffer_get_length(conn->incoming) > 0)
event_active(conn->response_ev, 0, 0);
event_del(conn->timeout_ev);
event_active(session->continue_ev, 0, 0);
break;
case SP_OK_OTHER: // Not the response we were waiting for, check for other
if (evbuffer_get_length(conn->incoming) > 0)
event_active(conn->response_ev, 0, 0);
break;
default:
event_del(conn->timeout_ev);
goto error;
}
return;
error:
session_error(session, ret);
}
static int
relogin(enum sp_msg_type type, struct sp_session *session)
{
int ret;
if (session->msg_type_queued != MSG_TYPE_NONE)
RETURN_ERROR(SP_ERR_NOCONNECTION, "Cannot send message, another request is waiting for handshake");
ret = request_make(MSG_TYPE_CLIENT_HELLO, session);
if (ret < 0)
RETURN_ERROR(ret, sp_errmsg);
// In case we lost connection to the AP we have to make a new handshake for
// the non-handshake message types. So queue the message until the handshake
// is complete.
session->msg_type_queued = type;
return 0;
error:
return ret;
}
static int
request_make(enum sp_msg_type type, struct sp_session *session)
{
struct sp_message msg;
struct sp_connection *conn = &session->conn;
struct sp_conn_callbacks cb = { sp_evbase, response_cb, timeout_cb };
int ret;
// Make sure the connection is in a state suitable for sending this message
ret = ap_connect(type, &cb, session);
if (ret == SP_OK_WAIT)
return relogin(type, session); // Can't proceed right now, the handshake needs to complete first
else if (ret < 0)
RETURN_ERROR(ret, sp_errmsg);
ret = msg_make(&msg, type, session);
if (type == MSG_TYPE_CLIENT_RESPONSE_ENCRYPTED)
memset(session->credentials.password, 0, sizeof(session->credentials.password));
if (ret < 0)
RETURN_ERROR(SP_ERR_INVALID, "Error constructing message to Spotify");
if (msg.encrypt)
conn->is_encrypted = true;
ret = msg_send(&msg, conn);
if (ret < 0)
RETURN_ERROR(ret, sp_errmsg);
// Only start timeout timer if a response is expected, otherwise go straight
// to next message
if (msg.response_handler)
event_add(conn->timeout_ev, &sp_response_timeout_tv);
else
event_active(session->continue_ev, 0, 0);
session->msg_type_next = msg.type_next;
session->response_handler = msg.response_handler;
return 0;
error:
return ret;
}
/* ----------------------------- Implementation ----------------------------- */
// This command is async
static enum command_state
track_write(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session;
struct sp_channel *channel;
int ret;
*retval = 0;
session = session_find_by_fd(cmdargs->fd_read);
if (!session)
RETURN_ERROR(SP_ERR_NOSESSION, "Cannot play track, no valid session found");
channel = session->now_streaming_channel;
if (!channel || !channel->is_allocated)
RETURN_ERROR(SP_ERR_INVALID, "No active channel to play, has track been opened?");
channel_play(channel);
ret = request_make(MSG_TYPE_CHUNK_REQUEST, session);
if (ret < 0)
RETURN_ERROR(SP_ERR_NOCONNECTION, "Could not send request for audio chunk");
channel->progress_cb = cmdargs->progress_cb;
channel->cb_arg = cmdargs->cb_arg;
return COMMAND_END;
error:
sp_cb.logmsg("Error %d: %s", ret, sp_errmsg);
return COMMAND_END;
}
static enum command_state
track_pause(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session;
struct sp_channel *channel;
int ret;
session = session_find_by_fd(cmdargs->fd_read);
if (!session)
RETURN_ERROR(SP_ERR_NOSESSION, "Cannot pause track, no valid session found");
channel = session->now_streaming_channel;
if (!channel || !channel->is_allocated)
RETURN_ERROR(SP_ERR_INVALID, "No active channel to pause, has track been opened?");
// If we are playing we are in the process of downloading a chunk, and in that
// case we need that to complete before doing anything else with the channel,
// e.g. reset it as track_close() does.
if (!channel->is_writing)
{
*retval = 0;
return COMMAND_END;
}
channel_pause(channel);
*retval = 1;
return COMMAND_PENDING;
error:
*retval = ret;
return COMMAND_END;
}
static enum command_state
track_seek(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session;
struct sp_channel *channel;
int ret;
session = session_find_by_fd(cmdargs->fd_read);
if (!session)
RETURN_ERROR(SP_ERR_NOSESSION, "Cannot seek, no valid session found");
channel = session->now_streaming_channel;
if (!channel || !channel->is_allocated)
RETURN_ERROR(SP_ERR_INVALID, "No active channel to seek, has track been opened?");
else if (channel->is_writing)
RETURN_ERROR(SP_ERR_INVALID, "Seeking during playback not currently supported");
// This operation is not safe during chunk downloading because it changes the
// AES decryptor to match the new position. It also flushes the pipe.
channel_seek(channel, cmdargs->seek_pos);
ret = request_make(MSG_TYPE_CHUNK_REQUEST, session);
if (ret < 0)
RETURN_ERROR(SP_ERR_NOCONNECTION, "Could not send track seek request");
*retval = 1;
return COMMAND_PENDING;
error:
*retval = ret;
return COMMAND_END;
}
static enum command_state
track_close(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session;
int ret;
session = session_find_by_fd(cmdargs->fd_read);
if (!session)
RETURN_ERROR(SP_ERR_NOSESSION, "Cannot close track, no valid session found");
channel_free(session->now_streaming_channel);
session->now_streaming_channel = NULL;
*retval = 0;
return COMMAND_END;
error:
*retval = ret;
return COMMAND_END;
}
static enum command_state
media_open(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session = cmdargs->session;
struct sp_channel *channel = NULL;
enum sp_msg_type type;
int ret;
ret = session_check(session);
if (ret < 0)
RETURN_ERROR(SP_ERR_NOSESSION, "Cannot open media, session is invalid");
if (session->now_streaming_channel)
RETURN_ERROR(SP_ERR_OCCUPIED, "Already getting media");
ret = channel_new(&channel, session, cmdargs->path, sp_evbase, audio_write_cb);
if (ret < 0)
RETURN_ERROR(SP_ERR_OOM, "Could not setup a channel");
cmdargs->fd_read = channel->audio_fd[0];
// Must be set before calling request_make() because this info is needed for
// making the request
session->now_streaming_channel = channel;
if (channel->file.media_type == SP_MEDIA_TRACK)
type = MSG_TYPE_MERCURY_TRACK_GET;
else if (channel->file.media_type == SP_MEDIA_EPISODE)
type = MSG_TYPE_MERCURY_EPISODE_GET;
else
RETURN_ERROR(SP_ERR_INVALID, "Unknown media type in Spotify path");
// Kicks of a sequence where we first get file info, then get the AES key and
// then the first chunk (incl. headers)
ret = request_make(type, session);
if (ret < 0)
RETURN_ERROR(SP_ERR_NOCONNECTION, "Could not send media request");
*retval = 1;
return COMMAND_PENDING;
error:
if (channel)
{
session->now_streaming_channel = NULL;
channel_free(channel);
}
*retval = ret;
return COMMAND_END;
}
static enum command_state
media_open_bh(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
if (*retval == SP_OK_DONE)
*retval = cmdargs->fd_read;
return COMMAND_END;
}
static enum command_state
login(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session = NULL;
int ret;
ret = session_new(&session, cmdargs, continue_cb);
if (ret < 0)
goto error;
ret = request_make(MSG_TYPE_CLIENT_HELLO, session);
if (ret < 0)
goto error;
cmdargs->session = session;
*retval = 1; // Pending command_exec_sync, i.e. response from Spotify
return COMMAND_PENDING;
error:
session_cleanup(session);
*retval = ret;
return COMMAND_END;
}
static enum command_state
login_bh(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
if (*retval == SP_OK_DONE)
cmdargs->session->is_logged_in = true;
else
cmdargs->session = NULL;
return COMMAND_END;
}
static enum command_state
logout(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session = cmdargs->session;
int ret;
ret = session_check(session);
if (ret < 0)
RETURN_ERROR(SP_ERR_NOSESSION, "Session has disappeared, cannot logout");
session_cleanup(session);
error:
*retval = ret;
return COMMAND_END;
}
static enum command_state
metadata_get(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session;
struct sp_metadata *metadata = cmdargs->metadata;
int ret = 0;
session = session_find_by_fd(cmdargs->fd_read);
if (!session || !session->now_streaming_channel)
RETURN_ERROR(SP_ERR_NOSESSION, "Session has disappeared, cannot get metadata");
memset(metadata, 0, sizeof(struct sp_metadata));
metadata->file_len = 4 * session->now_streaming_channel->file.len_words - SP_OGG_HEADER_LEN;;
error:
*retval = ret;
return COMMAND_END;
}
static enum command_state
bitrate_set(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session = cmdargs->session;
int ret;
if (cmdargs->bitrate == SP_BITRATE_ANY)
cmdargs->bitrate = SP_BITRATE_DEFAULT;
ret = session_check(session);
if (ret < 0)
RETURN_ERROR(SP_ERR_NOSESSION, "Session has disappeared, cannot set bitrate");
session->bitrate_preferred = cmdargs->bitrate;
error:
*retval = ret;
return COMMAND_END;
}
static enum command_state
credentials_get(void *arg, int *retval)
{
struct sp_cmdargs *cmdargs = arg;
struct sp_session *session = cmdargs->session;
struct sp_credentials *credentials = cmdargs->credentials;
int ret;
ret = session_check(session);
if (ret < 0)
RETURN_ERROR(SP_ERR_NOSESSION, "Session has disappeared, cannot get credentials");
memcpy(credentials, &session->credentials, sizeof(struct sp_credentials));
error:
*retval = ret;
return COMMAND_END;
}
/* ------------------------------ Event loop -------------------------------- */
static void *
librespotc(void *arg)
{
event_base_dispatch(sp_evbase);
pthread_exit(NULL);
}
/* ---------------------------------- API ----------------------------------- */
int
librespotc_open(const char *path, struct sp_session *session)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.session = session;
cmdargs.path = path;
return commands_exec_sync(sp_cmdbase, media_open, media_open_bh, &cmdargs);
}
int
librespotc_seek(int fd, size_t pos)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.fd_read = fd;
cmdargs.seek_pos = pos;
return commands_exec_sync(sp_cmdbase, track_seek, NULL, &cmdargs);
}
// Starts writing audio for the caller to read from the file descriptor
void
librespotc_write(int fd, sp_progress_cb progress_cb, void *cb_arg)
{
struct sp_cmdargs *cmdargs;
cmdargs = calloc(1, sizeof(struct sp_cmdargs));
cmdargs->fd_read = fd;
cmdargs->progress_cb = progress_cb;
cmdargs->cb_arg = cb_arg;
commands_exec_async(sp_cmdbase, track_write, cmdargs);
}
int
librespotc_close(int fd)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.fd_read = fd;
return commands_exec_sync(sp_cmdbase, track_pause, track_close, &cmdargs);
}
struct sp_session *
librespotc_login_password(const char *username, const char *password)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.username = username;
cmdargs.password = password;
commands_exec_sync(sp_cmdbase, login, login_bh, &cmdargs);
return cmdargs.session;
}
struct sp_session *
librespotc_login_stored_cred(const char *username, uint8_t *stored_cred, size_t stored_cred_len)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.username = username;
cmdargs.stored_cred = stored_cred;
cmdargs.stored_cred_len = stored_cred_len;
commands_exec_sync(sp_cmdbase, login, login_bh, &cmdargs);
return cmdargs.session;
}
struct sp_session *
librespotc_login_token(const char *username, const char *token)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.username = username;
cmdargs.token = token;
commands_exec_sync(sp_cmdbase, login, login_bh, &cmdargs);
return cmdargs.session;
}
int
librespotc_logout(struct sp_session *session)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.session = session;
return commands_exec_sync(sp_cmdbase, logout, NULL, &cmdargs);
}
int
librespotc_metadata_get(struct sp_metadata *metadata, int fd)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.metadata = metadata;
cmdargs.fd_read = fd;
return commands_exec_sync(sp_cmdbase, metadata_get, NULL, &cmdargs);
}
int
librespotc_bitrate_set(struct sp_session *session, enum sp_bitrates bitrate)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.session = session;
cmdargs.bitrate = bitrate;
return commands_exec_sync(sp_cmdbase, bitrate_set, NULL, &cmdargs);
}
int
librespotc_credentials_get(struct sp_credentials *credentials, struct sp_session *session)
{
struct sp_cmdargs cmdargs = { 0 };
cmdargs.credentials = credentials;
cmdargs.session = session;
return commands_exec_sync(sp_cmdbase, credentials_get, NULL, &cmdargs);
}
const char *
librespotc_last_errmsg(void)
{
return sp_errmsg ? sp_errmsg : "(no error)";
}
int
librespotc_init(struct sp_sysinfo *sysinfo, struct sp_callbacks *callbacks)
{
int ret;
if (sp_initialized)
RETURN_ERROR(SP_ERR_INVALID, "librespot-c already initialized");
sp_cb = *callbacks;
sp_initialized = true;
memcpy(&sp_sysinfo, sysinfo, sizeof(struct sp_sysinfo));
sp_evbase = event_base_new();
if (!sp_evbase)
RETURN_ERROR(SP_ERR_OOM, "event_base_new() failed");
sp_cmdbase = commands_base_new(sp_evbase, NULL);
if (!sp_cmdbase)
RETURN_ERROR(SP_ERR_OOM, "commands_base_new() failed");
ret = pthread_create(&sp_tid, NULL, librespotc, NULL);
if (ret < 0)
RETURN_ERROR(SP_ERR_OOM, "Could not start thread");
if (sp_cb.thread_name_set)
sp_cb.thread_name_set(sp_tid);
return 0;
error:
librespotc_deinit();
return ret;
}
void
librespotc_deinit()
{
struct sp_session *session;
if (sp_cmdbase)
{
commands_base_destroy(sp_cmdbase);
sp_cmdbase = NULL;
}
for (session = sp_sessions; sp_sessions; session = sp_sessions)
{
sp_sessions = session->next;
session_free(session);
}
if (sp_tid)
{
pthread_join(sp_tid, NULL);
}
if (sp_evbase)
{
event_base_free(sp_evbase);
sp_evbase = NULL;
}
sp_initialized = false;
memset(&sp_cb, 0, sizeof(struct sp_callbacks));
return;
}

View File

@ -0,0 +1 @@
test1

View File

@ -0,0 +1,10 @@
TEST_CFLAGS = $(CFLAGS) $(JSON_C_CFLAGS) $(LIBCURL_CFLAGS) $(LIBEVENT_CFLAGS) $(LIBGCRYPT_CFLAGS) $(LIBPROTOBUF_C_CFLAGS)
TEST_LIBS = $(LIBS) $(JSON_C_LIBS) $(LIBCURL_LIBS) $(LIBEVENT_LIBS) $(LIBGCRYPT_LIBS) $(LIBPROTOBUF_C_LIBS)
AM_CPPFLAGS = -I$(top_srcdir)
test1_SOURCES = test1.c
test1_LDADD = $(top_builddir)/librespot-c.a -lpthread $(TEST_LIBS)
test1_CFLAGS = $(TEST_CFLAGS)
check_PROGRAMS = test1

View File

@ -0,0 +1,357 @@
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// For file output
#include <sys/stat.h>
#include <fcntl.h>
#include <event2/event.h>
#include <event2/buffer.h>
#include <curl/curl.h>
#include "librespot-c.h"
static int audio_fd = -1;
static int test_file = -1;
static struct event_base *evbase;
static struct evbuffer *audio_buf;
static int total_bytes;
#include <ctype.h> // for isprint()
static void
hexdump_dummy(const char *msg, uint8_t *mem, size_t len)
{
return;
}
static void
hexdump(const char *msg, uint8_t *mem, size_t len)
{
int i, j;
int hexdump_cols = 16;
if (msg)
printf("%s", msg);
for (i = 0; i < len + ((len % hexdump_cols) ? (hexdump_cols - len % hexdump_cols) : 0); i++)
{
if(i % hexdump_cols == 0)
printf("0x%06x: ", i);
if (i < len)
printf("%02x ", 0xFF & ((char*)mem)[i]);
else
printf(" ");
if (i % hexdump_cols == (hexdump_cols - 1))
{
for (j = i - (hexdump_cols - 1); j <= i; j++)
{
if (j >= len)
putchar(' ');
else if (isprint(((char*)mem)[j]))
putchar(0xFF & ((char*)mem)[j]);
else
putchar('.');
}
putchar('\n');
}
}
}
static void
logmsg(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
}
static size_t
https_write_cb(char *data, size_t size, size_t nmemb, void *userdata)
{
char **body;
size_t realsize;
realsize = size * nmemb;
body = (char **)userdata;
*body = malloc(realsize + 1);
memcpy(*body, data, realsize);
(*body)[realsize] = 0;
return realsize;
}
static int
https_get(char **body, const char *url)
{
CURL *curl;
CURLcode res;
long response_code;
curl = curl_easy_init();
if (!curl)
{
printf("Could not initialize CURL\n");
goto error;
}
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, https_write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, body);
res = curl_easy_perform(curl);
if (res != CURLE_OK)
{
printf("CURL could not make request (%d)\n", (int)res);
goto error;
}
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
if (response_code != 200)
{
printf("HTTP response code %d\n", (int)response_code);
goto error;
}
curl_easy_cleanup(curl);
return 0;
error:
curl_easy_cleanup(curl);
return -1;
}
static int
tcp_connect(const char *address, unsigned short port)
{
struct addrinfo hints = { 0 };
struct addrinfo *servinfo;
struct addrinfo *ptr;
char strport[8];
int fd;
int ret;
hints.ai_socktype = SOCK_STREAM;
hints.ai_family = AF_UNSPEC;
snprintf(strport, sizeof(strport), "%hu", port);
ret = getaddrinfo(address, strport, &hints, &servinfo);
if (ret < 0)
{
printf("Could not connect to %s (port %u): %s\n", address, port, gai_strerror(ret));
return -1;
}
for (ptr = servinfo; ptr; ptr = ptr->ai_next)
{
fd = socket(ptr->ai_family, SOCK_STREAM, ptr->ai_protocol);
if (fd < 0)
{
continue;
}
ret = connect(fd, ptr->ai_addr, ptr->ai_addrlen);
if (ret < 0)
{
close(fd);
continue;
}
break;
}
freeaddrinfo(servinfo);
if (!ptr)
{
printf("Could not connect to '%s' (port %u): %s\n", address, port, strerror(errno));
return -1;
}
return fd;
}
static void
tcp_disconnect(int fd)
{
if (fd < 0)
return;
close(fd);
}
static void
progress_cb(int fd, void *arg, size_t received, size_t len)
{
printf("Progress on fd %d is %zu/%zu\n", fd, received, len);
}
// This thread
static void
audio_read_cb(int fd, short what, void *arg)
{
int got;
got = evbuffer_read(audio_buf, fd, -1);
if (got <= 0)
{
printf("Playback ended (%d)\n", got);
event_base_loopbreak(evbase);
return;
}
total_bytes += got;
printf("Got %d bytes of audio, total received is %d bytes\n", got, total_bytes);
evbuffer_write(audio_buf, test_file);
}
struct sp_callbacks callbacks =
{
.https_get = https_get,
.tcp_connect = tcp_connect,
.tcp_disconnect = tcp_disconnect,
.thread_name_set = NULL,
.hexdump = hexdump,
.logmsg = logmsg,
};
int
main(int argc, char * argv[])
{
struct sp_session *session = NULL;
struct sp_sysinfo sysinfo;
struct sp_credentials credentials;
struct sp_metadata metadata;
struct event *read_ev;
// struct event *stop_ev;
// struct timeval tv = { 0 };
int ret;
if (argc != 4)
{
printf("%s spotify_path username password|token\n", argv[0]);
goto error;
}
test_file = open("testfile.ogg", O_CREAT | O_RDWR, 0664);
if (test_file < 0)
{
printf("Error opening file: %s\n", strerror(errno));
goto error;
}
snprintf(sysinfo.client_name, sizeof(sysinfo.client_name), "myclient");
snprintf(sysinfo.client_version, sizeof(sysinfo.client_version), "0.1");
snprintf(sysinfo.client_build_id, sizeof(sysinfo.client_build_id), "a");
snprintf(sysinfo.device_id, sizeof(sysinfo.device_id), "aabbccddeeff");
ret = librespotc_init(&sysinfo, &callbacks);
if (ret < 0)
{
printf("Error initializing Spotify: %s\n", librespotc_last_errmsg());
goto error;
}
if (strlen(argv[3]) < 100)
session = librespotc_login_password(argv[2], argv[3]);
else
session = librespotc_login_token(argv[2], argv[3]); // Length of token should be 194
if (!session)
{
printf("Error logging in: %s\n", librespotc_last_errmsg());
goto error;
}
printf("\n --- Login OK --- \n");
ret = librespotc_credentials_get(&credentials, session);
if (ret < 0)
{
printf("Error getting session credentials: %s\n", librespotc_last_errmsg());
goto error;
}
printf("Username is %s\n", credentials.username);
audio_fd = librespotc_open(argv[1], session);
if (audio_fd < 0)
{
printf("Error opening file: %s\n", librespotc_last_errmsg());
goto error;
}
ret = librespotc_metadata_get(&metadata, audio_fd);
if (ret < 0)
{
printf("Error getting track metadata: %s\n", librespotc_last_errmsg());
goto error;
}
printf("File is open, length is %zu\n", metadata.file_len);
ret = librespotc_seek(audio_fd, 1000000);
if (ret < 0)
{
printf("Error seeking: %s\n", librespotc_last_errmsg());
goto error;
}
evbase = event_base_new();
audio_buf = evbuffer_new();
read_ev = event_new(evbase, audio_fd, EV_READ | EV_PERSIST, audio_read_cb, NULL);
event_add(read_ev, NULL);
librespotc_write(audio_fd, progress_cb, NULL);
// stop_ev = evtimer_new(evbase, stop, &audio_fd);
// tv.tv_sec = 2;
// event_add(stop_ev, &tv);
event_base_dispatch(evbase);
// event_free(stop_ev);
event_free(read_ev);
evbuffer_free(audio_buf);
event_base_free(evbase);
librespotc_close(audio_fd);
close(test_file);
librespotc_logout(session);
librespotc_deinit();
return 0;
error:
if (audio_fd >= 0)
librespotc_close(audio_fd);
if (test_file >= 0)
close(test_file);
if (session)
librespotc_logout(session);
librespotc_deinit();
return -1;
}

View File

@ -43,15 +43,15 @@
#include <libspotify/api.h>
#include <json.h>
#include "spotify.h"
#include "spotify_webapi.h"
#include "libspotify.h"
#include "library.h"
#include "library/spotify_webapi.h"
#include "logger.h"
#include "misc.h"
#include "http.h"
#include "conffile.h"
#include "cache.h"
#include "commands.h"
#include "library.h"
#include "input.h"
#include "listener.h"
@ -87,7 +87,7 @@ struct artwork_get_param
};
static void
spotify_playback_stop_nonblock(void);
libspotify_playback_stop_nonblock(void);
/* --- Globals --- */
// Spotify thread
@ -607,7 +607,7 @@ playback_setup(void *arg, int *retval)
if (SP_ERROR_OK != err)
{
DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed: %s\n", fptr_sp_error_message(err));
*retval = (SP_ERROR_IS_LOADING == err) ? SPOTIFY_SETUP_ERROR_IS_LOADING : -1;
*retval = (SP_ERROR_IS_LOADING == err) ? LIBSPOTIFY_SETUP_ERROR_IS_LOADING : -1;
return COMMAND_END;
}
@ -1017,7 +1017,7 @@ static int music_delivery(sp_session *sess, const sp_audioformat *format,
if ((format->sample_type != SP_SAMPLETYPE_INT16_NATIVE_ENDIAN) || (format->channels != 2))
{
DPRINTF(E_LOG, L_SPOTIFY, "Got music with unsupported sample format or number of channels, stopping playback\n");
spotify_playback_stop_nonblock();
libspotify_playback_stop_nonblock();
return num_frames;
}
@ -1084,7 +1084,7 @@ static void play_token_lost(sp_session *sess)
{
DPRINTF(E_LOG, L_SPOTIFY, "Music interrupted - some other session is playing on the account\n");
spotify_playback_stop_nonblock();
libspotify_playback_stop_nonblock();
}
static void connectionstate_updated(sp_session *session)
@ -1096,7 +1096,7 @@ static void connectionstate_updated(sp_session *session)
else if (g_state == SPOTIFY_STATE_PLAYING)
{
DPRINTF(E_LOG, L_SPOTIFY, "Music interrupted - connection error or logged out\n");
spotify_playback_stop_nonblock();
libspotify_playback_stop_nonblock();
}
}
@ -1217,7 +1217,7 @@ notify_cb(int fd, short what, void *arg)
/* Thread: player */
int
spotify_playback_setup(const char *path)
libspotify_playback_setup(const char *path)
{
sp_link *link;
@ -1234,7 +1234,7 @@ spotify_playback_setup(const char *path)
}
int
spotify_playback_play()
libspotify_playback_play()
{
DPRINTF(E_DBG, L_SPOTIFY, "Playback request\n");
@ -1242,7 +1242,7 @@ spotify_playback_play()
}
int
spotify_playback_pause()
libspotify_playback_pause()
{
DPRINTF(E_DBG, L_SPOTIFY, "Pause request\n");
@ -1251,7 +1251,7 @@ spotify_playback_pause()
/* Thread: libspotify */
void
spotify_playback_pause_nonblock(void)
libspotify_playback_pause_nonblock(void)
{
DPRINTF(E_DBG, L_SPOTIFY, "Nonblock pause request\n");
@ -1260,7 +1260,7 @@ spotify_playback_pause_nonblock(void)
/* Thread: player and libspotify */
int
spotify_playback_stop(void)
libspotify_playback_stop(void)
{
DPRINTF(E_DBG, L_SPOTIFY, "Stop request\n");
@ -1269,7 +1269,7 @@ spotify_playback_stop(void)
/* Thread: player and libspotify */
void
spotify_playback_stop_nonblock(void)
libspotify_playback_stop_nonblock(void)
{
DPRINTF(E_DBG, L_SPOTIFY, "Nonblock stop request\n");
@ -1278,7 +1278,7 @@ spotify_playback_stop_nonblock(void)
/* Thread: player */
int
spotify_playback_seek(int ms)
libspotify_playback_seek(int ms)
{
int ret;
@ -1292,7 +1292,7 @@ spotify_playback_seek(int ms)
/* Thread: httpd (artwork) and worker */
int
spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h)
libspotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h)
{
struct artwork_get_param artwork;
struct timespec ts;
@ -1327,7 +1327,7 @@ spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h)
/* Thread: httpd */
void
spotify_uri_register(const char *uri)
libspotify_uri_register(const char *uri)
{
char *tmp;
@ -1336,7 +1336,7 @@ spotify_uri_register(const char *uri)
}
void
spotify_status_info_get(struct spotify_status_info *info)
libspotify_status_info_get(struct spotify_status_info *info)
{
CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&status_lck));
memcpy(info, &spotify_status_info, sizeof(struct spotify_status_info));
@ -1345,7 +1345,7 @@ spotify_status_info_get(struct spotify_status_info *info)
/* Thread: library, httpd */
static int
logout(char **errmsg)
logout(const char **errmsg)
{
sp_error err;
@ -1365,7 +1365,7 @@ logout(char **errmsg)
{
DPRINTF(E_LOG, L_SPOTIFY, "Could not logout of Spotify: %s\n", fptr_sp_error_message(err));
if (errmsg)
*errmsg = safe_asprintf("Could not logout of Spotify: %s", fptr_sp_error_message(err));
*errmsg = fptr_sp_error_message(err);
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&login_lck));
return -1;
@ -1379,7 +1379,7 @@ logout(char **errmsg)
/* Thread: library, httpd */
static int
login_user(const char *user, const char *password, char **errmsg)
login_user(const char *user, const char *password, const char **errmsg)
{
sp_error err;
int ret;
@ -1390,13 +1390,13 @@ login_user(const char *user, const char *password, char **errmsg)
{
DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - could not find libspotify\n");
if (errmsg)
*errmsg = safe_asprintf("Could not find libspotify");
*errmsg = "Could not find libspotify";
}
else
{
DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - no valid Spotify session\n");
if (errmsg)
*errmsg = safe_asprintf("No valid Spotify session");
*errmsg = "No valid Spotify session";
}
return -1;
@ -1423,7 +1423,7 @@ login_user(const char *user, const char *password, char **errmsg)
{
DPRINTF(E_LOG, L_SPOTIFY, "Could not login into Spotify: %s\n", fptr_sp_error_message(err));
if (errmsg)
*errmsg = safe_asprintf("Could not login into Spotify: %s", fptr_sp_error_message(err));
*errmsg = fptr_sp_error_message(err);
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&login_lck));
return -1;
@ -1437,14 +1437,14 @@ login_user(const char *user, const char *password, char **errmsg)
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&status_lck));
if (ret < 0 && errmsg)
*errmsg = safe_asprintf("Login failed");
*errmsg = "Login failed";
return ret;
}
/* Thread: httpd, library */
int
spotify_login_user(const char *user, const char *password, char **errmsg)
libspotify_login(const char *user, const char *password, const char **errmsg)
{
int ret;
@ -1460,32 +1460,20 @@ spotify_login_user(const char *user, const char *password, char **errmsg)
/* Thread: library */
int
spotify_relogin()
libspotify_relogin(void)
{
return login_user(NULL, NULL, NULL);
}
/* Thread: library */
void
spotify_login(char **arglist)
{
if (arglist)
spotify_login_user(arglist[0], arglist[1], NULL);
else
spotify_login_user(NULL, NULL, NULL);
}
void
spotify_logout(void)
libspotify_logout(void)
{
logout(NULL);
spotifywebapi_purge();
}
/* Thread: main */
int
spotify_init(void)
libspotify_init(void)
{
cfg_t *spotify_cfg;
sp_session *sp;
@ -1628,7 +1616,7 @@ spotify_init(void)
}
void
spotify_deinit(void)
libspotify_deinit(void)
{
int ret;

View File

@ -0,0 +1,65 @@
#ifndef __LIBSPOTIFY_H__
#define __LIBSPOTIFY_H__
#include <event2/event.h>
#include <event2/buffer.h>
#include <event2/http.h>
#include <stdbool.h>
struct spotify_status_info
{
bool libspotify_installed;
bool libspotify_logged_in;
char libspotify_user[100];
};
#define LIBSPOTIFY_SETUP_ERROR_IS_LOADING -2
int
libspotify_playback_setup(const char *path);
int
libspotify_playback_play(void);
int
libspotify_playback_pause(void);
//void
//spotify_playback_pause_nonblock(void);
int
libspotify_playback_stop(void);
//void
//spotify_playback_stop_nonblock(void);
int
libspotify_playback_seek(int ms);
//int
//spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h);
int
libspotify_relogin(void);
int
libspotify_login(const char *user, const char *password, const char **errmsg);
void
libspotify_logout(void);
void
libspotify_status_info_get(struct spotify_status_info *info);
void
libspotify_uri_register(const char *uri);
int
libspotify_init(void);
void
libspotify_deinit(void);
#endif /* !__LIBSPOTIFY_H__ */

View File

@ -14,592 +14,133 @@
* 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 <string.h>
#include <event2/event.h>
#include "input.h"
#include "misc.h"
#include "logger.h"
#include "http.h"
#include "db.h"
#include "transcode.h"
#include "conffile.h"
#include "spotify.h"
#include "spotifyc/spotifyc.h"
#define SPOTIFY_PROBE_SIZE_MIN 65536
#ifdef SPOTIFY_LIBRESPOTC
extern struct spotify_backend spotify_librespotc;
#endif
#ifdef SPOTIFY_LIBSPOTIFY
extern struct spotify_backend spotify_libspotify;
#endif
struct global_ctx
static struct spotify_backend *
backend_set(void)
{
pthread_mutex_t lock;
pthread_cond_t cond;
struct sp_session *session;
bool response_pending; // waiting for a response from spotifyc
struct spotify_status status;
};
struct playback_ctx
{
struct transcode_ctx *xcode;
// This buffer gets fairly large, since it reads and holds the Ogg track that
// spotifyc downloads. It has no write limit, unlike the input buffer.
struct evbuffer *read_buf;
int read_fd;
size_t read_bytes;
uint32_t len_ms;
};
static struct global_ctx spotify_ctx;
static bool db_is_initialized;
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);
}
#ifdef SPOTIFY_LIBRESPOTC
if (!cfg_getbool(cfg_getsec(cfg, "spotify"), "use_libspotify"))
return &spotify_librespotc;
#endif
#ifdef SPOTIFY_LIBSPOTIFY
if (cfg_getbool(cfg_getsec(cfg, "spotify"), "use_libspotify"))
return &spotify_libspotify;
#endif
DPRINTF(E_LOG, L_SPOTIFY, "Invalid Spotify configuration (not built with the configured backend)\n");
return NULL;
}
/* -------------------- Callbacks from spotifyc thread ---------------------- */
static void
got_reply(struct global_ctx *ctx)
{
pthread_mutex_lock(&ctx->lock);
ctx->response_pending = false;
pthread_cond_signal(&ctx->cond);
pthread_mutex_unlock(&ctx->lock);
}
static void
error_cb(void *cb_arg, int err, const char *errmsg)
{
struct global_ctx *ctx = cb_arg;
got_reply(ctx);
DPRINTF(E_LOG, L_SPOTIFY, "%s (error code %d)\n", errmsg, err);
}
static void
logged_in_cb(struct sp_session *session, void *cb_arg, struct sp_credentials *credentials)
{
struct global_ctx *ctx = cb_arg;
char *db_stored_cred;
char *ptr;
int ret;
int i;
if (!db_is_initialized)
{
ret = db_perthread_init();
if (ret < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Error: DB init failed (spotify thread)\n");
return;
}
db_is_initialized = true;
}
DPRINTF(E_LOG, L_SPOTIFY, "Logged into Spotify succesfully\n");
if (!credentials->username || !credentials->stored_cred)
{
DPRINTF(E_LOG, L_SPOTIFY, "No credentials returned by Spotify, automatic login will not be possible\n");
return;
}
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);
pthread_mutex_lock(&ctx->lock);
ctx->response_pending = false;
ctx->status.logged_in = true;
snprintf(ctx->status.username, sizeof(ctx->status.username), "%s", credentials->username);
pthread_cond_signal(&ctx->cond);
pthread_mutex_unlock(&ctx->lock);
}
static void
logged_out_cb(void *cb_arg)
{
db_admin_delete("spotify_username");
db_admin_delete("spotify_stored_cred");
if (db_is_initialized)
db_perthread_deinit();
db_is_initialized = false;
}
static void
track_opened_cb(struct sp_session *session, void *cb_arg, int fd)
{
struct global_ctx *ctx = cb_arg;
DPRINTF(E_DBG, L_SPOTIFY, "track_opened_cb()\n");
pthread_mutex_lock(&ctx->lock);
ctx->response_pending = false;
ctx->status.track_opened = true;
pthread_cond_signal(&ctx->cond);
pthread_mutex_unlock(&ctx->lock);
}
static void
track_closed_cb(struct sp_session *session, void *cb_arg, int fd)
{
struct global_ctx *ctx = cb_arg;
DPRINTF(E_DBG, L_SPOTIFY, "track_closed_cb()\n");
pthread_mutex_lock(&ctx->lock);
ctx->response_pending = false;
ctx->status.track_opened = false;
pthread_cond_signal(&ctx->cond);
pthread_mutex_unlock(&ctx->lock);
}
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);
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
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);
}
/* --------------------- Implementation (input thread) ---------------------- */
struct sp_callbacks callbacks = {
.error = error_cb,
.logged_in = logged_in_cb,
.logged_out = logged_out_cb,
.track_opened = track_opened_cb,
.track_closed = track_closed_cb,
.https_get = https_get_cb,
.tcp_connect = tcp_connect,
.tcp_disconnect = tcp_disconnect,
.hexdump = hexdump_cb,
.logmsg = logmsg_cb,
};
// Has to be called after we have started receiving data, since ffmpeg needs to
// probe the data to find the audio streams
static int
playback_xcode_setup(struct playback_ctx *playback)
{
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 = playback->read_buf;
xcode->decode_ctx = transcode_decode_setup(XCODE_OGG, NULL, DATA_KIND_SPOTIFY, NULL, &xcode_evbuf_io, playback->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;
playback->xcode = xcode;
return 0;
error:
transcode_cleanup(&xcode);
return -1;
}
static void
playback_free(struct playback_ctx *playback)
{
if (!playback)
return;
if (playback->read_buf)
evbuffer_free(playback->read_buf);
if (playback->read_fd >= 0)
close(playback->read_fd);
transcode_cleanup(&playback->xcode);
free(playback);
}
static struct playback_ctx *
playback_new(struct input_source *source, int fd)
{
struct playback_ctx *playback;
CHECK_NULL(L_SPOTIFY, playback = calloc(1, sizeof(struct playback_ctx)));
CHECK_NULL(L_SPOTIFY, playback->read_buf = evbuffer_new());
playback->read_fd = fd;
playback->len_ms = source->len_ms;
return playback;
}
static int
stop(struct input_source *source)
{
struct global_ctx *ctx = &spotify_ctx;
struct playback_ctx *playback = source->input_ctx;
pthread_mutex_lock(&ctx->lock);
if (playback)
{
// Only need to request stop if spotifyc still has the track open
if (ctx->status.track_opened)
spotifyc_stop(playback->read_fd);
playback_free(playback);
}
if (source->evbuf)
evbuffer_free(source->evbuf);
source->input_ctx = NULL;
source->evbuf = NULL;
ctx->status.track_opened = false;
pthread_mutex_unlock(&ctx->lock);
return 0;
}
static int
setup(struct input_source *source)
{
struct global_ctx *ctx = &spotify_ctx;
int ret;
int fd;
pthread_mutex_lock(&ctx->lock);
fd = spotifyc_open(source->path, ctx->session);
if (fd < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Could not create fd for Spotify playback\n");
goto error;
}
ctx->response_pending = true;
while (ctx->response_pending)
pthread_cond_wait(&ctx->cond, &ctx->lock);
if (!ctx->status.track_opened)
{
close(fd);
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 playback_ctx, which is
// done in stop().
source->evbuf = evbuffer_new();
source->input_ctx = playback_new(source, fd);
if (!source->evbuf || !source->input_ctx)
goto error;
source->quality = spotify_quality;
// FIXME This makes sure we get the beginning of the file for ffmpeg to probe,
// but it doesn't work well if the player seeks after setup()
ret = spotifyc_play(fd);
if (ret < 0)
goto error;
pthread_mutex_unlock(&ctx->lock);
return 0;
error:
pthread_mutex_unlock(&ctx->lock);
stop(source);
return -1;
}
static int
play(struct input_source *source)
{
struct playback_ctx *playback = source->input_ctx;
int got;
int ret;
got = evbuffer_read(playback->read_buf, playback->read_fd, -1);
if (got < 0)
goto error;
// ffmpeg requires enough data to be able to probe the Ogg
playback->read_bytes += got;
if (playback->read_bytes < SPOTIFY_PROBE_SIZE_MIN)
{
input_wait();
return 0;
}
if (!playback->xcode)
{
ret = playback_xcode_setup(playback);
if (ret < 0)
goto error;
}
// Decode the Ogg Vorbis to PCM in chunks of 16 packets, which seems to keep
// the input buffer nice and full
ret = transcode(source->evbuf, NULL, playback->xcode, 16);
if (ret == 0)
{
input_write(source->evbuf, &source->quality, INPUT_FLAG_EOF);
stop(source);
return -1;
}
else if (ret < 0)
goto error;
// debug_count++;
// if (debug_count % 100 == 0)
// DPRINTF(E_DBG, L_SPOTIFY, "source->evbuf is %zu, playback->read_buf %zu, got is %d\n",
// evbuffer_get_length(source->evbuf), evbuffer_get_length(playback->read_buf), got);
input_write(source->evbuf, &source->quality, 0);
return 0;
error:
input_write(NULL, NULL, INPUT_FLAG_ERROR);
stop(source);
return -1;
}
static int
seek(struct input_source *source, int seek_ms)
{
return -1;
}
static int
init(void)
{
char *username = NULL;
char *db_stored_cred = NULL;
size_t db_stored_cred_len;
uint8_t *stored_cred = NULL;
size_t stored_cred_len;
int ret;
CHECK_ERR(L_SPOTIFY, mutex_init(&spotify_ctx.lock));
CHECK_ERR(L_SPOTIFY, pthread_cond_init(&spotify_ctx.cond, NULL));
ret = spotifyc_init(&callbacks, &spotify_ctx);
if (ret < 0)
goto error;
if ( db_admin_get(&username, "spotify_username") < 0 ||
db_admin_get(&db_stored_cred, "spotify_stored_cred") < 0 ||
!username || !db_stored_cred )
goto end; // User not logged in yet
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);
spotify_ctx.session = spotifyc_login_stored_cred(username, stored_cred, stored_cred_len);
if (!spotify_ctx.session)
goto error;
end:
free(username);
free(db_stored_cred);
free(stored_cred);
return 0;
error:
free(username);
free(db_stored_cred);
free(stored_cred);
return -1;
}
static void
deinit(void)
{
spotifyc_deinit();
}
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 (foreign threads) ----------- */
/* -------------- Dispatches functions exposed via spotify.h ---------------- */
/* (probably not necessary when libspotify is removed) */
/* Called from other threads than the input thread */
int
spotify_login_user(const char *user, const char *password, const char **errmsg)
spotify_init(void)
{
struct global_ctx *ctx = &spotify_ctx;
int ret;
struct spotify_backend *backend = backend_set();
pthread_mutex_lock(&ctx->lock);
if (!backend || !backend->init)
return 0; // Just a no-op
ctx->response_pending = true;
ctx->session = spotifyc_login_password(user, password);
if (!ctx->session)
{
pthread_mutex_unlock(&ctx->lock);
*errmsg = "Error creating Spotify session";
return -1;
}
while (ctx->response_pending)
pthread_cond_wait(&ctx->cond, &ctx->lock);
ret = ctx->status.logged_in ? 0 : -1;
if (ret < 0)
*errmsg = spotifyc_last_errmsg();
pthread_mutex_unlock(&ctx->lock);
return ret;
return backend->init();
}
void
spotify_login(char **arglist)
spotify_deinit(void)
{
return;
struct spotify_backend *backend = backend_set();
if (!backend || !backend->deinit)
return;
backend->deinit();
}
int
spotify_login(const char *username, const char *password, const char **errmsg)
{
struct spotify_backend *backend = backend_set();
if (!backend || !backend->login)
return -1;
return backend->login(username, password, errmsg);
}
int
spotify_login_token(const char *username, const char *token, const char **errmsg)
{
struct spotify_backend *backend = backend_set();
if (!backend || !backend->login_token)
return -1;
return backend->login_token(username, token, errmsg);
}
void
spotify_logout(void)
{
return;
struct spotify_backend *backend = backend_set();
if (!backend || !backend->logout)
return;
backend->logout();
}
int
spotify_relogin(void)
{
struct spotify_backend *backend = backend_set();
if (!backend || !backend->relogin)
return -1;
return backend->relogin();
}
void
spotify_uri_register(const char *uri)
{
struct spotify_backend *backend = backend_set();
if (!backend || !backend->uri_register)
return;
backend->uri_register(uri);
}
void
spotify_status_get(struct spotify_status *status)
{
struct global_ctx *ctx = &spotify_ctx;
struct spotify_backend *backend = backend_set();
pthread_mutex_lock(&ctx->lock);
memset(status, 0, sizeof(struct spotify_status));
memcpy(status->username, ctx->status.username, sizeof(status->username));
status->logged_in = ctx->status.logged_in;
status->installed = true;
if (!backend || !backend->status_get)
return;
pthread_mutex_unlock(&ctx->lock);
backend->status_get(status);
}

View File

@ -2,24 +2,48 @@
#define __SPOTIFY_H__
#include <stdbool.h>
#include <stdint.h>
struct spotify_status
{
bool installed;
bool logged_in;
bool track_opened;
char username[100];
char username[128];
};
struct spotify_backend
{
int (*init)(void);
void (*deinit)(void);
int (*login)(const char *username, const char *password, const char **errmsg);
int (*login_token)(const char *username, const char *token, const char **errmsg);
void (*logout)(void);
int (*relogin)(void);
void (*uri_register)(const char *uri);
void (*status_get)(struct spotify_status *status);
};
int
spotify_login_user(const char *user, const char *password, const char **errmsg);
spotify_init(void);
void
spotify_login(char **arglist);
spotify_deinit(void);
int
spotify_login(const char *username, const char *password, const char **errmsg);
int
spotify_login_token(const char *username, const char *token, const char **errmsg);
void
spotify_logout(void);
int
spotify_relogin(void);
void
spotify_uri_register(const char *uri);
void
spotify_status_get(struct spotify_status *status);

View File

@ -0,0 +1,756 @@
/*
* 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
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <pthread.h>
#ifdef HAVE_PTHREAD_NP_H
# include <pthread_np.h>
#endif
#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
{
pthread_mutex_t lock;
pthread_cond_t cond;
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;
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;
}
// 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);
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)
{
#if defined(HAVE_PTHREAD_SETNAME_NP)
pthread_setname_np(thread, "spotify");
#elif defined(HAVE_PTHREAD_SET_NAME_NP)
pthread_set_name_np(thread, "spotify");
#endif
}
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);
}
/* --------------------- Implementation (input thread) ---------------------- */
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,
};
static int64_t
download_seek(void *arg, int64_t offset, enum transcode_seek_type type)
{
struct global_ctx *ctx = &spotify_ctx;
struct download_ctx *download = arg;
int64_t out;
int ret;
pthread_mutex_lock(&ctx->lock);
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;
}
pthread_mutex_unlock(&ctx->lock);
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");
pthread_mutex_unlock(&ctx->lock);
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 global_ctx *ctx = &spotify_ctx;
struct download_ctx *download = source->input_ctx;
DPRINTF(E_DBG, L_SPOTIFY, "stop()\n");
pthread_mutex_lock(&ctx->lock);
download_free(download);
if (source->evbuf)
evbuffer_free(source->evbuf);
source->input_ctx = NULL;
source->evbuf = NULL;
pthread_mutex_unlock(&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(&ctx->lock);
fd = librespotc_open(source->path, ctx->session);
if (fd < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Eror 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(&ctx->lock);
return 0;
error:
pthread_mutex_unlock(&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;
// 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
return transcode_seek(download->xcode, seek_ms);
}
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;
}
static int
init(void)
{
struct sp_sysinfo sysinfo;
cfg_t *spotify_cfg;
char *username = NULL;
char *db_stored_cred = NULL;
int ret;
spotify_cfg = cfg_getsec(cfg, "spotify");
if (cfg_getbool(spotify_cfg, "use_libspotify"))
return -1;
CHECK_ERR(L_SPOTIFY, mutex_init(&spotify_ctx.lock));
CHECK_ERR(L_SPOTIFY, pthread_cond_init(&spotify_ctx.cond, NULL));
snprintf(sysinfo.client_name, sizeof(sysinfo.client_name), PACKAGE_NAME);
snprintf(sysinfo.client_version, sizeof(sysinfo.client_version), PACKAGE_VERSION);
snprintf(sysinfo.client_build_id, sizeof(sysinfo.client_build_id), "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:
spotify_ctx.bitrate_preferred = SP_BITRATE_96;
break;
case 2:
spotify_ctx.bitrate_preferred = SP_BITRATE_160;
break;
case 3:
spotify_ctx.bitrate_preferred = SP_BITRATE_320;
break;
default:
spotify_ctx.bitrate_preferred = SP_BITRATE_ANY;
}
// 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(&spotify_ctx, username, db_stored_cred);
if (ret < 0)
goto error;
}
free(username);
free(db_stored_cred);
return 0;
error:
free(username);
free(db_stored_cred);
return -1;
}
static void
deinit(void)
{
librespotc_deinit();
CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&spotify_ctx.cond));
CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&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(&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(&ctx->lock);
return 0;
error:
if (ctx->session)
librespotc_logout(ctx->session);
ctx->session = NULL;
if (errmsg)
*errmsg = librespotc_last_errmsg();
pthread_mutex_unlock(&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(&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(&ctx->lock);
return 0;
error:
if (ctx->session)
librespotc_logout(ctx->session);
ctx->session = NULL;
if (errmsg)
*errmsg = librespotc_last_errmsg();
pthread_mutex_unlock(&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(&ctx->lock);
librespotc_logout(ctx->session);
ctx->session = NULL;
pthread_mutex_unlock(&ctx->lock);
}
static int
relogin(void)
{
return 0; // re-login is only relevant for libspotify, here it is just a no-op
}
static void
status_get(struct spotify_status *status)
{
struct global_ctx *ctx = &spotify_ctx;
pthread_mutex_lock(&ctx->lock);
memcpy(status->username, ctx->status.username, sizeof(status->username));
status->logged_in = ctx->status.logged_in;
status->installed = true;
pthread_mutex_unlock(&ctx->lock);
}
struct spotify_backend spotify_librespotc =
{
.login = login,
.login_token = login_token,
.logout = logout,
.relogin = relogin,
.status_get = status_get,
};

View File

@ -0,0 +1,133 @@
/*
* Copyright (C) 2017 Espen Jurgensen
*
* 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
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include "input.h"
#include "conffile.h"
#include "logger.h"
#include "spotify.h"
#include "libspotify/libspotify.h"
// How many retries to start playback if resource is still loading
#define LIBSPOTIFY_SETUP_RETRIES 5
// How long to wait between retries in microseconds (500000 = 0.5 seconds)
#define LIBSPOTIFY_SETUP_RETRY_WAIT 500000
static int
init(void)
{
return cfg_getbool(cfg_getsec(cfg, "spotify"), "use_libspotify") ? 0 : -1;
}
static int
setup(struct input_source *source)
{
int i = 0;
int ret;
while((ret = libspotify_playback_setup(source->path)) == LIBSPOTIFY_SETUP_ERROR_IS_LOADING)
{
if (i >= LIBSPOTIFY_SETUP_RETRIES)
break;
DPRINTF(E_DBG, L_SPOTIFY, "Resource still loading (%d)\n", i);
usleep(LIBSPOTIFY_SETUP_RETRY_WAIT);
i++;
}
if (ret < 0)
return -1;
ret = libspotify_playback_play();
if (ret < 0)
return -1;
return 0;
}
static int
stop(struct input_source *source)
{
int ret;
ret = libspotify_playback_stop();
if (ret < 0)
return -1;
return 0;
}
static int
seek(struct input_source *source, int seek_ms)
{
int ret;
ret = libspotify_playback_seek(seek_ms);
if (ret < 0)
return -1;
return ret;
}
struct input_definition input_libspotify =
{
.name = "libspotify",
.type = INPUT_TYPE_LIBSPOTIFY,
.disabled = 0,
.init = init,
.setup = setup,
.stop = stop,
.seek = seek,
};
// No-op for libspotify since it doesn't support logging in with the web api token
static int
login_token(const char *username, const char *token, const char **errmsg)
{
return 0;
}
static void
status_get(struct spotify_status *status)
{
struct spotify_status_info info = { 0 };
libspotify_status_info_get(&info);
status->installed = info.libspotify_installed;
status->logged_in = info.libspotify_logged_in;
snprintf(status->username, sizeof(status->username), "%s", info.libspotify_user);
}
struct spotify_backend spotify_libspotify =
{
.init = libspotify_init,
.deinit = libspotify_deinit,
.login = libspotify_login,
.login_token = login_token,
.logout = libspotify_logout,
.relogin = libspotify_relogin,
.uri_register = libspotify_uri_register,
.status_get = status_get,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +0,0 @@
#ifndef __SPOTIFYC_H__
#define __SPOTIFYC_H__
#include <inttypes.h>
struct sp_session;
enum sp_bitrates
{
SP_BITRATE_96,
SP_BITRATE_160,
SP_BITRATE_320,
};
struct sp_credentials
{
char *username;
char *password;
uint8_t stored_cred[256]; // Actual size is 146, but leave room for some more
size_t stored_cred_len;
uint8_t token[256]; // Actual size is 190, but leave room for some more
size_t token_len;
};
struct sp_callbacks
{
void (*logged_in)(struct sp_session *session, void *cb_arg, struct sp_credentials *credentials);
void (*logged_out)(void *cb_arg);
void (*track_opened)(struct sp_session *session, void *cb_arg, int fd);
void (*track_closed)(struct sp_session *session, void *cb_arg, int fd);
void (*track_seeked)(struct sp_session *session, void *cb_arg, int fd);
void (*error)(void *cb_arg, int err, const char *errmsg);
// Bring your own https client and tcp connector
int (*https_get)(char **body, const char *url);
int (*tcp_connect)(const char *address, unsigned short port);
void (*tcp_disconnect)(int fd);
// Debugging
void (*hexdump)(const char *msg, uint8_t *data, size_t data_len);
void (*logmsg)(const char *fmt, ...);
};
// Async interface
struct sp_session *
spotifyc_login_password(const char *username, const char *password);
struct sp_session *
spotifyc_login_stored_cred(const char *username, uint8_t *stored_cred, size_t stored_cred_len);
struct sp_session *
spotifyc_login_token(const char *username, uint8_t *token, size_t token_len);
void
spotifyc_logout(struct sp_session *session);
int
spotifyc_open(const char *path, struct sp_session *session);
void
spotifyc_bitrate_set(enum sp_bitrates bitrate, struct sp_session *session);
// Starts writing audio to the file descriptor
int
spotifyc_play(int fd);
int
spotifyc_seek(int seek_ms, int fd);
int
spotifyc_stop(int fd);
// Sync interface
const char *
spotifyc_last_errmsg(void);
// This Spotify implementation is entirely async, so first the caller must set
// up callbacks
int
spotifyc_init(struct sp_callbacks *callbacks, void *cb_arg);
void
spotifyc_deinit(void);
#endif /* !__SPOTIFYC_H__ */

View File

@ -67,9 +67,6 @@
#ifdef LASTFM
# include "lastfm.h"
#endif
#ifdef SPOTIFY
# include "spotify.h"
#endif
#define F_SCAN_BULK (1 << 0)
@ -95,7 +92,6 @@ enum file_type {
FILE_CTRL_REMOTE,
FILE_CTRL_RAOP_VERIFICATION,
FILE_CTRL_LASTFM,
FILE_CTRL_SPOTIFY,
FILE_CTRL_INITSCAN,
FILE_CTRL_METASCAN, // forced scan for meta, preserves existing db records
FILE_CTRL_FULLSCAN,
@ -350,9 +346,6 @@ file_type_get(const char *path) {
if (strcasecmp(ext, ".lastfm") == 0)
return FILE_CTRL_LASTFM;
if (strcasecmp(ext, ".spotify") == 0)
return FILE_CTRL_SPOTIFY;
if (strcasecmp(ext, ".init-rescan") == 0)
return FILE_CTRL_INITSCAN;
@ -700,17 +693,6 @@ process_file(char *file, struct stat *sb, enum file_type file_type, int scan_typ
#endif
break;
case FILE_CTRL_SPOTIFY:
#ifdef SPOTIFY
if (flags & F_SCAN_BULK)
DPRINTF(E_LOG, L_SCAN, "Bulk scan will ignore '%s' (to process, add it after startup)\n", file);
else
kickoff(spotify_login, file, 2);
#else
DPRINTF(E_LOG, L_SCAN, "Found '%s', but this version was built without Spotify support\n", file);
#endif
break;
case FILE_CTRL_INITSCAN:
if (flags & F_SCAN_BULK)
break;

View File

@ -35,6 +35,7 @@
#include "listener.h"
#include "logger.h"
#include "misc_json.h"
#include "inputs/spotify.h"
enum spotify_request_type {
@ -134,7 +135,7 @@ static bool scanning;
// Endpoints and credentials for the web api
static const char *spotify_client_id = "0e684a5422384114a8ae7ac020f01789";
static const char *spotify_client_secret = "232af95f39014c9ba218285a5c11a239";
static const char *spotify_scope = "playlist-read-private playlist-read-collaborative user-library-read user-read-private";
static const char *spotify_scope = "playlist-read-private playlist-read-collaborative user-library-read user-read-private streaming";
static const char *spotify_auth_uri = "https://accounts.spotify.com/authorize";
static const char *spotify_token_uri = "https://accounts.spotify.com/api/token";
@ -974,10 +975,9 @@ spotifywebapi_oauth_uri_get(const char *redirect_uri)
/* Thread: httpd */
int
spotifywebapi_oauth_callback(struct evkeyvalq *param, const char *redirect_uri, char **errmsg)
spotifywebapi_oauth_callback(struct evkeyvalq *param, const char *redirect_uri, const char **errmsg)
{
const char *code;
const char *err;
int ret;
*errmsg = NULL;
@ -985,18 +985,19 @@ spotifywebapi_oauth_callback(struct evkeyvalq *param, const char *redirect_uri,
code = evhttp_find_header(param, "code");
if (!code)
{
*errmsg = safe_asprintf("Error: Didn't receive a code from Spotify");
*errmsg = "Error: Didn't receive a code from Spotify";
return -1;
}
DPRINTF(E_DBG, L_SPOTIFY, "Received OAuth code: %s\n", code);
ret = token_get(code, redirect_uri, &err);
ret = token_get(code, redirect_uri, errmsg);
if (ret < 0)
{
*errmsg = safe_asprintf("Error: %s", err);
return -1;
}
return -1;
ret = spotify_login_token(spotify_credentials.user, spotify_credentials.access_token, errmsg);
if (ret < 0)
return -1;
// Trigger scan after successful access to spotifywebapi
spotifywebapi_fullrescan();
@ -1470,6 +1471,9 @@ track_add(struct spotify_track *track, struct spotify_album *album, const char *
free_mfi(&mfi, 1);
}
// This is only required for the libspotify backend
spotify_uri_register(track->uri);
if (album && album->uri)
cache_artwork_ping(track->uri, album->mtime, 0);
else
@ -1709,7 +1713,7 @@ scan_playlists(enum spotify_request_type request_type)
}
static void
create_saved_tracks_playlist()
create_saved_tracks_playlist(void)
{
struct playlist_info pli =
{
@ -1735,7 +1739,7 @@ create_saved_tracks_playlist()
* Add or update playlist folder for all spotify playlists (if enabled in config)
*/
static void
create_base_playlist()
create_base_playlist(void)
{
cfg_t *spotify_cfg;
struct playlist_info pli =
@ -1792,17 +1796,17 @@ scan(enum spotify_request_type request_type)
/* Thread: library */
static int
initscan()
initscan(void)
{
int ret;
/* Refresh access token for the spotify webapi */
ret = token_refresh();
ret = token_refresh();
if (ret < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Spotify webapi token refresh failed. "
"In order to use the web api, authorize the server to access "
"your saved tracks by visiting http://owntone.local:3689\n");
"In order to use Spotify, authorize the server to access your saved "
"tracks by visiting http://owntone.local:3689\n");
db_spotify_purge();
@ -1811,6 +1815,21 @@ initscan()
spotify_saved_plid = 0;
/*
* libspotify needs to be logged in before before scanning tracks from the web
* since scanned tracks need to be registered for playback
*/
ret = spotify_relogin();
if (ret < 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "libspotify-login failed. In order to use Spotify, "
"provide valid credentials for libspotify by visiting http://owntone.local:3689\n");
db_spotify_purge();
return 0;
}
/*
* Scan saved tracks from the web api
*/
@ -1821,7 +1840,7 @@ initscan()
/* Thread: library */
static int
rescan()
rescan(void)
{
scan(SPOTIFY_REQUEST_TYPE_RESCAN);
return 0;
@ -1829,7 +1848,7 @@ rescan()
/* Thread: library */
static int
metarescan()
metarescan(void)
{
scan(SPOTIFY_REQUEST_TYPE_METARESCAN);
return 0;
@ -1837,7 +1856,7 @@ metarescan()
/* Thread: library */
static int
fullrescan()
fullrescan(void)
{
db_spotify_purge();
scan(SPOTIFY_REQUEST_TYPE_RESCAN);
@ -2050,7 +2069,8 @@ spotifywebapi_init()
{
CHECK_ERR(L_SPOTIFY, mutex_init(&token_lck));
return 0;
// Required for libspotify backend
return spotify_init();
}
static void
@ -2058,6 +2078,8 @@ spotifywebapi_deinit()
{
CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&token_lck));
spotify_deinit();
free_credentials();
}

View File

@ -45,7 +45,7 @@ struct spotifywebapi_access_token
char *
spotifywebapi_oauth_uri_get(const char *redirect_uri);
int
spotifywebapi_oauth_callback(struct evkeyvalq *param, const char *redirect_uri, char **errmsg);
spotifywebapi_oauth_callback(struct evkeyvalq *param, const char *redirect_uri, const char **errmsg);
void
spotifywebapi_fullrescan(void);

View File

@ -70,6 +70,12 @@ static char *buildopts[] =
#else
"Without Spotify",
#endif
#ifdef SPOTIFY_LIBRESPOTC
"librespot-c",
#endif
#ifdef SPOTIFY_LIBSPOTIFY
"libspotify",
#endif
#ifdef LASTFM
"LastFM",
#else

View File

@ -1,68 +0,0 @@
#ifndef __SPOTIFY_H__
#define __SPOTIFY_H__
#include <event2/event.h>
#include <event2/buffer.h>
#include <event2/http.h>
#include <stdbool.h>
struct spotify_status_info
{
bool libspotify_installed;
bool libspotify_logged_in;
char libspotify_user[100];
};
#define SPOTIFY_SETUP_ERROR_IS_LOADING -2
int
spotify_playback_setup(const char *path);
int
spotify_playback_play();
int
spotify_playback_pause();
//void
//spotify_playback_pause_nonblock(void);
int
spotify_playback_stop(void);
//void
//spotify_playback_stop_nonblock(void);
int
spotify_playback_seek(int ms);
//int
//spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h);
int
spotify_relogin();
int
spotify_login_user(const char *user, const char *password, char **errmsg);
void
spotify_login(char **arglist);
void
spotify_logout(void);
void
spotify_status_info_get(struct spotify_status_info *info);
void
spotify_uri_register(const char *uri);
int
spotify_init(void);
void
spotify_deinit(void);
#endif /* !__SPOTIFY_H__ */

View File

@ -871,12 +871,7 @@ avio_evbuffer_open(struct transcode_evbuf_io *evbuf_io, int is_output)
return NULL;
}
// 0 here does not seem to mean not seekable, because ffmpeg will still call
// avio_evbuffer_seek. If set to AVIO_SEEKABLE_NORMAL then ffmpeg seems to
// make random access seek requests during input_open (i.e. asking for start
// and end of file), which are hard to fulfill when the source is something
// that is downloaded.
s->seekable = 0;
s->seekable = (evbuf_io->seekfn ? AVIO_SEEKABLE_NORMAL : 0);
return s;
}