Merge pull request #408 from chme/web

Add a web api and a webinterface
This commit is contained in:
ejurgensen 2017-09-15 23:15:49 +02:00 committed by GitHub
commit 546362980b
49 changed files with 15441 additions and 264 deletions

View File

@ -3,11 +3,11 @@ sudo: required
dist: trusty
env:
matrix:
- CFG="--disable-verification"
- CFG="--enable-lastfm --disable-verification"
- CFG="--enable-spotify --disable-verification"
- CFG="--enable-chromecast --disable-verification"
- CFG="--with-pulseaudio --disable-verification"
- CFG="--disable-websocket --disable-verification"
- CFG="--disable-websocket --enable-lastfm --disable-verification"
- CFG="--disable-websocket --enable-spotify --disable-verification"
- CFG="--disable-websocket --enable-chromecast --disable-verification"
- CFG="--disable-websocket --with-pulseaudio --disable-verification"
script:
- autoreconf -fi

13
INSTALL
View File

@ -31,16 +31,17 @@ libraries:
antlr3 libantlr3c-dev libconfuse-dev libunistring-dev libsqlite3-dev \
libavcodec-dev libavformat-dev libavfilter-dev libswscale-dev libavutil-dev \
libasound2-dev libmxml-dev libgcrypt11-dev libavahi-client-dev zlib1g-dev \
libevent-dev libplist-dev libsodium-dev
libevent-dev libplist-dev libsodium-dev libjson-c-dev
Optional packages:
Feature | Configure argument | Packages
--------------------|------------------------|---------------------------------------------
Chromecast | --enable-chromecast | libjson-c-dev libgnutls-dev libprotobuf-c-dev
Chromecast | --enable-chromecast | libgnutls-dev libprotobuf-c-dev
LastFM | --enable-lastfm | libcurl4-gnutls-dev OR libcurl4-openssl-dev
iTunes XML | --disable-itunes | libplist-dev
Device verification | --disable-verification | libplist-dev libsodium-dev
Websocket | --disable-websocket | libwebsockets-dev
Pulseaudio | --with-pulseaudio | libpulse-dev
Note that while forked-daapd will work with versions of libevent between 2.0.0
@ -220,6 +221,8 @@ Libraries:
from <http://zlib.net/>
- libunistring 0.9.3+
from <http://www.gnu.org/software/libunistring/#downloading>
- libjson-c
from <https://github.com/json-c/json-c/wiki>
- libasound (optional - ALSA local audio)
often already installed as part of your distro
- libpulse (optional - Pulseaudio local audio)
@ -232,12 +235,12 @@ Libraries:
from <https://developer.spotify.com>
- libcurl (optional - LastFM support)
from <http://curl.haxx.se/libcurl/>
- libjson-c (optional - Chromecast support)
from <https://github.com/json-c/json-c/wiki>
- libgnutls (optional - Chromecast support)
from <http://www.gnutls.org/>
- libprotobuf-c (optional - Chromecast support)
from <https://github.com/protobuf-c/protobuf-c/wiki>
- libwebsockets (optional - websocket support)
from <https://libwebsockets.org/>
If using binary packages, remember that you need the development packages to
build forked-daapd (usually named -dev or -devel).
@ -294,6 +297,8 @@ feature.
Support for Apple TV device verification is optional. Use --disable-verification
to disable this feature.
Support for websocket is optional. Use --disable-websocket to disable this feature.
Support for Chromecast devices is optional. Use --enable-chromecast to enable
this feature.

View File

@ -8,7 +8,7 @@ sysconf_DATA = $(CONF_FILE)
BUILT_SOURCES = $(CONF_FILE) $(SYSTEMD_SERVICE_FILE)
SUBDIRS = sqlext src
SUBDIRS = sqlext src htdocs
dist_man_MANS = forked-daapd.8

View File

@ -259,6 +259,10 @@ dnl Build with libcurl
FORK_ARG_WITH_CHECK([FORKED_OPTS], [libcurl support], [libcurl], [LIBCURL],
[libcurl], [curl_global_init], [curl/curl.h])
dnl Build with libwebsockets
FORK_ARG_WITH_CHECK([FORKED_OPTS], [libwebsockets support], [libwebsockets], [LIBWEBSOCKETS],
[libwebsockets >= 2.0.2])
dnl Build with libsodium
FORK_ARG_WITH_CHECK([FORKED_OPTS], [libsodium support], [libsodium], [LIBSODIUM],
[libsodium], [sodium_init], [sodium.h])
@ -340,6 +344,12 @@ FORK_ARG_ENABLE([Chromecast support], [chromecast], [CHROMECAST],
AM_CONDITIONAL([COND_CHROMECAST], [[test "x$enable_chromecast" = "xyes"]])
AM_CONDITIONAL([COND_PROTOBUF_OLD], [[test "x$protobuf_old" = "xyes"]])
dnl Websocket support with libwebsockets
FORK_ARG_DISABLE([Websocket support], [websocket], [WEBSOCKET],
[AS_IF([[test "x$with_libwebsockets" = "xno"]],
[AC_MSG_ERROR([[Websocket support requires libwebsockets]])])])
AM_CONDITIONAL([COND_WEBSOCKET], [[test "x$enable_websocket" = "xyes"]])
dnl iTunes playlists with libplist
FORK_ARG_DISABLE([iTunes Music Library XML support], [itunes], [ITUNES],
[AS_IF([[test "x$with_libplist" = "xno"]],
@ -380,6 +390,7 @@ dnl --- End options ---
AC_CONFIG_FILES([
src/Makefile
sqlext/Makefile
htdocs/Makefile
Makefile
forked-daapd.spec
])

View File

@ -21,8 +21,11 @@ general {
logfile = "@localstatedir@/log/@PACKAGE@.log"
loglevel = log
# Admin password for the non-existent web interface
admin_password = "unused"
# Admin password for the web interface
# If not set (default), access to the web interface is only permitted from localhost
# admin_password = ""
# Websocket port for the web interface.
# websocket_port = 3688
# Enable/disable IPv6
ipv6 = yes

32
htdocs/Makefile.am Normal file
View File

@ -0,0 +1,32 @@
htdocsdir = $(datadir)/forked-daapd/htdocs
htdocs_DATA = \
admin.html
htdocscssdir = $(datadir)/forked-daapd/htdocs/css
htdocscss_DATA = \
css/bulma.min.css \
css/font-awesome.min.css \
css/forked-daapd.css
htdocsfontsdir = $(datadir)/forked-daapd/htdocs/fonts
htdocsfonts_DATA = \
fonts/FontAwesome.otf\
fonts/fontawesome-webfont.eot \
fonts/fontawesome-webfont.svg \
fonts/fontawesome-webfont.ttf \
fonts/fontawesome-webfont.woff \
fonts/fontawesome-webfont.woff2
htdocsjsdir = $(datadir)/forked-daapd/htdocs/js
htdocsjs_DATA = \
js/axios.js \
js/axios.map \
js/axios.min.js \
js/axios.min.map \
js/forked-daapd.js \
js/vue.js \
js/vue.min.js

198
htdocs/admin.html Normal file
View File

@ -0,0 +1,198 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>forked-daapd</title>
<link rel="stylesheet" href="/css/font-awesome.min.css">
<link rel="stylesheet" href="/css/bulma.min.css">
<link rel="stylesheet" href="/css/forked-daapd.css">
</head>
<body>
<div id="root" v-cloak>
<!--
############# Navbar #############
-->
<nav class="navbar">
<div class="navbar-brand">
<b class="navbar-item">forked-daapd</b>
<a class="navbar-item" href="https://github.com/ejurgensen/forked-daapd" title="GitHub"><i class="fa fa-github"></i></a>
</div>
</nav>
<!--
############# Hero section #############
-->
<section class="hero is-dark is-bold">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column">
<nav class="level is-mobile">
<div class="level-item has-text-centered">
<div>
<p class="heading">Artists</p>
<p class="title">{{ library.artists }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Albums</p>
<p class="title">{{ library.albums }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Songs</p>
<p class="title">{{ library.songs }}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Total playtime</p>
<p class="title">{{ library.db_playtime | duration }}</p>
</div>
</div>
</nav>
</div>
</div> <!-- columns -->
</div><!-- container -->
</div><!-- hero -->
</section>
<!--
############# Content section #############
-->
<section class="section">
<div class="container">
<div class="columns">
<div class="column">
<div class="card">
<header class="card-header">
<p class="card-header-title">
<span class="icon" v-show="library.updating"><i class="fa fa-refresh fa-spin"></i></span>
<span class="icon" v-show="!library.updating"><i class="fa fa-refresh"></i></span>
Update library
</p>
</header>
<div class="card-content">
<div class="content">
Scan new and modified items into your library.
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item is-primary" v-on:click="update" v-show="!library.updating">Update</a>
<span class="card-footer-item" v-show="library.updating">Update in progress ...</span>
</footer>
</div> <!-- card update library -->
</div> <!-- column -->
<div class="column">
<div class="card">
<header class="card-header">
<p class="card-header-title">
<span class="icon"><i class="fa fa-mobile"></i></span> Remote pairing
</p>
</header>
<div class="card-content">
<div class="content" v-show="pairing.active">
<p>Remote pairing request from <b>{{pairing.remote}}</b></p>
<form v-on:submit.prevent="kickoffPairing">
<div class="field has-addons">
<div class="control">
<input class="input" type="text" placeholder="Enter pairing code" v-model="pairing_req.pin">
</div>
<div class="control">
<button class="button is-primary" type="submit">Send</button>
</div>
</div>
</form>
</div>
<div class="content" v-show="!pairing.active">
<p>No active pairing request.</p>
<a class="button" v-on:click="loadPairing" v-show="!config.websocket_port">Refresh</a>
</div>
</div>
</div> <!-- card remote pairing -->
</div> <!-- column -->
<div class="column">
<div class="card" v-show="spotify.enabled">
<header class="card-header">
<p class="card-header-title">
<span class="icon"><i class="fa fa-spotify"></i></span> Spotify
</p>
</header>
<div class="card-content">
<div class="content" v-show="!spotify.libspotify_installed">
<p><b>libspotify</b> is not installed (required for playing spotify tracks)</p>
</div>
<div class="content" v-show="spotify.libspotify_installed">
<div v-show="!spotify.libspotify_logged_in"><p><b>libspotify</b> (requires Spotify premium account, enables playback of Spotify songs):</p>
<form v-on:submit.prevent="loginLibspotify">
<div class="field has-addons">
<div class="control">
<input class="input" type="text" placeholder="Username" v-model="libspotify.user">
<p class="help is-danger">{{ libspotify.errors.user }}</p>
</div>
<div class="control">
<input class="input" type="password" placeholder="Password" v-model="libspotify.password">
<p class="help is-danger">{{ libspotify.errors.password }}</p>
</div>
<div class="control">
<button class="button" type="submit">Login</button>
</div>
</div>
<p class="help is-danger">{{ libspotify.errors.error }}</p>
</form>
</div>
<p v-show="spotify.libspotify_logged_in"><b>libspotify</b> (requires Spotify premium account, enables playback of Spotify songs): logged in as <b>{{ spotify.libspotify_user }}</b></p>
<hr>
<div v-show="!spotify.webapi_token_valid">
<p><b>Spotify Web API</b> access is required to add saved albums and playlists to your library.</p>
<a class="button" v-bind:href="spotify.oauth_uri">Authorize Web API access</a>
</div>
<div v-show="spotify.webapi_token_valid">
<p><b>Spotify Web API</b>: access authorized for <b>{{ spotify.webapi_user }}</b></p>
<a class="button" v-bind:href="spotify.oauth_uri">Reauthorize Web API access</a>
</div>
</div>
</div>
</div> <!-- card spotify -->
</div> <!-- column -->
</div> <!-- columns -->
</div> <!-- container -->
</section>
<footer class="footer">
<div class="container">
<div class="content has-text-centered">
<p>
<strong>forked-daapd</strong> - version {{ config.version }}
</p>
<p class="is-size-7">Compiled with support for {{ config.buildoptions | join }}.</p>
<p class="is-size-7">Web interface built with <a href="http://bulma.io">Bulma</a>, <a href="http://fontawesome.io/">Font Awesome</a>, <a href="https://vuejs.org/">Vue.js</a>, <a href="https://github.com/mzabriskie/axios">axios</a>.</p>
</div>
</div>
</footer>
</div> <!-- #root -->
<script src="/js/vue.min.js"></script>
<script src="/js/axios.min.js"></script>
<script src="/js/forked-daapd.js"></script>
</body>
</html>

2
htdocs/css/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

4
htdocs/css/font-awesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
[v-cloak] {
display: none;
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

1585
htdocs/js/axios.js Normal file

File diff suppressed because it is too large Load Diff

1
htdocs/js/axios.map Normal file

File diff suppressed because one or more lines are too long

9
htdocs/js/axios.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
htdocs/js/axios.min.map Normal file

File diff suppressed because one or more lines are too long

111
htdocs/js/forked-daapd.js Normal file
View File

@ -0,0 +1,111 @@
var app = new Vue({
el: '#root',
data: {
config: {},
library: {},
spotify: {},
pairing: {},
pairing_req: { pin: '' },
libspotify: { user: '', password: '', errors: { user: '', password: '', error: '' } }
},
created: function () {
this.loadConfig();
this.loadLibrary();
this.loadSpotify();
this.loadPairing();
},
methods: {
loadConfig: function() {
axios.get('/api/config').then(response => {
this.config = response.data;
this.connect()});
},
loadLibrary: function() {
axios.get('/api/library').then(response => this.library = response.data);
},
loadSpotify: function() {
axios.get('/api/spotify').then(response => this.spotify = response.data);
},
loadPairing: function() {
axios.get('/api/pairing').then(response => this.pairing = response.data);
},
update: function() {
this.library.updating = true;
axios.get('/api/update').then(console.log('Library is updating'));
},
kickoffPairing: function() {
axios.post('/api/pairing', this.pairing_req).then(response => {
console.log('Kicked off pairing');
if (!this.config.websocket_port) {
this.pairing = {};
}
});
},
loginLibspotify: function() {
axios.post('/api/spotify-login', this.libspotify).then(response => {
this.libspotify.user = '';
this.libspotify.password = '';
this.libspotify.errors.user = '';
this.libspotify.errors.password = '';
this.libspotify.errors.error = '';
if (!response.data.success) {
this.libspotify.errors.user = response.data.errors.user;
this.libspotify.errors.password = response.data.errors.password;
this.libspotify.errors.error = response.data.errors.error;
}
});
},
connect: function() {
if (this.config.websocket_port <= 0) {
console.log('Websocket disabled');
return;
}
var socket = new WebSocket('ws://' + document.domain + ':' + this.config.websocket_port, 'notify');
const vm = this;
socket.onopen = function() {
socket.send(JSON.stringify({ notify: ['update', 'pairing', 'spotify']}));
socket.onmessage = function(response) {
console.log(response.data); // upon message
var data = JSON.parse(response.data);
if (data.notify.includes('update')) {
vm.loadLibrary();
}
if (data.notify.includes('pairing')) {
vm.loadPairing();
}
if (data.notify.includes('spotify')) {
vm.loadSpotify();
}
};
};
}
},
filters: {
duration: function(seconds) {
// Display seconds as hours:minutes:seconds
var h = Math.floor(seconds / 3600);
var m = Math.floor(seconds % 3600 / 60);
var s = Math.floor(seconds % 3600 % 60);
return h + ":" + ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2);
},
join: function(array) {
return array.join(', ');
}
}
})

9175
htdocs/js/vue.js Normal file

File diff suppressed because it is too large Load Diff

8
htdocs/js/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -43,6 +43,10 @@ else
MDNS_SRC=mdns_dnssd.c
endif
if COND_WEBSOCKET
WEBSOCKET_SRC=websocket.c websocket.h
endif
if COND_FFMPEG_LEGACY
FFMPEG_SRC=transcode_legacy.c artwork_legacy.c ffmpeg-compat.h
else
@ -108,11 +112,13 @@ forked_daapd_SOURCES = main.c \
httpd_rsp.c httpd_rsp.h \
httpd_daap.c httpd_daap.h \
httpd_dacp.c httpd_dacp.h \
httpd_jsonapi.c httpd_jsonapi.h \
httpd_streaming.c httpd_streaming.h \
http.c http.h \
dmap_common.c dmap_common.h \
$(FFMPEG_SRC) \
misc.c misc.h \
misc_json.c misc_json.h \
rng.c rng.h \
rsp_query.c rsp_query.h \
daap_query.c daap_query.h \
@ -131,8 +137,9 @@ forked_daapd_SOURCES = main.c \
listener.c listener.h \
commands.c commands.h \
mxml-compat.h \
$(WEBSOCKET_SRC) \
$(GPERF_SRC) \
$(ANTLR_SRC)
$(ANTLR_SRC)
# built by maintainers, and distributed. Clean with maintainer-clean
BUILT_SOURCES = \

View File

@ -46,6 +46,7 @@ static cfg_opt_t sec_general[] =
{
CFG_STR("uid", "nobody", CFGF_NONE),
CFG_STR("admin_password", NULL, CFGF_NONE),
CFG_INT("websocket_port", 3688, CFGF_NONE),
CFG_STR("logfile", STATEDIR "/log/" PACKAGE ".log", CFGF_NONE),
CFG_STR("db_path", STATEDIR "/cache/" PACKAGE "/songs3.db", CFGF_NONE),
CFG_INT("db_pragma_cache_size", -1, CFGF_NONE),

View File

@ -1685,6 +1685,31 @@ db_query_fetch_count(struct query_params *qp, struct filecount_info *fci)
return 0;
}
int
db_filecount_get(struct filecount_info *fci, struct query_params *qp)
{
int ret;
ret = db_query_start(qp);
if (ret < 0)
{
db_query_end(qp);
return -1;
}
ret = db_query_fetch_count(qp, fci);
if (ret < 0)
{
db_query_end(qp);
return -1;
}
db_query_end(qp);
return 0;
}
int
db_query_fetch_string(struct query_params *qp, char **string)
{

View File

@ -562,6 +562,9 @@ db_file_enable_bycookie(uint32_t cookie, char *path);
int
db_file_update_directoryid(char *path, int dir_id);
int
db_filecount_get(struct filecount_info *fci, struct query_params *qp);
/* Playlists */
int
db_pl_get_count(void);

View File

@ -59,6 +59,7 @@
#include "httpd_rsp.h"
#include "httpd_daap.h"
#include "httpd_dacp.h"
#include "httpd_jsonapi.h"
#include "httpd_streaming.h"
#include "transcode.h"
#ifdef LASTFM
@ -67,6 +68,10 @@
#ifdef HAVE_SPOTIFY_H
# include "spotify.h"
#endif
#ifdef WEBSOCKET
# include "websocket.h"
#endif
/*
* HTTP client quirks by User-Agent, from mt-daapd
@ -86,9 +91,9 @@
* + Does not encode space as + in query string
*/
#define WEB_ROOT DATADIR "/htdocs"
#define STREAM_CHUNK_SIZE (64 * 1024)
#define WEBFACE_ROOT DATADIR "/webface/"
#define ERR_PAGE "<html>\n<head>\n" \
"<title>%d %s</title>\n" \
"</head>\n<body>\n" \
@ -149,6 +154,8 @@ static int httpd_port;
struct stream_ctx *g_st;
#endif
static void
redirect_to_admin(struct evhttp_request *req);
static void
stream_end(struct stream_ctx *st, int failed)
@ -205,24 +212,16 @@ scrobble_cb(void *arg)
static void
oauth_interface(struct evhttp_request *req, const char *uri)
{
struct evbuffer *evbuf;
struct evkeyvalq query;
const char *req_uri;
const char *ptr;
char __attribute__((unused)) redirect_uri[256];
char *errmsg;
int ret;
#ifdef HAVE_SPOTIFY_H
req_uri = evhttp_request_get_uri(req);
evbuf = evbuffer_new();
if (!evbuf)
{
DPRINTF(E_LOG, L_HTTPD, "Could not alloc evbuf for oauth\n");
return;
}
evbuffer_add_printf(evbuf, "<H1>forked-daapd oauth</H1>\n\n");
memset(&query, 0, sizeof(struct evkeyvalq));
ptr = strchr(req_uri, '?');
@ -231,32 +230,38 @@ oauth_interface(struct evhttp_request *req, const char *uri)
ret = evhttp_parse_query_str(ptr + 1, &query);
if (ret < 0)
{
evbuffer_add_printf(evbuf, "OAuth error: Could not parse parameters in callback (%s)\n", req_uri);
httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0);
evbuffer_free(evbuf);
DPRINTF(E_LOG, L_HTTPD, "OAuth error: Could not parse parameters in callback (%s)\n", req_uri);
httpd_send_error(req, HTTP_BADREQUEST, "Could not parse parameters in callback");
return;
}
}
#ifdef HAVE_SPOTIFY_H
snprintf(redirect_uri, sizeof(redirect_uri), "http://forked-daapd.local:%d/oauth/spotify", httpd_port);
if (strncmp(uri, "/oauth/spotify", strlen("/oauth/spotify")) == 0)
spotify_oauth_callback(evbuf, &query, redirect_uri);
{
snprintf(redirect_uri, sizeof(redirect_uri), "http://forked-daapd.local:%d/oauth/spotify", httpd_port);
ret = spotify_oauth_callback(&query, redirect_uri, &errmsg);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "OAuth error: Could not parse parameters in callback (%s)\n", req_uri);
httpd_send_error(req, HTTP_INTERNAL, errmsg);
}
else
{
redirect_to_admin(req);
}
evhttp_clear_headers(&query);
free(errmsg);
}
else
spotify_oauth_interface(evbuf, redirect_uri);
{
httpd_send_error(req, HTTP_NOTFOUND, NULL);
}
#else
evbuffer_add_printf(evbuf, "<p>This version was built without modules requiring OAuth support</p>\n");
DPRINTF(E_LOG, L_HTTPD, "This version was built without modules requiring OAuth support\n");
httpd_send_error(req, HTTP_NOTFOUND, "No modules with OAuth support");
#endif
evbuffer_add_printf(evbuf, "<p><i>(sorry about this ugly interface)</i></p>\n");
evhttp_clear_headers(&query);
httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0);
evbuffer_free(evbuf);
}
static void
@ -901,7 +906,19 @@ httpd_send_error(struct evhttp_request* req, int error, const char* reason)
static int
path_is_legal(char *path)
{
return strncmp(WEBFACE_ROOT, path, strlen(WEBFACE_ROOT));
return strncmp(WEB_ROOT, path, strlen(WEB_ROOT));
}
/* Thread: httpd */
static void
redirect_to_admin(struct evhttp_request *req)
{
struct evkeyvalq *headers;
headers = evhttp_request_get_output_headers(req);
evhttp_add_header(headers, "Location", "/admin.html");
httpd_send_reply(req, HTTP_MOVETEMP, "Moved", NULL, HTTPD_SEND_NO_GZIP);
}
/* Thread: httpd */
@ -930,24 +947,13 @@ redirect_to_index(struct evhttp_request *req, char *uri)
httpd_send_reply(req, HTTP_MOVETEMP, "Moved", NULL, HTTPD_SEND_NO_GZIP);
}
/* Thread: httpd */
static void
serve_file(struct evhttp_request *req, char *uri)
bool
httpd_admin_check_auth(struct evhttp_request *req)
{
const char *host;
const char *passwd;
char *ext;
char path[PATH_MAX];
char *deref;
char *ctype;
struct evbuffer *evbuf;
struct evkeyvalq *headers;
struct stat sb;
int fd;
int i;
int ret;
/* Check authentication */
passwd = cfg_getstr(cfg_getsec(cfg, "general"), "admin_password");
if (passwd)
{
@ -955,7 +961,7 @@ serve_file(struct evhttp_request *req, char *uri)
ret = httpd_basic_auth(req, "admin", passwd, PACKAGE " web interface");
if (ret != 0)
return;
return false;
DPRINTF(E_DBG, L_HTTPD, "Authentication successful\n");
}
@ -968,17 +974,47 @@ serve_file(struct evhttp_request *req, char *uri)
DPRINTF(E_LOG, L_HTTPD, "Remote web interface request denied; no password set\n");
httpd_send_error(req, 403, "Forbidden");
return;
return false;
}
}
return true;
}
/* Thread: httpd */
static void
serve_file(struct evhttp_request *req, char *uri)
{
char *ext;
char path[PATH_MAX];
char *deref;
char *ctype;
struct evbuffer *evbuf;
struct evkeyvalq *input_headers;
struct evkeyvalq *output_headers;
struct stat sb;
int fd;
int i;
uint8_t buf[4096];
const char *modified_since;
char last_modified[1000];
struct tm *tm_modified;
int ret;
/* Check authentication */
if (!httpd_admin_check_auth(req))
{
DPRINTF(E_DBG, L_HTTPD, "Remote web interface request denied;\n");
return;
}
if (strncmp(uri, "/oauth", strlen("/oauth")) == 0)
{
oauth_interface(req, uri);
return;
}
ret = snprintf(path, sizeof(path), "%s%s", WEBFACE_ROOT, uri + 1); /* skip starting '/' */
ret = snprintf(path, sizeof(path), "%s%s", WEB_ROOT, uri);
if ((ret < 0) || (ret >= sizeof(path)))
{
DPRINTF(E_LOG, L_HTTPD, "Request exceeds PATH_MAX: %s\n", uri);
@ -1054,6 +1090,18 @@ serve_file(struct evhttp_request *req, char *uri)
return;
}
tm_modified = gmtime(&sb.st_mtime);
strftime(last_modified, sizeof(last_modified), "%a, %d %b %Y %H:%M:%S %Z", tm_modified);
input_headers = evhttp_request_get_input_headers(req);
modified_since = evhttp_find_header(input_headers, "If-Modified-Since");
if (modified_since && strcasecmp(last_modified, modified_since) == 0)
{
httpd_send_reply(req, HTTP_NOTMODIFIED, NULL, NULL, HTTPD_SEND_NO_GZIP);
return;
}
evbuf = evbuffer_new();
if (!evbuf)
{
@ -1069,20 +1117,24 @@ serve_file(struct evhttp_request *req, char *uri)
DPRINTF(E_LOG, L_HTTPD, "Could not open %s: %s\n", path, strerror(errno));
httpd_send_error(req, HTTP_NOTFOUND, "Not Found");
evbuffer_free(evbuf);
return;
}
/* FIXME: this is broken, if we ever need to serve files here,
* this must be fixed.
*/
ret = evbuffer_read(evbuf, fd, sb.st_size);
close(fd);
ret = evbuffer_expand(evbuf, sb.st_size);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Out of memory for htdocs-file\n");
goto out_fail;
}
while ((ret = read(fd, buf, sizeof(buf))) > 0)
evbuffer_add(evbuf, buf, ret);
if (ret < 0)
{
DPRINTF(E_LOG, L_HTTPD, "Could not read file into evbuffer\n");
httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal error");
return;
goto out_fail;
}
ctype = "application/octet-stream";
@ -1099,12 +1151,23 @@ serve_file(struct evhttp_request *req, char *uri)
}
}
headers = evhttp_request_get_output_headers(req);
evhttp_add_header(headers, "Content-Type", ctype);
output_headers = evhttp_request_get_output_headers(req);
evhttp_add_header(output_headers, "Content-Type", ctype);
// Allow browsers to cache the file
evhttp_add_header(output_headers, "Cache-Control", "private");
evhttp_add_header(output_headers, "Last-Modified", last_modified);
httpd_send_reply(req, HTTP_OK, "OK", evbuf, HTTPD_SEND_NO_GZIP);
evbuffer_free(evbuf);
close(fd);
return;
out_fail:
httpd_send_error(req, HTTP_SERVUNAVAIL, "Internal error");
evbuffer_free(evbuf);
close(fd);
}
/* Thread: httpd */
@ -1138,9 +1201,9 @@ httpd_gen_cb(struct evhttp_request *req, void *arg)
}
req_uri = evhttp_request_get_uri(req);
if (!req_uri)
if (!req_uri || strcmp(req_uri, "/") == 0)
{
redirect_to_index(req, "/");
redirect_to_admin(req);
return;
}
@ -1175,6 +1238,12 @@ httpd_gen_cb(struct evhttp_request *req, void *arg)
{
dacp_request(req);
goto out;
}
else if (jsonapi_is_request(req, uri))
{
jsonapi_request(req);
goto out;
}
else if (streaming_is_request(req, uri))
@ -1449,6 +1518,24 @@ httpd_init(void)
goto dacp_fail;
}
ret = jsonapi_init();
if (ret < 0)
{
DPRINTF(E_FATAL, L_HTTPD, "JSON api init failed\n");
goto jsonapi_fail;
}
#ifdef WEBSOCKET
ret = websocket_init();
if (ret < 0)
{
DPRINTF(E_FATAL, L_HTTPD, "Websocket init failed\n");
goto websocket_fail;
}
#endif
streaming_init();
#ifdef HAVE_EVENTFD
@ -1560,6 +1647,12 @@ httpd_init(void)
#endif
pipe_fail:
streaming_deinit();
#ifdef WEBSOCKET
websocket_deinit();
#endif
websocket_fail:
jsonapi_deinit();
jsonapi_fail:
dacp_deinit();
dacp_fail:
daap_deinit();
@ -1606,6 +1699,10 @@ httpd_deinit(void)
}
streaming_deinit();
#ifdef WEBSOCKET
websocket_deinit();
#endif
jsonapi_deinit();
rsp_deinit();
dacp_deinit();
daap_deinit();

