[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:
parent
2bbc5f16c5
commit
6f0fef6179
|
@ -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
|
||||
|
||||
|
|
45
configure.ac
45
configure.ac
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
22
src/input.c
22
src/input.c
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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__ */
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
|
@ -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, ¤t_cmd->ret);
|
||||
|
||||
cmdbase->current_cmd = NULL;
|
||||
|
||||
/* Process commands again */
|
||||
event_add(cmdbase->command_event, NULL);
|
||||
|
||||
pthread_cond_signal(¤t_cmd->cond);
|
||||
pthread_mutex_unlock(¤t_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);
|
||||
}
|
||||
|
|
@ -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
|
@ -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);
|
|
@ -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;
|
||||
}
|
|
@ -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__ */
|
|
@ -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__
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
test1
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
@ -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__ */
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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
|
@ -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__ */
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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__ */
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue