commit
546362980b
10
.travis.yml
10
.travis.yml
|
@ -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
13
INSTALL
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
11
configure.ac
11
configure.ac
|
@ -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
|
||||
])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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(', ');
|
||||
}
|
||||
}
|
||||
|
||||
})
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -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 = \
|
||||
|
|
|
@ -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),
|
||||
|
|
25
src/db.c
25
src/db.c
|
@ -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)
|
||||
{
|
||||
|
|
3
src/db.h
3
src/db.h
|
@ -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);
|
||||
|
|
211
src/httpd.c
211
src/httpd.c
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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__ */
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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
|
||||
|
|
37
src/main.c
37
src/main.c
|
@ -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)
|
||||
|
|
38
src/misc.c
38
src/misc.c
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -29,6 +29,9 @@ struct keyval {
|
|||
};
|
||||
|
||||
|
||||
char **
|
||||
buildopts_get(void);
|
||||
|
||||
int
|
||||
safe_atoi32(const char *str, int32_t *val);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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_ */
|
36
src/mpd.c
36
src/mpd.c
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
void
|
||||
remote_pairing_kickoff(char **arglist);
|
||||
|
||||
char *
|
||||
remote_pairing_get_name(void);
|
||||
|
||||
int
|
||||
remote_pairing_init(void);
|
||||
|
||||
|
|
133
src/spotify.c
133
src/spotify.c
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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_ */
|
Loading…
Reference in New Issue