View File

@ -4,6 +4,7 @@
#include <event2/http.h>
#include <event2/buffer.h>
#include <stdbool.h>
enum httpd_send_flags
{
@ -58,6 +59,9 @@ httpd_fixup_uri(struct evhttp_request *req);
int
httpd_basic_auth(struct evhttp_request *req, const char *user, const char *passwd, const char *realm);
bool
httpd_admin_check_auth(struct evhttp_request *req);
int
httpd_init(void);

577
src/httpd_jsonapi.c Normal file
View File

@ -0,0 +1,577 @@
/*
* Copyright (C) 2017 Christian Meffert <christian.meffert@googlemail.com>
*
* Adapted from httpd_adm.c:
* Copyright (C) 2015 Stuart NAIFEH <stu@naifeh.org>
*
* Adapted from httpd_daap.c and httpd.c:
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
* Copyright (C) 2010 Kai Elwert <elwertk@googlemail.com>
*
* Adapted from mt-daapd:
* Copyright (C) 2003-2007 Ron Pedde <ron@pedde.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 "httpd_jsonapi.h"
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <event2/event.h>
#include <event2/buffer.h>
#include <event2/keyvalq_struct.h>
#include <json.h>
#include <regex.h>
#include <string.h>
#include "conffile.h"
#include "db.h"
#include "httpd.h"
#include "library.h"
#include "logger.h"
#include "misc_json.h"
#include "remote_pairing.h"
#ifdef HAVE_SPOTIFY_H
# include "spotify_webapi.h"
# include "spotify.h"
#endif
struct uri_map
{
regex_t preg;
char *regexp;
int (*handler)(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query);
};
/*
* Endpoint to retrieve configuration values
*
* Example response:
*
* {
* "websocket_port": 6603,
* "version": "25.0"
* }
*/
static int
jsonapi_reply_config(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
{
json_object *reply;
json_object *buildopts;
int websocket_port;
char **buildoptions;
int i;
int ret;
reply = json_object_new_object();
// Websocket port
#ifdef WEBSOCKET
websocket_port = cfg_getint(cfg_getsec(cfg, "general"), "websocket_port");
#else
websocket_port = 0;
#endif
json_object_object_add(reply, "websocket_port", json_object_new_int(websocket_port));
// forked-daapd version
json_object_object_add(reply, "version", json_object_new_string(VERSION));
// enabled build options
buildopts = json_object_new_array();
buildoptions = buildopts_get();
for (i = 0; buildoptions[i]; i++)
{
json_object_array_add(buildopts, json_object_new_string(buildoptions[i]));
}
json_object_object_add(reply, "buildoptions", buildopts);
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
jparse_free(reply);
if (ret < 0)
{
DPRINTF(E_LOG, L_WEB, "config: Couldn't add config data to response buffer.\n");
return -1;
}
return 0;
}
/*
* Endpoint to retrieve informations about the library
*
* Example response:
*
* {
* "artists": 84,
* "albums": 151,
* "songs": 3085,
* "db_playtime": 687824,
* "updating": false
*}
*/
static int
jsonapi_reply_library(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
{
struct query_params qp;
struct filecount_info fci;
int artists;
int albums;
bool is_scanning;
json_object *reply;
int ret;
// Fetch values for response
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_COUNT_ITEMS;
ret = db_filecount_get(&fci, &qp);
if (ret < 0)
{
DPRINTF(E_LOG, L_WEB, "library: failed to get file count info\n");
return -1;
}
artists = db_files_get_artist_count();
albums = db_files_get_album_count();
is_scanning = library_is_scanning();
// Build json response
reply = json_object_new_object();
json_object_object_add(reply, "artists", json_object_new_int(artists));
json_object_object_add(reply, "albums", json_object_new_int(albums));
json_object_object_add(reply, "songs", json_object_new_int(fci.count));
json_object_object_add(reply, "db_playtime", json_object_new_int64((fci.length / 1000)));
json_object_object_add(reply, "updating", json_object_new_boolean(is_scanning));
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
jparse_free(reply);
if (ret < 0)
{
DPRINTF(E_LOG, L_WEB, "library: Couldn't add library information data to response buffer.\n");
return -1;
}
return 0;
}
/*
* Endpoint to trigger a library rescan
*/
static int
jsonapi_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
{
library_rescan();
return 0;
}
/*
* Endpoint to retrieve information about the spotify integration
*
* Exampe response:
*
* {
* "enabled": true,
* "oauth_uri": "https://accounts.spotify.com/authorize/?client_id=...
* }
*/
static int
jsonapi_reply_spotify(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
{
int httpd_port;
char __attribute__((unused)) redirect_uri[256];
char *oauth_uri;
json_object *reply;
int ret;
reply = json_object_new_object();
#ifdef HAVE_SPOTIFY_H
struct spotify_status_info info;
json_object_object_add(reply, "enabled", json_object_new_boolean(true));
httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port");
snprintf(redirect_uri, sizeof(redirect_uri), "http://forked-daapd.local:%d/oauth/spotify", httpd_port);
oauth_uri = spotifywebapi_oauth_uri_get(redirect_uri);
if (!uri)
{
DPRINTF(E_LOG, L_WEB, "Cannot display Spotify oauth interface (http_form_uriencode() failed)\n");
}
else
{
json_object_object_add(reply, "oauth_uri", json_object_new_string(oauth_uri));
free(oauth_uri);
}
spotify_status_info_get(&info);
json_object_object_add(reply, "libspotify_installed", json_object_new_boolean(info.libspotify_installed));
json_object_object_add(reply, "libspotify_logged_in", json_object_new_boolean(info.libspotify_logged_in));
json_object_object_add(reply, "libspotify_user", json_object_new_string(info.libspotify_user));
json_object_object_add(reply, "webapi_token_valid", json_object_new_boolean(info.webapi_token_valid));
json_object_object_add(reply, "webapi_user", json_object_new_string(info.webapi_user));
#else
json_object_object_add(reply, "enabled", json_object_new_boolean(false));
#endif
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
jparse_free(reply);
if (ret < 0)
{
DPRINTF(E_LOG, L_WEB, "spotify: Couldn't add spotify information data to response buffer.\n");
return -1;
}
return 0;
}
static int
jsonapi_reply_spotify_login(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
{
struct evbuffer *in_evbuf;
json_object* request;
const char *user;
const char *password;
char *errmsg = NULL;
json_object* reply;
json_object* errors;
int ret;
DPRINTF(E_DBG, L_WEB, "Received spotify login request\n");
#ifdef HAVE_SPOTIFY_H
in_evbuf = evhttp_request_get_input_buffer(req);
request = jparse_obj_from_evbuffer(in_evbuf);
if (!request)
{
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n");
return -1;
}
reply = json_object_new_object();
user = jparse_str_from_obj(request, "user");
password = jparse_str_from_obj(request, "password");
if (user && strlen(user) > 0 && password && strlen(password) > 0)
{
ret = spotify_login_user(user, password, &errmsg);
if (ret < 0)
{
json_object_object_add(reply, "success", json_object_new_boolean(false));
errors = json_object_new_object();
json_object_object_add(errors, "error", json_object_new_string(errmsg));
json_object_object_add(reply, "errors", errors);
}
else
{
json_object_object_add(reply, "success", json_object_new_boolean(true));
}
free(errmsg);
}
else
{
DPRINTF(E_LOG, L_WEB, "No user or password in spotify login post request\n");
json_object_object_add(reply, "success", json_object_new_boolean(false));
errors = json_object_new_object();
if (!user || strlen(user) == 0)
json_object_object_add(errors, "user", json_object_new_string("Username is required"));
if (!password || strlen(password) == 0)
json_object_object_add(errors, "password", json_object_new_string("Password is required"));
json_object_object_add(reply, "errors", errors);
}
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
jparse_free(reply);
if (ret < 0)
{
DPRINTF(E_LOG, L_WEB, "spotify: Couldn't add spotify login data to response buffer.\n");
return -1;
}
#else
DPRINTF(E_LOG, L_WEB, "Received spotify login request but was not compiled with enable-spotify\n");
#endif
return 0;
}
/*
* Endpoint to kickoff pairing of a daap/dacp client
*
* Expects the paring pin to be present in the post request body, e. g.:
*
* {
* "pin": "1234"
* }
*/
static int
pairing_kickoff(struct evhttp_request* req)
{
struct evbuffer *evbuf;
json_object* request;
const char* message;
evbuf = evhttp_request_get_input_buffer(req);
request = jparse_obj_from_evbuffer(evbuf);
if (!request)
{
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n");
return -1;
}
DPRINTF(E_DBG, L_WEB, "Received pairing post request: %s\n", json_object_to_json_string(request));
message = jparse_str_from_obj(request, "pin");
if (message)
remote_pairing_kickoff((char **)&message);
else
DPRINTF(E_LOG, L_WEB, "Missing pin in request body: %s\n", json_object_to_json_string(request));
jparse_free(request);
return 0;
}
/*
* Endpoint to retrieve pairing information
*
* Example response:
*
* {
* "active": true,
* "remote": "remote name"
* }
*/
static int
pairing_get(struct evbuffer *evbuf)
{
char *remote_name;
json_object *reply;
int ret;
remote_name = remote_pairing_get_name();
reply = json_object_new_object();
if (remote_name)
{
json_object_object_add(reply, "active", json_object_new_boolean(true));
json_object_object_add(reply, "remote", json_object_new_string(remote_name));
}
else
{
json_object_object_add(reply, "active", json_object_new_boolean(false));
}
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
jparse_free(reply);
free(remote_name);
if (ret < 0)
{
DPRINTF(E_LOG, L_WEB, "pairing: Couldn't add pairing information data to response buffer.\n");
return -1;
}
return 0;
}
/*
* Endpoint to pair daap/dacp client
*
* If request is a GET request, returns information about active pairing remote.
* If request is a POST request, tries to pair the active remote with the given pin.
*/
static int
jsonapi_reply_pairing(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
{
if (evhttp_request_get_command(req) == EVHTTP_REQ_POST)
{
return pairing_kickoff(req);
}
return pairing_get(evbuf);
}
static struct uri_map adm_handlers[] =
{
{ .regexp = "^/api/config", .handler = jsonapi_reply_config },
{ .regexp = "^/api/library", .handler = jsonapi_reply_library },
{ .regexp = "^/api/update", .handler = jsonapi_reply_update },
{ .regexp = "^/api/spotify-login", .handler = jsonapi_reply_spotify_login },
{ .regexp = "^/api/spotify", .handler = jsonapi_reply_spotify },
{ .regexp = "^/api/pairing", .handler = jsonapi_reply_pairing },
{ .regexp = NULL, .handler = NULL }
};
void
jsonapi_request(struct evhttp_request *req)
{
char *full_uri;
char *uri;
char *ptr;
struct evbuffer *evbuf;
struct evkeyvalq query;
struct evkeyvalq *headers;
int handler;
int ret;
int i;
/* Check authentication */
if (!httpd_admin_check_auth(req))
{
DPRINTF(E_DBG, L_WEB, "JSON api request denied;\n");
return;
}
memset(&query, 0, sizeof(struct evkeyvalq));
full_uri = httpd_fixup_uri(req);
if (!full_uri)
{
evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
return;
}
ptr = strchr(full_uri, '?');
if (ptr)
*ptr = '\0';
uri = strdup(full_uri);
if (!uri)
{
free(full_uri);
evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
return;
}
if (ptr)
*ptr = '?';
ptr = uri;
uri = evhttp_decode_uri(uri);
free(ptr);
DPRINTF(E_DBG, L_WEB, "Web admin request: %s\n", full_uri);
handler = -1;
for (i = 0; adm_handlers[i].handler; i++)
{
ret = regexec(&adm_handlers[i].preg, uri, 0, NULL, 0);
if (ret == 0)
{
handler = i;
break;
}
}
if (handler < 0)
{
DPRINTF(E_LOG, L_WEB, "Unrecognized web admin request\n");
evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
free(uri);
free(full_uri);
return;
}
evbuf = evbuffer_new();
if (!evbuf)
{
DPRINTF(E_LOG, L_WEB, "Could not allocate evbuffer for Web Admin reply\n");
evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error");
free(uri);
free(full_uri);
return;
}
evhttp_parse_query(full_uri, &query);
headers = evhttp_request_get_output_headers(req);
evhttp_add_header(headers, "DAAP-Server", "forked-daapd/" VERSION);
ret = adm_handlers[handler].handler(req, evbuf, uri, &query);
if (ret < 0)
{
evhttp_send_error(req, 500, "Internal Server Error");
}
else
{
headers = evhttp_request_get_output_headers(req);
evhttp_add_header(headers, "Content-Type", "application/json");
httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0);
}
evbuffer_free(evbuf);
evhttp_clear_headers(&query);
free(uri);
free(full_uri);
}
int
jsonapi_is_request(struct evhttp_request *req, char *uri)
{
if (strncmp(uri, "/api/", strlen("/api/")) == 0)
return 1;
if (strcmp(uri, "/api") == 0)
return 1;
return 0;
}
int
jsonapi_init(void)
{
char buf[64];
int i;
int ret;
for (i = 0; adm_handlers[i].handler; i++)
{
ret = regcomp(&adm_handlers[i].preg, adm_handlers[i].regexp, REG_EXTENDED | REG_NOSUB);
if (ret != 0)
{
regerror(ret, &adm_handlers[i].preg, buf, sizeof(buf));
DPRINTF(E_FATAL, L_WEB, "Admin web interface init failed; regexp error: %s\n", buf);
return -1;
}
}
return 0;
}
void
jsonapi_deinit(void)
{
int i;
for (i = 0; adm_handlers[i].handler; i++)
regfree(&adm_handlers[i].preg);
}

19
src/httpd_jsonapi.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef __HTTPD_JSONAPI_H__
#define __HTTPD_JSONAPI_H__
#include <event2/http.h>
int
jsonapi_init(void);
void
jsonapi_deinit(void);
void
jsonapi_request(struct evhttp_request *req);
int
jsonapi_is_request(struct evhttp_request *req, char *uri);
#endif /* !__HTTPD_JSONAPI_H__ */

View File

@ -549,7 +549,7 @@ rescan(void *arg, int *ret)
int i;
DPRINTF(E_LOG, L_LIB, "Library rescan triggered\n");
listener_notify(LISTENER_UPDATE);
starttime = time(NULL);
for (i = 0; sources[i]; i++)
@ -569,8 +569,9 @@ rescan(void *arg, int *ret)
endtime = time(NULL);
DPRINTF(E_LOG, L_LIB, "Library rescan completed in %.f sec\n", difftime(endtime, starttime));
scanning = false;
listener_notify(LISTENER_UPDATE);
*ret = 0;
return COMMAND_END;
}
@ -583,7 +584,7 @@ fullrescan(void *arg, int *ret)
int i;
DPRINTF(E_LOG, L_LIB, "Library full-rescan triggered\n");
listener_notify(LISTENER_UPDATE);
starttime = time(NULL);
player_playback_stop();
@ -605,8 +606,9 @@ fullrescan(void *arg, int *ret)
endtime = time(NULL);
DPRINTF(E_LOG, L_LIB, "Library full-rescan completed in %.f sec\n", difftime(endtime, starttime));
scanning = false;
listener_notify(LISTENER_UPDATE);
*ret = 0;
return COMMAND_END;
}
@ -665,6 +667,7 @@ initscan()
scanning = true;
starttime = time(NULL);
listener_notify(LISTENER_UPDATE);
// Only clear the queue if enabled (default) in config
clear_queue_disabled = cfg_getbool(cfg_getsec(cfg, "mpd"), "clear_queue_on_stop_disable");
@ -691,7 +694,7 @@ initscan()
DPRINTF(E_LOG, L_LIB, "Library init scan completed in %.f sec\n", difftime(endtime, starttime));
scanning = false;
listener_notify(LISTENER_UPDATE);
listener_notify(LISTENER_DATABASE);
}

View File

@ -18,6 +18,12 @@ enum listener_event_type
LISTENER_DATABASE = (1 << 5),
/* A stored playlist has been modified (create, delete, add, rename) */
LISTENER_STORED_PLAYLIST = (1 << 6),
/* A library update has started or finished */
LISTENER_UPDATE = (1 << 7),
/* A pairing request has started or finished */
LISTENER_PAIRING = (1 << 8),
/* Spotify status changes (login, logout) */
LISTENER_SPOTIFY = (1 << 9),
};
typedef void (*notify)(enum listener_event_type type);

View File

@ -45,7 +45,7 @@ static int threshold;
static int console = 1;
static char *logfilename;
static FILE *logfile;
static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd", "stream", "cast", "fifo", "lib" };
static char *labels[] = { "config", "daap", "db", "httpd", "http", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd", "stream", "cast", "fifo", "lib", "web" };
static char *severities[] = { "FATAL", "LOG", "WARN", "INFO", "DEBUG", "SPAM" };
/* We need our own check to avoid nested locking or recursive calls */

View File

@ -35,8 +35,9 @@
#define L_CAST 26
#define L_FIFO 27
#define L_LIB 28
#define L_WEB 29
#define N_LOGDOMAINS 29
#define N_LOGDOMAINS 30
/* Severities */
#define E_FATAL 0

View File

@ -465,13 +465,14 @@ main(int argc, char **argv)
char *logfile;
char *ffid;
char *pidfile;
char buildopts[256];
char **buildopts;
const char *gcry_version;
sigset_t sigs;
int sigfd;
#ifdef HAVE_KQUEUE
struct kevent ke_sigs[4];
#endif
int i;
int ret;
struct option option_map[] =
@ -591,34 +592,12 @@ main(int argc, char **argv)
DPRINTF(E_LOG, L_MAIN, "Forked Media Server Version %s taking off\n", VERSION);
/* Remember to check the size of buildopts when adding new opts */
strcpy(buildopts, "");
#ifdef ITUNES
strcat(buildopts, " --enable-itunes");
#endif
#ifdef SPOTIFY
strcat(buildopts, " --enable-spotify");
#endif
#ifdef LASTFM
strcat(buildopts, " --enable-lastfm");
#endif
#ifdef CHROMECAST
strcat(buildopts, " --enable-chromecast");
#endif
#ifdef MPD
strcat(buildopts, " --enable-mpd");
#endif
#ifdef RAOP_VERIFICATION
strcat(buildopts, " --enable-verification");
#endif
#ifdef HAVE_ALSA
strcat(buildopts, " --with-alsa");
#endif
#ifdef HAVE_LIBPULSE
strcat(buildopts, " --with-pulseaudio");
#endif
DPRINTF(E_LOG, L_MAIN, "Built %s with:%s\n", __DATE__, buildopts);
DPRINTF(E_LOG, L_MAIN, "Built %s with:\n", __DATE__);
buildopts = buildopts_get();
for (i = 0; buildopts[i]; i++)
{
DPRINTF(E_LOG, L_MAIN, "- %s\n", buildopts[i]);
}
ret = av_lockmgr_register(ffmpeg_lockmgr);
if (ret < 0)

View File

@ -46,6 +46,44 @@
#include "misc.h"
static char *buildopts[] =
{
#ifdef ITUNES
"iTunes XML",
#endif
#ifdef SPOTIFY
"Spotify",
#endif
#ifdef LASTFM
"LastFM",
#endif
#ifdef CHROMECAST
"Chromecast",
#endif
#ifdef MPD
"MPD",
#endif
#ifdef RAOP_VERIFICATION
"Device verification",
#endif
#ifdef WEBSOCKET
"Websocket",
#endif
#ifdef HAVE_ALSA
"ALSA",
#endif
#ifdef HAVE_LIBPULSE
"Pulseaudio",
#endif
NULL
};
char **
buildopts_get()
{
return buildopts;
}
int
safe_atoi32(const char *str, int32_t *val)
{

View File

@ -29,6 +29,9 @@ struct keyval {
};
char **
buildopts_get(void);
int
safe_atoi32(const char *str, int32_t *val);

145
src/misc_json.c Normal file
View File

@ -0,0 +1,145 @@
/*
* Copyright (C) 2017 Christian Meffert <christian.meffert@googlemail.com>
*
* Some code included below is in the public domain, check comments
* in the file.
*
* Pieces of code adapted from mt-daapd:
* Copyright (C) 2003-2007 Ron Pedde (ron@pedde.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 <event2/buffer.h>
#include <event2/event.h>
#include <json.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <time.h>
#include "logger.h"
void
jparse_free(json_object* haystack)
{
if (haystack)
{
#ifdef HAVE_JSON_C_OLD
json_object_put(haystack);
#else
if (json_object_put(haystack) != 1)
DPRINTF(E_LOG, L_MISC, "Memleak: JSON parser did not free object\n");
#endif
}
}
int
jparse_array_from_obj(json_object *haystack, const char *key, json_object **needle)
{
if (! (json_object_object_get_ex(haystack, key, needle) && json_object_get_type(*needle) == json_type_array) )
return -1;
else
return 0;
}
const char *
jparse_str_from_obj(json_object *haystack, const char *key)
{
json_object *needle;
if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_string)
return json_object_get_string(needle);
else
return NULL;
}
int
jparse_int_from_obj(json_object *haystack, const char *key)
{
json_object *needle;
if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_int)
return json_object_get_int(needle);
else
return 0;
}
int
jparse_bool_from_obj(json_object *haystack, const char *key)
{
json_object *needle;
if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_boolean)
return json_object_get_boolean(needle);
else
return false;
}
time_t
jparse_time_from_obj(json_object *haystack, const char *key)
{
const char *tmp;
struct tm tp;
time_t parsed_time;
memset(&tp, 0, sizeof(struct tm));
tmp = jparse_str_from_obj(haystack, key);
if (!tmp)
return 0;
strptime(tmp, "%Y-%m-%dT%H:%M:%SZ", &tp);
parsed_time = mktime(&tp);
if (parsed_time < 0)
return 0;
return parsed_time;
}
const char *
jparse_str_from_array(json_object *array, int index, const char *key)
{
json_object *item;
int count;
if (json_object_get_type(array) != json_type_array)
return NULL;
count = json_object_array_length(array);
if (count <= 0 || count <= index)
return NULL;
item = json_object_array_get_idx(array, index);
return jparse_str_from_obj(item, key);
}
json_object *
jparse_obj_from_evbuffer(struct evbuffer *evbuf)
{
char *json_str;
// 0-terminate for safety
evbuffer_add(evbuf, "", 1);
json_str = (char *) evbuffer_pullup(evbuf, -1);
if (!json_str || (strlen(json_str) == 0))
{
DPRINTF(E_LOG, L_MISC, "Failed to parse JSON from input buffer\n");
return NULL;
}
return json_tokener_parse(json_str);
}

60
src/misc_json.h Normal file
View File

@ -0,0 +1,60 @@
/*
* Copyright (C) 2017 Christian Meffert <christian.meffert@googlemail.com>
*
* Some code included below is in the public domain, check comments
* in the file.
*
* Pieces of code adapted from mt-daapd:
* Copyright (C) 2003-2007 Ron Pedde (ron@pedde.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
*/
#ifndef SRC_MISC_JSON_H_
#define SRC_MISC_JSON_H_
#include <event2/event.h>
#include <json.h>
#include <stdbool.h>
#include <stddef.h>
#include <time.h>
void
jparse_free(json_object *haystack);
int
jparse_array_from_obj(json_object *haystack, const char *key, json_object **needle);
const char *
jparse_str_from_obj(json_object *haystack, const char *key);
int
jparse_int_from_obj(json_object *haystack, const char *key);
int
jparse_bool_from_obj(json_object *haystack, const char *key);
time_t
jparse_time_from_obj(json_object *haystack, const char *key);
const char *
jparse_str_from_array(json_object *array, int index, const char *key);
json_object *
jparse_obj_from_evbuffer(struct evbuffer *evbuf);
#endif /* SRC_MISC_JSON_H_ */

View File

@ -608,6 +608,10 @@ mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
{
client->events |= LISTENER_DATABASE;
}
else if (0 == strcmp(argv[i], "update"))
{
client->events |= LISTENER_UPDATE;
}
else if (0 == strcmp(argv[i], "player"))
{
client->events |= LISTENER_PLAYER;
@ -639,7 +643,7 @@ mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
}
}
else
client->events = LISTENER_PLAYER | LISTENER_QUEUE | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS;
client->events = LISTENER_PLAYER | LISTENER_QUEUE | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS | LISTENER_DATABASE | LISTENER_UPDATE | LISTENER_STORED_PLAYLIST;
idle_clients = client;
@ -808,26 +812,13 @@ mpd_command_stats(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_COUNT_ITEMS;
ret = db_query_start(&qp);
ret = db_filecount_get(&fci, &qp);
if (ret < 0)
{
db_query_end(&qp);
*errmsg = safe_asprintf("Could not start query");
return ACK_ERROR_UNKNOWN;
}
ret = db_query_fetch_count(&qp, &fci);
if (ret < 0)
{
db_query_end(&qp);
*errmsg = safe_asprintf("Could not fetch query count");
return ACK_ERROR_UNKNOWN;
}
db_query_end(&qp);
artists = db_files_get_artist_count();
albums = db_files_get_album_count();
@ -2458,31 +2449,18 @@ mpd_command_count(struct evbuffer *evbuf, int argc, char **argv, char **errmsg)
}
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_COUNT_ITEMS;
mpd_get_query_params_find(argc - 1, argv + 1, &qp);
ret = db_query_start(&qp);
ret = db_filecount_get(&fci, &qp);
if (ret < 0)
{
db_query_end(&qp);
sqlite3_free(qp.filter);
*errmsg = safe_asprintf("Could not start query");
return ACK_ERROR_UNKNOWN;
}
ret = db_query_fetch_count(&qp, &fci);
if (ret < 0)
{
db_query_end(&qp);
sqlite3_free(qp.filter);
*errmsg = safe_asprintf("Could not fetch query count");
return ACK_ERROR_UNKNOWN;
}
evbuffer_add_printf(evbuf,
"songs: %d\n"
"playtime: %" PRIu64 "\n",

View File

@ -56,6 +56,7 @@
#include "misc.h"
#include "db.h"
#include "remote_pairing.h"
#include "listener.h"
struct remote_info {
@ -70,8 +71,6 @@ struct remote_info {
char *v6_address;
struct evhttp_connection *evcon;
struct remote_info *next;
};
@ -613,6 +612,7 @@ pairing_cb(int fd, short event, void *arg)
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&remote_lck));
listener_notify(LISTENER_PAIRING);
event_add(pairingev, NULL);
}
@ -710,6 +710,8 @@ touch_remote_cb(const char *name, const char *type, const char *domain, const ch
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&remote_lck));
}
listener_notify(LISTENER_PAIRING);
}
/* Thread: filescanner, mpd */
@ -736,6 +738,31 @@ remote_pairing_kickoff(char **arglist)
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&remote_lck));
}
/*
* Returns the remote name of the current active pairing request as an allocated string (needs to be freed by the caller)
* or NULL in case there is no active pairing request.
*
* Thread: httpd
*/
char *
remote_pairing_get_name(void)
{
char *remote_name;
DPRINTF(E_DBG, L_REMOTE, "Get pairing remote name\n");
CHECK_ERR(L_REMOTE, pthread_mutex_lock(&remote_lck));
if (remote_info)
remote_name = strdup(remote_info->pi.name);
else
remote_name = NULL;
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&remote_lck));
return remote_name;
}
/* Thread: main */
int

View File

@ -5,6 +5,9 @@
void
remote_pairing_kickoff(char **arglist);
char *
remote_pairing_get_name(void);
int
remote_pairing_init(void);

View File

@ -55,6 +55,7 @@
#include "commands.h"
#include "library.h"
#include "input.h"
#include "listener.h"
/* TODO for the web api:
* - UI should be prettier
@ -140,6 +141,9 @@ static int spotify_saved_plid;
// Flag to avoid triggering playlist change events while the (re)scan is running
static bool scanning;
static pthread_mutex_t status_lck;
static struct spotify_status_info spotify_status_info;
// Timeout timespec
static struct timespec spotify_artwork_timeout = { SPOTIFY_ARTWORK_TIMEOUT, 0 };
@ -194,6 +198,7 @@ typedef sp_error (*fptr_sp_session_player_play_t)(sp_session *session, bool
typedef sp_error (*fptr_sp_session_player_seek_t)(sp_session *session, int offset);
typedef sp_connectionstate (*fptr_sp_session_connectionstate_t)(sp_session *session);
typedef sp_error (*fptr_sp_session_preferred_bitrate_t)(sp_session *session, sp_bitrate bitrate);
typedef const char* (*fptr_sp_session_user_name_t)(sp_session *session);
typedef sp_error (*fptr_sp_playlistcontainer_add_callbacks_t)(sp_playlistcontainer *pc, sp_playlistcontainer_callbacks *callbacks, void *userdata);
typedef int (*fptr_sp_playlistcontainer_num_playlists_t)(sp_playlistcontainer *pc);
@ -262,6 +267,7 @@ fptr_sp_session_player_play_t fptr_sp_session_player_play;
fptr_sp_session_player_seek_t fptr_sp_session_player_seek;
fptr_sp_session_connectionstate_t fptr_sp_session_connectionstate;
fptr_sp_session_preferred_bitrate_t fptr_sp_session_preferred_bitrate;
fptr_sp_session_user_name_t fptr_sp_session_user_name;
fptr_sp_playlistcontainer_add_callbacks_t fptr_sp_playlistcontainer_add_callbacks;
fptr_sp_playlistcontainer_num_playlists_t fptr_sp_playlistcontainer_num_playlists;
@ -338,6 +344,7 @@ fptr_assign_all()
&& (fptr_sp_session_player_seek = dlsym(h, "sp_session_player_seek"))
&& (fptr_sp_session_connectionstate = dlsym(h, "sp_session_connectionstate"))
&& (fptr_sp_session_preferred_bitrate = dlsym(h, "sp_session_preferred_bitrate"))
&& (fptr_sp_session_user_name = dlsym(h, "sp_session_user_name"))
&& (fptr_sp_playlistcontainer_add_callbacks = dlsym(h, "sp_playlistcontainer_add_callbacks"))
&& (fptr_sp_playlistcontainer_num_playlists = dlsym(h, "sp_playlistcontainer_num_playlists"))
&& (fptr_sp_session_starred_create = dlsym(h, "sp_session_starred_create"))
@ -1412,6 +1419,14 @@ logged_in(sp_session *sess, sp_error error)
if (SP_ERROR_OK != error)
{
DPRINTF(E_LOG, L_SPOTIFY, "Login failed: %s\n", fptr_sp_error_message(error));
CHECK_ERR(L_REMOTE, pthread_mutex_lock(&status_lck));
spotify_status_info.libspotify_logged_in = false;
spotify_status_info.libspotify_user[0] = '\0';
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&status_lck));
listener_notify(LISTENER_SPOTIFY);
return;
}
@ -1436,6 +1451,14 @@ logged_in(sp_session *sess, sp_error error)
pl = fptr_sp_playlistcontainer_playlist(pc, i);
fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL);
}
CHECK_ERR(L_REMOTE, pthread_mutex_lock(&status_lck));
spotify_status_info.libspotify_logged_in = true;
snprintf(spotify_status_info.libspotify_user, sizeof(spotify_status_info.libspotify_user), "%s", fptr_sp_session_user_name(sess));
spotify_status_info.libspotify_user[sizeof(spotify_status_info.libspotify_user) - 1] = '\0';
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&status_lck));
listener_notify(LISTENER_SPOTIFY);
}
/**
@ -1450,10 +1473,17 @@ logged_out(sp_session *sess)
{
DPRINTF(E_INFO, L_SPOTIFY, "Logout complete\n");
CHECK_ERR(L_REMOTE, pthread_mutex_lock(&status_lck));
spotify_status_info.libspotify_logged_in = false;
spotify_status_info.libspotify_user[0] = '\0';
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&status_lck));
CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&login_lck));
CHECK_ERR(L_SPOTIFY, pthread_cond_signal(&login_cond));
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&login_lck));
listener_notify(LISTENER_SPOTIFY);
}
/**
@ -1794,40 +1824,51 @@ spotify_oauth_interface(struct evbuffer *evbuf, const char *redirect_uri)
}
/* Thread: httpd */
void
spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const char *redirect_uri)
int
spotify_oauth_callback(struct evkeyvalq *param, const char *redirect_uri, char **errmsg)
{
const char *code;
const char *err;
char *user = NULL;
int ret;
*errmsg = NULL;
code = evhttp_find_header(param, "code");
if (!code)
{
evbuffer_add_printf(evbuf, "Error: Didn't receive a code from Spotify\n");
return;
*errmsg = safe_asprintf("Error: Didn't receive a code from Spotify");
return -1;
}
DPRINTF(E_DBG, L_SPOTIFY, "Received OAuth code: %s\n", code);
evbuffer_add_printf(evbuf, "<p>Requesting access token from Spotify...\n");
ret = spotifywebapi_token_get(code, redirect_uri, &err);
ret = spotifywebapi_token_get(code, redirect_uri, &user, &err);
if (ret < 0)
{
evbuffer_add_printf(evbuf, "failed</p>\n<p>Error: %s</p>\n", err);
return;
*errmsg = safe_asprintf("Error: %s", err);
return -1;
}
// Received a valid access token
spotify_access_token_valid = true;
CHECK_ERR(L_REMOTE, pthread_mutex_lock(&status_lck));
spotify_status_info.webapi_token_valid = spotify_access_token_valid;
if (user)
{
snprintf(spotify_status_info.webapi_user, sizeof(spotify_status_info.webapi_user), "%s", user);
spotify_status_info.webapi_user[sizeof(spotify_status_info.webapi_user) - 1] = '\0';
free(user);
}
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&status_lck));
// Trigger scan after successful access to spotifywebapi
library_exec_async(webapi_scan, NULL);
evbuffer_add_printf(evbuf, "ok, all done</p>\n");
listener_notify(LISTENER_SPOTIFY);
return;
return 0;
}
static void
@ -1839,20 +1880,36 @@ spotify_uri_register(const char *uri)
commands_exec_async(cmdbase, uri_register, tmp);
}
/* Thread: library */
void
spotify_login(char **arglist)
spotify_status_info_get(struct spotify_status_info *info)
{
CHECK_ERR(L_REMOTE, pthread_mutex_lock(&status_lck));
memcpy(info, &spotify_status_info, sizeof(struct spotify_status_info));
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&status_lck));
}
/* Thread: library, httpd */
int
spotify_login_user(const char *user, const char *password, char **errmsg)
{
sp_error err;
if (!g_sess)
{
if (!g_libhandle)
DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - could not find libspotify\n");
{
DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - could not find libspotify\n");
if (errmsg)
*errmsg = safe_asprintf("Could not find libspotify");
}
else
DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - no valid Spotify session\n");
{
DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - no valid Spotify session\n");
if (errmsg)
*errmsg = safe_asprintf("No valid Spotify session");
}
return;
return -1;
}
if (SP_CONNECTION_STATE_LOGGED_IN == fptr_sp_session_connectionstate(g_sess))
@ -1867,18 +1924,21 @@ spotify_login(char **arglist)
if (SP_ERROR_OK != err)
{
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));
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&login_lck));
return;
return -1;
}
CHECK_ERR(L_SPOTIFY, pthread_cond_wait(&login_cond, &login_lck));
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&login_lck));
}
if (arglist)
if (user && password)
{
DPRINTF(E_LOG, L_SPOTIFY, "Spotify credentials file OK, logging in with username %s\n", arglist[0]);
err = fptr_sp_session_login(g_sess, arglist[0], arglist[1], 1, NULL);
DPRINTF(E_LOG, L_SPOTIFY, "Spotify credentials file OK, logging in with username %s\n", user);
err = fptr_sp_session_login(g_sess, user, password, 1, NULL);
}
else
{
@ -1889,8 +1949,22 @@ spotify_login(char **arglist)
if (SP_ERROR_OK != err)
{
DPRINTF(E_LOG, L_SPOTIFY, "Could not login into Spotify: %s\n", fptr_sp_error_message(err));
return;
if (errmsg)
*errmsg = safe_asprintf("Could not login into Spotify: %s", fptr_sp_error_message(err));
return -1;
}
return 0;
}
/* Thread: library */
void
spotify_login(char **arglist)
{
if (arglist)
spotify_login_user(arglist[0], arglist[1], NULL);
else
spotify_login_user(NULL, NULL, NULL);
}
static void
@ -2234,10 +2308,12 @@ create_base_playlist()
static int
initscan()
{
char *user = NULL;
scanning = true;
/* Refresh access token for the spotify webapi */
spotify_access_token_valid = (0 == spotifywebapi_token_refresh());
spotify_access_token_valid = (0 == spotifywebapi_token_refresh(&user));
if (!spotify_access_token_valid)
{
DPRINTF(E_LOG, L_SPOTIFY, "Spotify webapi token refresh failed. "
@ -2246,6 +2322,15 @@ initscan()
db_spotify_purge();
}
CHECK_ERR(L_REMOTE, pthread_mutex_lock(&status_lck));
spotify_status_info.webapi_token_valid = spotify_access_token_valid;
if (user)
{
snprintf(spotify_status_info.webapi_user, sizeof(spotify_status_info.webapi_user), "%s", user);
spotify_status_info.webapi_user[sizeof(spotify_status_info.webapi_user) - 1] = '\0';
free(user);
}
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&status_lck));
spotify_saved_plid = 0;
@ -2447,6 +2532,10 @@ spotify_init(void)
break;
}
CHECK_ERR(L_REMOTE, pthread_mutex_lock(&status_lck));
spotify_status_info.libspotify_installed = true;
CHECK_ERR(L_REMOTE, pthread_mutex_unlock(&status_lck));
spotify_audio_buffer = evbuffer_new();
CHECK_ERR(L_SPOTIFY, evbuffer_enable_locking(spotify_audio_buffer, NULL));

View File

@ -5,6 +5,18 @@
#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];
bool webapi_token_valid;
char webapi_user[100];
};
int
spotify_playback_setup(const char *path);
@ -33,12 +45,18 @@ spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h);
void
spotify_oauth_interface(struct evbuffer *evbuf, const char *redirect_uri);
void
spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const char *redirect_uri);
int
spotify_oauth_callback(struct evkeyvalq *param, const char *redirect_uri, char **errmsg);
int
spotify_login_user(const char *user, const char *password, char **errmsg);
void
spotify_login(char **arglist);
void
spotify_status_info_get(struct spotify_status_info *info);
int
spotify_init(void);

View File

@ -30,6 +30,7 @@
#include "http.h"
#include "library.h"
#include "logger.h"
#include "misc_json.h"
@ -37,6 +38,7 @@
static char *spotify_access_token;
static char *spotify_refresh_token;
static char *spotify_user_country;
static char *spotify_user;
static int32_t expires_in = 3600;
static time_t token_requested = 0;
@ -53,101 +55,6 @@ static const char *spotify_me_uri = "https://api.spotify.com/v1/me";
/*--------------------- HELPERS FOR SPOTIFY WEB API -------------------------*/
/* All the below is in the httpd thread */
static void
jparse_free(json_object* haystack)
{
if (haystack)
{
#ifdef HAVE_JSON_C_OLD
json_object_put(haystack);
#else
if (json_object_put(haystack) != 1)
DPRINTF(E_LOG, L_SPOTIFY, "Memleak: JSON parser did not free object\n");
#endif
}
}
static int
jparse_array_from_obj(json_object *haystack, const char *key, json_object **needle)
{
if (! (json_object_object_get_ex(haystack, key, needle) && json_object_get_type(*needle) == json_type_array) )
return -1;
else
return 0;
}
static const char *
jparse_str_from_obj(json_object *haystack, const char *key)
{
json_object *needle;
if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_string)
return json_object_get_string(needle);
else
return NULL;
}
static int
jparse_int_from_obj(json_object *haystack, const char *key)
{
json_object *needle;
if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_int)
return json_object_get_int(needle);
else
return 0;
}
static int
jparse_bool_from_obj(json_object *haystack, const char *key)
{
json_object *needle;
if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_boolean)
return json_object_get_boolean(needle);
else
return false;
}
static time_t
jparse_time_from_obj(json_object *haystack, const char *key)
{
const char *tmp;
struct tm tp;
time_t parsed_time;
memset(&tp, 0, sizeof(struct tm));
tmp = jparse_str_from_obj(haystack, key);
if (!tmp)
return 0;
strptime(tmp, "%Y-%m-%dT%H:%M:%SZ", &tp);
parsed_time = mktime(&tp);
if (parsed_time < 0)
return 0;
return parsed_time;
}
static const char *
jparse_str_from_array(json_object *array, int index, const char *key)
{
json_object *item;
int count;
if (json_object_get_type(array) != json_type_array)
return NULL;
count = json_object_array_length(array);
if (count <= 0 || count <= index)
return NULL;
item = json_object_array_get_idx(array, index);
return jparse_str_from_obj(item, key);
}
static void
free_http_client_ctx(struct http_client_ctx *ctx)
{
@ -172,7 +79,7 @@ request_uri(struct spotify_request *request, const char *uri)
memset(request, 0, sizeof(struct spotify_request));
if (0 > spotifywebapi_token_refresh())
if (0 > spotifywebapi_token_refresh(NULL))
{
return -1;
}
@ -585,13 +492,15 @@ spotifywebapi_playlist_start(struct spotify_request *request, const char *path,
}
static int
request_user_country()
request_user_info()
{
struct spotify_request request;
int ret;
free(spotify_user_country);
spotify_user_country = NULL;
free(spotify_user);
spotify_user = NULL;
ret = request_uri(&request, spotify_me_uri);
@ -601,8 +510,10 @@ request_user_country()
}
else
{
spotify_user = safe_strdup(jparse_str_from_obj(request.haystack, "id"));
spotify_user_country = safe_strdup(jparse_str_from_obj(request.haystack, "country"));
DPRINTF(E_DBG, L_SPOTIFY, "User country: '%s'\n", spotify_user_country);
DPRINTF(E_DBG, L_SPOTIFY, "User '%s', country '%s'\n", spotify_user, spotify_user_country);
}
spotifywebapi_request_end(&request);
@ -733,7 +644,7 @@ tokens_get(struct keyval *kv, const char **err)
if (spotify_refresh_token)
db_admin_set("spotify_refresh_token", spotify_refresh_token);
request_user_country();
request_user_info();
ret = 0;
@ -746,7 +657,7 @@ tokens_get(struct keyval *kv, const char **err)
}
int
spotifywebapi_token_get(const char *code, const char *redirect_uri, const char **err)
spotifywebapi_token_get(const char *code, const char *redirect_uri, char **user, const char **err)
{
struct keyval kv;
int ret;
@ -767,13 +678,17 @@ spotifywebapi_token_get(const char *code, const char *redirect_uri, const char *
else
ret = tokens_get(&kv, err);
if (user && ret == 0)
{
*user = safe_strdup(spotify_user);
}
keyval_clear(&kv);
return ret;
}
int
spotifywebapi_token_refresh()
spotifywebapi_token_refresh(char **user)
{
struct keyval kv;
char *refresh_token;
@ -808,6 +723,10 @@ spotifywebapi_token_refresh()
else
ret = tokens_get(&kv, &err);
if (user && ret == 0)
{
*user = safe_strdup(spotify_user);
}
free(refresh_token);
keyval_clear(&kv);

View File

@ -98,9 +98,9 @@ struct spotify_request
char *
spotifywebapi_oauth_uri_get(const char *redirect_uri);
int
spotifywebapi_token_get(const char *code, const char *redirect_uri, const char **err);
spotifywebapi_token_get(const char *code, const char *redirect_uri, char **user, const char **err);
int
spotifywebapi_token_refresh();
spotifywebapi_token_refresh(char **user);
void
spotifywebapi_request_end(struct spotify_request *request);

327
src/websocket.c Normal file
View File

@ -0,0 +1,327 @@
/*
* Copyright (C) 2017 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
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <json.h>
#include <libwebsockets.h>
#include <pthread.h>
#ifdef HAVE_PTHREAD_NP_H
# include <pthread_np.h>
#endif
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include "conffile.h"
#include "listener.h"
#include "logger.h"
static struct lws_context *context;
static pthread_t tid_websocket;
static int websocket_port;
static bool ws_exit = false;
// Event mask of events to notify websocket clients
static short events;
// Event mask of events processed by the writeable callback
static short write_events;
/* Thread: library (the thread the event occurred) */
static void
listener_cb(enum listener_event_type type)
{
// Add event to the event mask, clients will be notified at the next break of the libwebsockets service loop
events |= type;
}
/*
* Libwebsocket requires the HTTP protocol to be the first supported protocol
*
* This adds an empty implementation, because we are serving HTTP over libevent in httpd.h.
*/
static int
callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
{
return 0;
}
/*
* Each session of the "notify" protocol holds this event mask
*
* The client sends the events it wants to be notified of and the event mask is
* set accordingly translating them to the LISTENER enum (see listener.h)
*/
struct ws_session_data_notify
{
short events;
};
/*
* Processes client requests to the notify-protocol
*
* Expects the message in "in" to be a JSON string of the form:
*
* {
* "notify": [ "update" ]
* }
*/
static int
process_notify_request(struct ws_session_data_notify *session_data, void *in, size_t len)
{
json_tokener *tokener;
json_object *request;
json_object *item;
int count, i;
enum json_tokener_error jerr;
json_object *needle;
const char *event_type;
memset(session_data, 0, sizeof(struct ws_session_data_notify));
tokener = json_tokener_new();
request = json_tokener_parse_ex(tokener, in, len);
jerr = json_tokener_get_error(tokener);
if (jerr != json_tokener_success)
{
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request: %s\n", json_tokener_error_desc(jerr));
json_tokener_free(tokener);
return -1;
}
DPRINTF(E_DBG, L_WEB, "notify callback request: %s\n", json_object_to_json_string(request));
if (json_object_object_get_ex(request, "notify", &needle) && json_object_get_type(needle) == json_type_array)
{
count = json_object_array_length(needle);
for (i = 0; i < count; i++)
{
item = json_object_array_get_idx(needle, i);
if (json_object_get_type(item) == json_type_string)
{
event_type = json_object_get_string(item);
DPRINTF(E_DBG, L_WEB, "notify callback event received: %s\n", event_type);
if (0 == strcmp(event_type, "update"))
{
session_data->events |= LISTENER_UPDATE;
}
else if (0 == strcmp(event_type, "pairing"))
{
session_data->events |= LISTENER_PAIRING;
}
else if (0 == strcmp(event_type, "spotify"))
{
session_data->events |= LISTENER_SPOTIFY;
}
}
}
}
json_tokener_free(tokener);
json_object_put(request);
return 0;
}
/*
* Notify clients of the notify-protocol about occurred events
*
* Sends a JSON message of the form:
*
* {
* "notify": [ "update" ]
* }
*/
static void
send_notify_reply(short events, struct lws* wsi)
{
unsigned char* buf;
const char* json_response;
json_object* reply;
json_object* notify;
notify = json_object_new_array();
if (events & LISTENER_UPDATE)
{
json_object_array_add(notify, json_object_new_string("update"));
}
if (events & LISTENER_PAIRING)
{
json_object_array_add(notify, json_object_new_string("pairing"));
}
if (events & LISTENER_SPOTIFY)
{
json_object_array_add(notify, json_object_new_string("spotify"));
}
reply = json_object_new_object();
json_object_object_add(reply, "notify", notify);
json_response = json_object_to_json_string(reply);
buf = malloc(LWS_PRE + strlen(json_response));
memcpy(&buf[LWS_PRE], json_response, strlen(json_response));
lws_write(wsi, &buf[LWS_PRE], strlen(json_response), LWS_WRITE_TEXT);
free(buf);
json_object_put(reply);
}
/*
* Callback for the "notify" protocol
*/
static int
callback_notify(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
{
struct ws_session_data_notify *session_data = user;
int ret = 0;
DPRINTF(E_DBG, L_WEB, "notify callback reason: %d\n", reason);
switch (reason)
{
case LWS_CALLBACK_ESTABLISHED:
// Initialize session data for new connections
memset(session_data, 0, sizeof(struct ws_session_data_notify));
break;
case LWS_CALLBACK_RECEIVE:
ret = process_notify_request(session_data, in, len);
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
if (write_events)
{
send_notify_reply(write_events, wsi);
}
break;
default:
break;
}
return ret;
}
/*
* Supported protocols of the websocket, needs to be in line with the protocols array
*/
enum ws_protocols
{
WS_PROTOCOL_HTTP = 0,
WS_PROTOCOL_NOTIFY,
};
static struct lws_protocols protocols[] =
{
// The first protocol must always be the HTTP handler
{
"http-only", // Protocol name
callback_http, // Callback function
0, // Size of per session data
0, // Frame size / rx buffer (0 = max frame size)
},
{
"notify",
callback_notify,
sizeof(struct ws_session_data_notify),
0,
},
{ NULL, NULL, 0, 0 } // terminator
};
/* Thread: websocket */
static void *
websocket(void *arg)
{
listener_add(listener_cb, LISTENER_UPDATE | LISTENER_PAIRING | LISTENER_SPOTIFY);
while(!ws_exit)
{
lws_service(context, 1000);
if (events)
{
write_events = events;
events = 0;
lws_callback_on_writable_all_protocol(context, &protocols[WS_PROTOCOL_NOTIFY]);
}
}
lws_context_destroy(context);
pthread_exit(NULL);
}
int
websocket_init(void)
{
struct lws_context_creation_info info;
int ret;
websocket_port = cfg_getint(cfg_getsec(cfg, "general"), "websocket_port");
if (websocket_port <= 0)
{
DPRINTF(E_LOG, L_WEB, "Websocket disabled. To enable it, set websocket_port in config to a valid port number.\n");
return 0;
}
memset(&info, 0, sizeof(info));
info.port = websocket_port;
info.protocols = protocols;
info.gid = -1;
info.uid = -1;
context = lws_create_context(&info);
if (context == NULL)
{
DPRINTF(E_LOG, L_WEB, "Failed to create websocket context\n");
return -1;
}
ret = pthread_create(&tid_websocket, NULL, websocket, NULL);
if (ret < 0)
{
DPRINTF(E_LOG, L_WEB, "Could not spawn websocket thread: %s\n", strerror(errno));
lws_context_destroy(context);
return -1;
}
return 0;
}
void
websocket_deinit(void)
{
if (websocket_port > 0)
{
ws_exit = true;
pthread_join(tid_websocket, NULL);
}
}

29
src/websocket.h Normal file
View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2017 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
*/
#ifndef SRC_WEBSOCKET_H_
#define SRC_WEBSOCKET_H_
int
websocket_init(void);
void
websocket_deinit(void);
#endif /* SRC_WEBSOCKET_H_ */