mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-15 16:53:18 -05:00
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
|
||||
|
32
htdocs/Makefile.am
Normal file
32
htdocs/Makefile.am
Normal file
@ -0,0 +1,32 @@
|
||||
htdocsdir = $(datadir)/forked-daapd/htdocs
|
||||
|
||||
htdocs_DATA = \
|
||||
admin.html
|
||||
|
||||
htdocscssdir = $(datadir)/forked-daapd/htdocs/css
|
||||
|
||||
htdocscss_DATA = \
|
||||
css/bulma.min.css \
|
||||
css/font-awesome.min.css \
|
||||
css/forked-daapd.css
|
||||
|
||||
htdocsfontsdir = $(datadir)/forked-daapd/htdocs/fonts
|
||||
|
||||
htdocsfonts_DATA = \
|
||||
fonts/FontAwesome.otf\
|
||||
fonts/fontawesome-webfont.eot \
|
||||
fonts/fontawesome-webfont.svg \
|
||||
fonts/fontawesome-webfont.ttf \
|
||||
fonts/fontawesome-webfont.woff \
|
||||
fonts/fontawesome-webfont.woff2
|
||||
|
||||
htdocsjsdir = $(datadir)/forked-daapd/htdocs/js
|
||||
|
||||
htdocsjs_DATA = \
|
||||
js/axios.js \
|
||||
js/axios.map \
|
||||
js/axios.min.js \
|
||||
js/axios.min.map \
|
||||
js/forked-daapd.js \
|
||||
js/vue.js \
|
||||
js/vue.min.js
|
198
htdocs/admin.html
Normal file
198
htdocs/admin.html
Normal file
@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>forked-daapd</title>
|
||||
<link rel="stylesheet" href="/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="/css/forked-daapd.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root" v-cloak>
|
||||
|
||||
|
||||
<!--
|
||||
############# Navbar #############
|
||||
-->
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">
|
||||
<b class="navbar-item">forked-daapd</b>
|
||||
<a class="navbar-item" href="https://github.com/ejurgensen/forked-daapd" title="GitHub"><i class="fa fa-github"></i></a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<!--
|
||||
############# Hero section #############
|
||||
-->
|
||||
<section class="hero is-dark is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
|
||||
<div class="column">
|
||||
<nav class="level is-mobile">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Artists</p>
|
||||
<p class="title">{{ library.artists }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Albums</p>
|
||||
<p class="title">{{ library.albums }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Songs</p>
|
||||
<p class="title">{{ library.songs }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Total playtime</p>
|
||||
<p class="title">{{ library.db_playtime | duration }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</div> <!-- columns -->
|
||||
</div><!-- container -->
|
||||
</div><!-- hero -->
|
||||
</section>
|
||||
|
||||
|
||||
<!--
|
||||
############# Content section #############
|
||||
-->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
<span class="icon" v-show="library.updating"><i class="fa fa-refresh fa-spin"></i></span>
|
||||
<span class="icon" v-show="!library.updating"><i class="fa fa-refresh"></i></span>
|
||||
Update library
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
Scan new and modified items into your library.
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item is-primary" v-on:click="update" v-show="!library.updating">Update</a>
|
||||
<span class="card-footer-item" v-show="library.updating">Update in progress ...</span>
|
||||
</footer>
|
||||
</div> <!-- card update library -->
|
||||
</div> <!-- column -->
|
||||
|
||||
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
<span class="icon"><i class="fa fa-mobile"></i></span> Remote pairing
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content" v-show="pairing.active">
|
||||
<p>Remote pairing request from <b>{{pairing.remote}}</b></p>
|
||||
<form v-on:submit.prevent="kickoffPairing">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Enter pairing code" v-model="pairing_req.pin">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="content" v-show="!pairing.active">
|
||||
<p>No active pairing request.</p>
|
||||
<a class="button" v-on:click="loadPairing" v-show="!config.websocket_port">Refresh</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- card remote pairing -->
|
||||
</div> <!-- column -->
|
||||
|
||||
|
||||
<div class="column">
|
||||
<div class="card" v-show="spotify.enabled">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
<span class="icon"><i class="fa fa-spotify"></i></span> Spotify
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content" v-show="!spotify.libspotify_installed">
|
||||
<p><b>libspotify</b> is not installed (required for playing spotify tracks)</p>
|
||||
</div>
|
||||
<div class="content" v-show="spotify.libspotify_installed">
|
||||
<div v-show="!spotify.libspotify_logged_in"><p><b>libspotify</b> (requires Spotify premium account, enables playback of Spotify songs):</p>
|
||||
<form v-on:submit.prevent="loginLibspotify">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Username" v-model="libspotify.user">
|
||||
<p class="help is-danger">{{ libspotify.errors.user }}</p>
|
||||
</div>
|
||||
<div class="control">
|
||||
<input class="input" type="password" placeholder="Password" v-model="libspotify.password">
|
||||
<p class="help is-danger">{{ libspotify.errors.password }}</p>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button" type="submit">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger">{{ libspotify.errors.error }}</p>
|
||||
</form>
|
||||
</div>
|
||||
<p v-show="spotify.libspotify_logged_in"><b>libspotify</b> (requires Spotify premium account, enables playback of Spotify songs): logged in as <b>{{ spotify.libspotify_user }}</b></p>
|
||||
<hr>
|
||||
<div v-show="!spotify.webapi_token_valid">
|
||||
<p><b>Spotify Web API</b> access is required to add saved albums and playlists to your library.</p>
|
||||
<a class="button" v-bind:href="spotify.oauth_uri">Authorize Web API access</a>
|
||||
</div>
|
||||
<div v-show="spotify.webapi_token_valid">
|
||||
<p><b>Spotify Web API</b>: access authorized for <b>{{ spotify.webapi_user }}</b></p>
|
||||
<a class="button" v-bind:href="spotify.oauth_uri">Reauthorize Web API access</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- card spotify -->
|
||||
</div> <!-- column -->
|
||||
|
||||
</div> <!-- columns -->
|
||||
</div> <!-- container -->
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="content has-text-centered">
|
||||
<p>
|
||||
<strong>forked-daapd</strong> - version {{ config.version }}
|
||||
</p>
|
||||
<p class="is-size-7">Compiled with support for {{ config.buildoptions | join }}.</p>
|
||||
<p class="is-size-7">Web interface built with <a href="http://bulma.io">Bulma</a>, <a href="http://fontawesome.io/">Font Awesome</a>, <a href="https://vuejs.org/">Vue.js</a>, <a href="https://github.com/mzabriskie/axios">axios</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
</div> <!-- #root -->
|
||||
|
||||
<script src="/js/vue.min.js"></script>
|
||||
<script src="/js/axios.min.js"></script>
|
||||
<script src="/js/forked-daapd.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
2
htdocs/css/bulma.min.css
vendored
Normal file
2
htdocs/css/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
htdocs/css/font-awesome.min.css
vendored
Normal file
4
htdocs/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
htdocs/css/forked-daapd.css
Normal file
3
htdocs/css/forked-daapd.css
Normal file
@ -0,0 +1,3 @@
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
BIN
htdocs/fonts/FontAwesome.otf
Normal file
BIN
htdocs/fonts/FontAwesome.otf
Normal file
Binary file not shown.
BIN
htdocs/fonts/fontawesome-webfont.eot
Normal file
BIN
htdocs/fonts/fontawesome-webfont.eot
Normal file
Binary file not shown.
2671
htdocs/fonts/fontawesome-webfont.svg
Normal file
2671
htdocs/fonts/fontawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 434 KiB |
BIN
htdocs/fonts/fontawesome-webfont.ttf
Normal file
BIN
htdocs/fonts/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
htdocs/fonts/fontawesome-webfont.woff
Normal file
BIN
htdocs/fonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
htdocs/fonts/fontawesome-webfont.woff2
Normal file
BIN
htdocs/fonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
1585
htdocs/js/axios.js
Normal file
1585
htdocs/js/axios.js
Normal file
File diff suppressed because it is too large
Load Diff
1
htdocs/js/axios.map
Normal file
1
htdocs/js/axios.map
Normal file
File diff suppressed because one or more lines are too long
9
htdocs/js/axios.min.js
vendored
Normal file
9
htdocs/js/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
htdocs/js/axios.min.map
Normal file
1
htdocs/js/axios.min.map
Normal file
File diff suppressed because one or more lines are too long
111
htdocs/js/forked-daapd.js
Normal file
111
htdocs/js/forked-daapd.js
Normal file
@ -0,0 +1,111 @@
|
||||
|
||||
|
||||
var app = new Vue({
|
||||
el: '#root',
|
||||
data: {
|
||||
config: {},
|
||||
library: {},
|
||||
spotify: {},
|
||||
pairing: {},
|
||||
pairing_req: { pin: '' },
|
||||
libspotify: { user: '', password: '', errors: { user: '', password: '', error: '' } }
|
||||
},
|
||||
|
||||
created: function () {
|
||||
this.loadConfig();
|
||||
this.loadLibrary();
|
||||
this.loadSpotify();
|
||||
this.loadPairing();
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadConfig: function() {
|
||||
axios.get('/api/config').then(response => {
|
||||
this.config = response.data;
|
||||
this.connect()});
|
||||
},
|
||||
|
||||
loadLibrary: function() {
|
||||
axios.get('/api/library').then(response => this.library = response.data);
|
||||
},
|
||||
|
||||
loadSpotify: function() {
|
||||
axios.get('/api/spotify').then(response => this.spotify = response.data);
|
||||
},
|
||||
|
||||
loadPairing: function() {
|
||||
axios.get('/api/pairing').then(response => this.pairing = response.data);
|
||||
},
|
||||
|
||||
update: function() {
|
||||
this.library.updating = true;
|
||||
axios.get('/api/update').then(console.log('Library is updating'));
|
||||
},
|
||||
|
||||
kickoffPairing: function() {
|
||||
axios.post('/api/pairing', this.pairing_req).then(response => {
|
||||
console.log('Kicked off pairing');
|
||||
if (!this.config.websocket_port) {
|
||||
this.pairing = {};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loginLibspotify: function() {
|
||||
axios.post('/api/spotify-login', this.libspotify).then(response => {
|
||||
this.libspotify.user = '';
|
||||
this.libspotify.password = '';
|
||||
this.libspotify.errors.user = '';
|
||||
this.libspotify.errors.password = '';
|
||||
this.libspotify.errors.error = '';
|
||||
if (!response.data.success) {
|
||||
this.libspotify.errors.user = response.data.errors.user;
|
||||
this.libspotify.errors.password = response.data.errors.password;
|
||||
this.libspotify.errors.error = response.data.errors.error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
connect: function() {
|
||||
if (this.config.websocket_port <= 0) {
|
||||
console.log('Websocket disabled');
|
||||
return;
|
||||
}
|
||||
var socket = new WebSocket('ws://' + document.domain + ':' + this.config.websocket_port, 'notify');
|
||||
const vm = this;
|
||||
socket.onopen = function() {
|
||||
socket.send(JSON.stringify({ notify: ['update', 'pairing', 'spotify']}));
|
||||
socket.onmessage = function(response) {
|
||||
console.log(response.data); // upon message
|
||||
var data = JSON.parse(response.data);
|
||||
if (data.notify.includes('update')) {
|
||||
vm.loadLibrary();
|
||||
}
|
||||
if (data.notify.includes('pairing')) {
|
||||
vm.loadPairing();
|
||||
}
|
||||
if (data.notify.includes('spotify')) {
|
||||
vm.loadSpotify();
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
filters: {
|
||||
duration: function(seconds) {
|
||||
// Display seconds as hours:minutes:seconds
|
||||
|
||||
var h = Math.floor(seconds / 3600);
|
||||
var m = Math.floor(seconds % 3600 / 60);
|
||||
var s = Math.floor(seconds % 3600 % 60);
|
||||
|
||||
return h + ":" + ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2);
|
||||
},
|
||||
|
||||
join: function(array) {
|
||||
return array.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
})
|
9175
htdocs/js/vue.js
Normal file
9175
htdocs/js/vue.js
Normal file
File diff suppressed because it is too large
Load Diff
8
htdocs/js/vue.min.js
vendored
Normal file
8
htdocs/js/vue.min.js
vendored
Normal file
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);
|
||||
|
||||
|
577
src/httpd_jsonapi.c
Normal file
577
src/httpd_jsonapi.c
Normal file
@ -0,0 +1,577 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Christian Meffert <christian.meffert@googlemail.com>
|
||||
*
|
||||
* Adapted from httpd_adm.c:
|
||||
* Copyright (C) 2015 Stuart NAIFEH <stu@naifeh.org>
|
||||
*
|
||||
* Adapted from httpd_daap.c and httpd.c:
|
||||
* Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
|
||||
* Copyright (C) 2010 Kai Elwert <elwertk@googlemail.com>
|
||||
*
|
||||
* Adapted from mt-daapd:
|
||||
* Copyright (C) 2003-2007 Ron Pedde <ron@pedde.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#include "httpd_jsonapi.h"
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
#include <event2/event.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/keyvalq_struct.h>
|
||||
#include <json.h>
|
||||
#include <regex.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "conffile.h"
|
||||
#include "db.h"
|
||||
#include "httpd.h"
|
||||
#include "library.h"
|
||||
#include "logger.h"
|
||||
#include "misc_json.h"
|
||||
#include "remote_pairing.h"
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
# include "spotify_webapi.h"
|
||||
# include "spotify.h"
|
||||
#endif
|
||||
|
||||
struct uri_map
|
||||
{
|
||||
regex_t preg;
|
||||
char *regexp;
|
||||
int (*handler)(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Endpoint to retrieve configuration values
|
||||
*
|
||||
* Example response:
|
||||
*
|
||||
* {
|
||||
* "websocket_port": 6603,
|
||||
* "version": "25.0"
|
||||
* }
|
||||
*/
|
||||
static int
|
||||
jsonapi_reply_config(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
|
||||
{
|
||||
json_object *reply;
|
||||
json_object *buildopts;
|
||||
int websocket_port;
|
||||
char **buildoptions;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
reply = json_object_new_object();
|
||||
|
||||
// Websocket port
|
||||
#ifdef WEBSOCKET
|
||||
websocket_port = cfg_getint(cfg_getsec(cfg, "general"), "websocket_port");
|
||||
#else
|
||||
websocket_port = 0;
|
||||
#endif
|
||||
json_object_object_add(reply, "websocket_port", json_object_new_int(websocket_port));
|
||||
|
||||
// forked-daapd version
|
||||
json_object_object_add(reply, "version", json_object_new_string(VERSION));
|
||||
|
||||
// enabled build options
|
||||
buildopts = json_object_new_array();
|
||||
buildoptions = buildopts_get();
|
||||
for (i = 0; buildoptions[i]; i++)
|
||||
{
|
||||
json_object_array_add(buildopts, json_object_new_string(buildoptions[i]));
|
||||
}
|
||||
json_object_object_add(reply, "buildoptions", buildopts);
|
||||
|
||||
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
|
||||
jparse_free(reply);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "config: Couldn't add config data to response buffer.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Endpoint to retrieve informations about the library
|
||||
*
|
||||
* Example response:
|
||||
*
|
||||
* {
|
||||
* "artists": 84,
|
||||
* "albums": 151,
|
||||
* "songs": 3085,
|
||||
* "db_playtime": 687824,
|
||||
* "updating": false
|
||||
*}
|
||||
*/
|
||||
static int
|
||||
jsonapi_reply_library(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
|
||||
{
|
||||
struct query_params qp;
|
||||
struct filecount_info fci;
|
||||
int artists;
|
||||
int albums;
|
||||
bool is_scanning;
|
||||
json_object *reply;
|
||||
int ret;
|
||||
|
||||
// Fetch values for response
|
||||
|
||||
memset(&qp, 0, sizeof(struct query_params));
|
||||
qp.type = Q_COUNT_ITEMS;
|
||||
ret = db_filecount_get(&fci, &qp);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "library: failed to get file count info\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
artists = db_files_get_artist_count();
|
||||
albums = db_files_get_album_count();
|
||||
|
||||
is_scanning = library_is_scanning();
|
||||
|
||||
|
||||
// Build json response
|
||||
|
||||
reply = json_object_new_object();
|
||||
|
||||
json_object_object_add(reply, "artists", json_object_new_int(artists));
|
||||
json_object_object_add(reply, "albums", json_object_new_int(albums));
|
||||
json_object_object_add(reply, "songs", json_object_new_int(fci.count));
|
||||
json_object_object_add(reply, "db_playtime", json_object_new_int64((fci.length / 1000)));
|
||||
json_object_object_add(reply, "updating", json_object_new_boolean(is_scanning));
|
||||
|
||||
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
|
||||
jparse_free(reply);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "library: Couldn't add library information data to response buffer.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Endpoint to trigger a library rescan
|
||||
*/
|
||||
static int
|
||||
jsonapi_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
|
||||
{
|
||||
library_rescan();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Endpoint to retrieve information about the spotify integration
|
||||
*
|
||||
* Exampe response:
|
||||
*
|
||||
* {
|
||||
* "enabled": true,
|
||||
* "oauth_uri": "https://accounts.spotify.com/authorize/?client_id=...
|
||||
* }
|
||||
*/
|
||||
static int
|
||||
jsonapi_reply_spotify(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
|
||||
{
|
||||
int httpd_port;
|
||||
char __attribute__((unused)) redirect_uri[256];
|
||||
char *oauth_uri;
|
||||
json_object *reply;
|
||||
int ret;
|
||||
|
||||
reply = json_object_new_object();
|
||||
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
struct spotify_status_info info;
|
||||
|
||||
json_object_object_add(reply, "enabled", json_object_new_boolean(true));
|
||||
|
||||
httpd_port = cfg_getint(cfg_getsec(cfg, "library"), "port");
|
||||
snprintf(redirect_uri, sizeof(redirect_uri), "http://forked-daapd.local:%d/oauth/spotify", httpd_port);
|
||||
|
||||
oauth_uri = spotifywebapi_oauth_uri_get(redirect_uri);
|
||||
if (!uri)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Cannot display Spotify oauth interface (http_form_uriencode() failed)\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
json_object_object_add(reply, "oauth_uri", json_object_new_string(oauth_uri));
|
||||
free(oauth_uri);
|
||||
}
|
||||
|
||||
spotify_status_info_get(&info);
|
||||
json_object_object_add(reply, "libspotify_installed", json_object_new_boolean(info.libspotify_installed));
|
||||
json_object_object_add(reply, "libspotify_logged_in", json_object_new_boolean(info.libspotify_logged_in));
|
||||
json_object_object_add(reply, "libspotify_user", json_object_new_string(info.libspotify_user));
|
||||
json_object_object_add(reply, "webapi_token_valid", json_object_new_boolean(info.webapi_token_valid));
|
||||
json_object_object_add(reply, "webapi_user", json_object_new_string(info.webapi_user));
|
||||
|
||||
#else
|
||||
json_object_object_add(reply, "enabled", json_object_new_boolean(false));
|
||||
#endif
|
||||
|
||||
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
|
||||
jparse_free(reply);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "spotify: Couldn't add spotify information data to response buffer.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
jsonapi_reply_spotify_login(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
|
||||
{
|
||||
struct evbuffer *in_evbuf;
|
||||
json_object* request;
|
||||
const char *user;
|
||||
const char *password;
|
||||
char *errmsg = NULL;
|
||||
json_object* reply;
|
||||
json_object* errors;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_WEB, "Received spotify login request\n");
|
||||
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
in_evbuf = evhttp_request_get_input_buffer(req);
|
||||
request = jparse_obj_from_evbuffer(in_evbuf);
|
||||
if (!request)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
reply = json_object_new_object();
|
||||
|
||||
user = jparse_str_from_obj(request, "user");
|
||||
password = jparse_str_from_obj(request, "password");
|
||||
if (user && strlen(user) > 0 && password && strlen(password) > 0)
|
||||
{
|
||||
ret = spotify_login_user(user, password, &errmsg);
|
||||
if (ret < 0)
|
||||
{
|
||||
json_object_object_add(reply, "success", json_object_new_boolean(false));
|
||||
errors = json_object_new_object();
|
||||
json_object_object_add(errors, "error", json_object_new_string(errmsg));
|
||||
json_object_object_add(reply, "errors", errors);
|
||||
}
|
||||
else
|
||||
{
|
||||
json_object_object_add(reply, "success", json_object_new_boolean(true));
|
||||
}
|
||||
free(errmsg);
|
||||
}
|
||||
else
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "No user or password in spotify login post request\n");
|
||||
|
||||
json_object_object_add(reply, "success", json_object_new_boolean(false));
|
||||
errors = json_object_new_object();
|
||||
if (!user || strlen(user) == 0)
|
||||
json_object_object_add(errors, "user", json_object_new_string("Username is required"));
|
||||
if (!password || strlen(password) == 0)
|
||||
json_object_object_add(errors, "password", json_object_new_string("Password is required"));
|
||||
json_object_object_add(reply, "errors", errors);
|
||||
}
|
||||
|
||||
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
|
||||
jparse_free(reply);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "spotify: Couldn't add spotify login data to response buffer.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
#else
|
||||
DPRINTF(E_LOG, L_WEB, "Received spotify login request but was not compiled with enable-spotify\n");
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Endpoint to kickoff pairing of a daap/dacp client
|
||||
*
|
||||
* Expects the paring pin to be present in the post request body, e. g.:
|
||||
*
|
||||
* {
|
||||
* "pin": "1234"
|
||||
* }
|
||||
*/
|
||||
static int
|
||||
pairing_kickoff(struct evhttp_request* req)
|
||||
{
|
||||
struct evbuffer *evbuf;
|
||||
json_object* request;
|
||||
const char* message;
|
||||
|
||||
evbuf = evhttp_request_get_input_buffer(req);
|
||||
request = jparse_obj_from_evbuffer(evbuf);
|
||||
if (!request)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_WEB, "Received pairing post request: %s\n", json_object_to_json_string(request));
|
||||
|
||||
message = jparse_str_from_obj(request, "pin");
|
||||
if (message)
|
||||
remote_pairing_kickoff((char **)&message);
|
||||
else
|
||||
DPRINTF(E_LOG, L_WEB, "Missing pin in request body: %s\n", json_object_to_json_string(request));
|
||||
|
||||
jparse_free(request);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Endpoint to retrieve pairing information
|
||||
*
|
||||
* Example response:
|
||||
*
|
||||
* {
|
||||
* "active": true,
|
||||
* "remote": "remote name"
|
||||
* }
|
||||
*/
|
||||
static int
|
||||
pairing_get(struct evbuffer *evbuf)
|
||||
{
|
||||
char *remote_name;
|
||||
json_object *reply;
|
||||
int ret;
|
||||
|
||||
remote_name = remote_pairing_get_name();
|
||||
|
||||
reply = json_object_new_object();
|
||||
|
||||
if (remote_name)
|
||||
{
|
||||
json_object_object_add(reply, "active", json_object_new_boolean(true));
|
||||
json_object_object_add(reply, "remote", json_object_new_string(remote_name));
|
||||
}
|
||||
else
|
||||
{
|
||||
json_object_object_add(reply, "active", json_object_new_boolean(false));
|
||||
}
|
||||
|
||||
ret = evbuffer_add_printf(evbuf, "%s", json_object_to_json_string(reply));
|
||||
jparse_free(reply);
|
||||
free(remote_name);
|
||||
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "pairing: Couldn't add pairing information data to response buffer.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Endpoint to pair daap/dacp client
|
||||
*
|
||||
* If request is a GET request, returns information about active pairing remote.
|
||||
* If request is a POST request, tries to pair the active remote with the given pin.
|
||||
*/
|
||||
static int
|
||||
jsonapi_reply_pairing(struct evhttp_request *req, struct evbuffer *evbuf, char *uri, struct evkeyvalq *query)
|
||||
{
|
||||
if (evhttp_request_get_command(req) == EVHTTP_REQ_POST)
|
||||
{
|
||||
return pairing_kickoff(req);
|
||||
}
|
||||
|
||||
return pairing_get(evbuf);
|
||||
}
|
||||
|
||||
static struct uri_map adm_handlers[] =
|
||||
{
|
||||
{ .regexp = "^/api/config", .handler = jsonapi_reply_config },
|
||||
{ .regexp = "^/api/library", .handler = jsonapi_reply_library },
|
||||
{ .regexp = "^/api/update", .handler = jsonapi_reply_update },
|
||||
{ .regexp = "^/api/spotify-login", .handler = jsonapi_reply_spotify_login },
|
||||
{ .regexp = "^/api/spotify", .handler = jsonapi_reply_spotify },
|
||||
{ .regexp = "^/api/pairing", .handler = jsonapi_reply_pairing },
|
||||
{ .regexp = NULL, .handler = NULL }
|
||||
};
|
||||
|
||||
void
|
||||
jsonapi_request(struct evhttp_request *req)
|
||||
{
|
||||
char *full_uri;
|
||||
char *uri;
|
||||
char *ptr;
|
||||
struct evbuffer *evbuf;
|
||||
struct evkeyvalq query;
|
||||
struct evkeyvalq *headers;
|
||||
int handler;
|
||||
int ret;
|
||||
int i;
|
||||
|
||||
/* Check authentication */
|
||||
if (!httpd_admin_check_auth(req))
|
||||
{
|
||||
DPRINTF(E_DBG, L_WEB, "JSON api request denied;\n");
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&query, 0, sizeof(struct evkeyvalq));
|
||||
|
||||
full_uri = httpd_fixup_uri(req);
|
||||
if (!full_uri)
|
||||
{
|
||||
evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
ptr = strchr(full_uri, '?');
|
||||
if (ptr)
|
||||
*ptr = '\0';
|
||||
|
||||
uri = strdup(full_uri);
|
||||
if (!uri)
|
||||
{
|
||||
free(full_uri);
|
||||
evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ptr)
|
||||
*ptr = '?';
|
||||
|
||||
ptr = uri;
|
||||
uri = evhttp_decode_uri(uri);
|
||||
free(ptr);
|
||||
|
||||
DPRINTF(E_DBG, L_WEB, "Web admin request: %s\n", full_uri);
|
||||
|
||||
handler = -1;
|
||||
for (i = 0; adm_handlers[i].handler; i++)
|
||||
{
|
||||
ret = regexec(&adm_handlers[i].preg, uri, 0, NULL, 0);
|
||||
if (ret == 0)
|
||||
{
|
||||
handler = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (handler < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Unrecognized web admin request\n");
|
||||
|
||||
evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
|
||||
|
||||
free(uri);
|
||||
free(full_uri);
|
||||
return;
|
||||
}
|
||||
|
||||
evbuf = evbuffer_new();
|
||||
if (!evbuf)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Could not allocate evbuffer for Web Admin reply\n");
|
||||
|
||||
evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error");
|
||||
|
||||
free(uri);
|
||||
free(full_uri);
|
||||
return;
|
||||
}
|
||||
|
||||
evhttp_parse_query(full_uri, &query);
|
||||
|
||||
headers = evhttp_request_get_output_headers(req);
|
||||
evhttp_add_header(headers, "DAAP-Server", "forked-daapd/" VERSION);
|
||||
|
||||
ret = adm_handlers[handler].handler(req, evbuf, uri, &query);
|
||||
if (ret < 0)
|
||||
{
|
||||
evhttp_send_error(req, 500, "Internal Server Error");
|
||||
}
|
||||
else
|
||||
{
|
||||
headers = evhttp_request_get_output_headers(req);
|
||||
evhttp_add_header(headers, "Content-Type", "application/json");
|
||||
httpd_send_reply(req, HTTP_OK, "OK", evbuf, 0);
|
||||
}
|
||||
|
||||
evbuffer_free(evbuf);
|
||||
evhttp_clear_headers(&query);
|
||||
free(uri);
|
||||
free(full_uri);
|
||||
}
|
||||
|
||||
int
|
||||
jsonapi_is_request(struct evhttp_request *req, char *uri)
|
||||
{
|
||||
if (strncmp(uri, "/api/", strlen("/api/")) == 0)
|
||||
return 1;
|
||||
if (strcmp(uri, "/api") == 0)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
jsonapi_init(void)
|
||||
{
|
||||
char buf[64];
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
for (i = 0; adm_handlers[i].handler; i++)
|
||||
{
|
||||
ret = regcomp(&adm_handlers[i].preg, adm_handlers[i].regexp, REG_EXTENDED | REG_NOSUB);
|
||||
if (ret != 0)
|
||||
{
|
||||
regerror(ret, &adm_handlers[i].preg, buf, sizeof(buf));
|
||||
|
||||
DPRINTF(E_FATAL, L_WEB, "Admin web interface init failed; regexp error: %s\n", buf);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
jsonapi_deinit(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; adm_handlers[i].handler; i++)
|
||||
regfree(&adm_handlers[i].preg);
|
||||
}
|
||||
|
19
src/httpd_jsonapi.h
Normal file
19
src/httpd_jsonapi.h
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
#ifndef __HTTPD_JSONAPI_H__
|
||||
#define __HTTPD_JSONAPI_H__
|
||||
|
||||
#include <event2/http.h>
|
||||
|
||||
int
|
||||
jsonapi_init(void);
|
||||
|
||||
void
|
||||
jsonapi_deinit(void);
|
||||
|
||||
void
|
||||
jsonapi_request(struct evhttp_request *req);
|
||||
|
||||
int
|
||||
jsonapi_is_request(struct evhttp_request *req, char *uri);
|
||||
|
||||
#endif /* !__HTTPD_JSONAPI_H__ */
|
@ -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);
|
||||
|
||||
|
145
src/misc_json.c
Normal file
145
src/misc_json.c
Normal file
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Christian Meffert <christian.meffert@googlemail.com>
|
||||
*
|
||||
* Some code included below is in the public domain, check comments
|
||||
* in the file.
|
||||
*
|
||||
* Pieces of code adapted from mt-daapd:
|
||||
* Copyright (C) 2003-2007 Ron Pedde (ron@pedde.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/event.h>
|
||||
#include <json.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
void
|
||||
jparse_free(json_object* haystack)
|
||||
{
|
||||
if (haystack)
|
||||
{
|
||||
#ifdef HAVE_JSON_C_OLD
|
||||
json_object_put(haystack);
|
||||
#else
|
||||
if (json_object_put(haystack) != 1)
|
||||
DPRINTF(E_LOG, L_MISC, "Memleak: JSON parser did not free object\n");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
jparse_array_from_obj(json_object *haystack, const char *key, json_object **needle)
|
||||
{
|
||||
if (! (json_object_object_get_ex(haystack, key, needle) && json_object_get_type(*needle) == json_type_array) )
|
||||
return -1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *
|
||||
jparse_str_from_obj(json_object *haystack, const char *key)
|
||||
{
|
||||
json_object *needle;
|
||||
|
||||
if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_string)
|
||||
return json_object_get_string(needle);
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int
|
||||
jparse_int_from_obj(json_object *haystack, const char *key)
|
||||
{
|
||||
json_object *needle;
|
||||
|
||||
if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_int)
|
||||
return json_object_get_int(needle);
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
jparse_bool_from_obj(json_object *haystack, const char *key)
|
||||
{
|
||||
json_object *needle;
|
||||
|
||||
if (json_object_object_get_ex(haystack, key, &needle) && json_object_get_type(needle) == json_type_boolean)
|
||||
return json_object_get_boolean(needle);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
time_t
|
||||
jparse_time_from_obj(json_object *haystack, const char *key)
|
||||
{
|
||||
const char *tmp;
|
||||
struct tm tp;
|
||||
time_t parsed_time;
|
||||
|
||||
memset(&tp, 0, sizeof(struct tm));
|
||||
|
||||
tmp = jparse_str_from_obj(haystack, key);
|
||||
if (!tmp)
|
||||
return 0;
|
||||
|
||||
strptime(tmp, "%Y-%m-%dT%H:%M:%SZ", &tp);
|
||||
parsed_time = mktime(&tp);
|
||||
if (parsed_time < 0)
|
||||
return 0;
|
||||
|
||||
return parsed_time;
|
||||
}
|
||||
|
||||
const char *
|
||||
jparse_str_from_array(json_object *array, int index, const char *key)
|
||||
{
|
||||
json_object *item;
|
||||
int count;
|
||||
|
||||
if (json_object_get_type(array) != json_type_array)
|
||||
return NULL;
|
||||
|
||||
count = json_object_array_length(array);
|
||||
if (count <= 0 || count <= index)
|
||||
return NULL;
|
||||
|
||||
item = json_object_array_get_idx(array, index);
|
||||
return jparse_str_from_obj(item, key);
|
||||
}
|
||||
|
||||
json_object *
|
||||
jparse_obj_from_evbuffer(struct evbuffer *evbuf)
|
||||
{
|
||||
char *json_str;
|
||||
|
||||
// 0-terminate for safety
|
||||
evbuffer_add(evbuf, "", 1);
|
||||
|
||||
json_str = (char *) evbuffer_pullup(evbuf, -1);
|
||||
if (!json_str || (strlen(json_str) == 0))
|
||||
{
|
||||
DPRINTF(E_LOG, L_MISC, "Failed to parse JSON from input buffer\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return json_tokener_parse(json_str);
|
||||
}
|
60
src/misc_json.h
Normal file
60
src/misc_json.h
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Christian Meffert <christian.meffert@googlemail.com>
|
||||
*
|
||||
* Some code included below is in the public domain, check comments
|
||||
* in the file.
|
||||
*
|
||||
* Pieces of code adapted from mt-daapd:
|
||||
* Copyright (C) 2003-2007 Ron Pedde (ron@pedde.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
|
||||
#ifndef SRC_MISC_JSON_H_
|
||||
#define SRC_MISC_JSON_H_
|
||||
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <json.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
|
||||
void
|
||||
jparse_free(json_object *haystack);
|
||||
|
||||
int
|
||||
jparse_array_from_obj(json_object *haystack, const char *key, json_object **needle);
|
||||
|
||||
const char *
|
||||
jparse_str_from_obj(json_object *haystack, const char *key);
|
||||
|
||||
int
|
||||
jparse_int_from_obj(json_object *haystack, const char *key);
|
||||
|
||||
int
|
||||
jparse_bool_from_obj(json_object *haystack, const char *key);
|
||||
|
||||
time_t
|
||||
jparse_time_from_obj(json_object *haystack, const char *key);
|
||||
|
||||
const char *
|
||||
jparse_str_from_array(json_object *array, int index, const char *key);
|
||||
|
||||
json_object *
|
||||
jparse_obj_from_evbuffer(struct evbuffer *evbuf);
|
||||
|
||||
#endif /* SRC_MISC_JSON_H_ */
|
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);
|
||||
|
327
src/websocket.c
Normal file
327
src/websocket.c
Normal file
@ -0,0 +1,327 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Christian Meffert <christian.meffert@googlemail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
#include <json.h>
|
||||
#include <libwebsockets.h>
|
||||
#include <pthread.h>
|
||||
#ifdef HAVE_PTHREAD_NP_H
|
||||
# include <pthread_np.h>
|
||||
#endif
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "conffile.h"
|
||||
#include "listener.h"
|
||||
#include "logger.h"
|
||||
|
||||
|
||||
static struct lws_context *context;
|
||||
static pthread_t tid_websocket;
|
||||
|
||||
static int websocket_port;
|
||||
static bool ws_exit = false;
|
||||
|
||||
// Event mask of events to notify websocket clients
|
||||
static short events;
|
||||
// Event mask of events processed by the writeable callback
|
||||
static short write_events;
|
||||
|
||||
|
||||
|
||||
/* Thread: library (the thread the event occurred) */
|
||||
static void
|
||||
listener_cb(enum listener_event_type type)
|
||||
{
|
||||
// Add event to the event mask, clients will be notified at the next break of the libwebsockets service loop
|
||||
events |= type;
|
||||
}
|
||||
|
||||
/*
|
||||
* Libwebsocket requires the HTTP protocol to be the first supported protocol
|
||||
*
|
||||
* This adds an empty implementation, because we are serving HTTP over libevent in httpd.h.
|
||||
*/
|
||||
static int
|
||||
callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Each session of the "notify" protocol holds this event mask
|
||||
*
|
||||
* The client sends the events it wants to be notified of and the event mask is
|
||||
* set accordingly translating them to the LISTENER enum (see listener.h)
|
||||
*/
|
||||
struct ws_session_data_notify
|
||||
{
|
||||
short events;
|
||||
};
|
||||
|
||||
/*
|
||||
* Processes client requests to the notify-protocol
|
||||
*
|
||||
* Expects the message in "in" to be a JSON string of the form:
|
||||
*
|
||||
* {
|
||||
* "notify": [ "update" ]
|
||||
* }
|
||||
*/
|
||||
static int
|
||||
process_notify_request(struct ws_session_data_notify *session_data, void *in, size_t len)
|
||||
{
|
||||
json_tokener *tokener;
|
||||
json_object *request;
|
||||
json_object *item;
|
||||
int count, i;
|
||||
enum json_tokener_error jerr;
|
||||
json_object *needle;
|
||||
const char *event_type;
|
||||
|
||||
memset(session_data, 0, sizeof(struct ws_session_data_notify));
|
||||
|
||||
tokener = json_tokener_new();
|
||||
request = json_tokener_parse_ex(tokener, in, len);
|
||||
jerr = json_tokener_get_error(tokener);
|
||||
|
||||
if (jerr != json_tokener_success)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request: %s\n", json_tokener_error_desc(jerr));
|
||||
json_tokener_free(tokener);
|
||||
return -1;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_WEB, "notify callback request: %s\n", json_object_to_json_string(request));
|
||||
|
||||
if (json_object_object_get_ex(request, "notify", &needle) && json_object_get_type(needle) == json_type_array)
|
||||
{
|
||||
count = json_object_array_length(needle);
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
item = json_object_array_get_idx(needle, i);
|
||||
|
||||
if (json_object_get_type(item) == json_type_string)
|
||||
{
|
||||
event_type = json_object_get_string(item);
|
||||
DPRINTF(E_DBG, L_WEB, "notify callback event received: %s\n", event_type);
|
||||
|
||||
if (0 == strcmp(event_type, "update"))
|
||||
{
|
||||
session_data->events |= LISTENER_UPDATE;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "pairing"))
|
||||
{
|
||||
session_data->events |= LISTENER_PAIRING;
|
||||
}
|
||||
else if (0 == strcmp(event_type, "spotify"))
|
||||
{
|
||||
session_data->events |= LISTENER_SPOTIFY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json_tokener_free(tokener);
|
||||
json_object_put(request);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Notify clients of the notify-protocol about occurred events
|
||||
*
|
||||
* Sends a JSON message of the form:
|
||||
*
|
||||
* {
|
||||
* "notify": [ "update" ]
|
||||
* }
|
||||
*/
|
||||
static void
|
||||
send_notify_reply(short events, struct lws* wsi)
|
||||
{
|
||||
unsigned char* buf;
|
||||
const char* json_response;
|
||||
json_object* reply;
|
||||
json_object* notify;
|
||||
|
||||
notify = json_object_new_array();
|
||||
if (events & LISTENER_UPDATE)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("update"));
|
||||
}
|
||||
if (events & LISTENER_PAIRING)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("pairing"));
|
||||
}
|
||||
if (events & LISTENER_SPOTIFY)
|
||||
{
|
||||
json_object_array_add(notify, json_object_new_string("spotify"));
|
||||
}
|
||||
|
||||
reply = json_object_new_object();
|
||||
json_object_object_add(reply, "notify", notify);
|
||||
|
||||
json_response = json_object_to_json_string(reply);
|
||||
|
||||
buf = malloc(LWS_PRE + strlen(json_response));
|
||||
memcpy(&buf[LWS_PRE], json_response, strlen(json_response));
|
||||
lws_write(wsi, &buf[LWS_PRE], strlen(json_response), LWS_WRITE_TEXT);
|
||||
|
||||
free(buf);
|
||||
json_object_put(reply);
|
||||
}
|
||||
|
||||
/*
|
||||
* Callback for the "notify" protocol
|
||||
*/
|
||||
static int
|
||||
callback_notify(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
|
||||
{
|
||||
struct ws_session_data_notify *session_data = user;
|
||||
int ret = 0;
|
||||
|
||||
DPRINTF(E_DBG, L_WEB, "notify callback reason: %d\n", reason);
|
||||
switch (reason)
|
||||
{
|
||||
case LWS_CALLBACK_ESTABLISHED:
|
||||
// Initialize session data for new connections
|
||||
memset(session_data, 0, sizeof(struct ws_session_data_notify));
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_RECEIVE:
|
||||
ret = process_notify_request(session_data, in, len);
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_SERVER_WRITEABLE:
|
||||
if (write_events)
|
||||
{
|
||||
send_notify_reply(write_events, wsi);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Supported protocols of the websocket, needs to be in line with the protocols array
|
||||
*/
|
||||
enum ws_protocols
|
||||
{
|
||||
WS_PROTOCOL_HTTP = 0,
|
||||
WS_PROTOCOL_NOTIFY,
|
||||
};
|
||||
|
||||
static struct lws_protocols protocols[] =
|
||||
{
|
||||
// The first protocol must always be the HTTP handler
|
||||
{
|
||||
"http-only", // Protocol name
|
||||
callback_http, // Callback function
|
||||
0, // Size of per session data
|
||||
0, // Frame size / rx buffer (0 = max frame size)
|
||||
},
|
||||
{
|
||||
"notify",
|
||||
callback_notify,
|
||||
sizeof(struct ws_session_data_notify),
|
||||
0,
|
||||
},
|
||||
{ NULL, NULL, 0, 0 } // terminator
|
||||
};
|
||||
|
||||
|
||||
/* Thread: websocket */
|
||||
static void *
|
||||
websocket(void *arg)
|
||||
{
|
||||
listener_add(listener_cb, LISTENER_UPDATE | LISTENER_PAIRING | LISTENER_SPOTIFY);
|
||||
|
||||
while(!ws_exit)
|
||||
{
|
||||
lws_service(context, 1000);
|
||||
if (events)
|
||||
{
|
||||
write_events = events;
|
||||
events = 0;
|
||||
lws_callback_on_writable_all_protocol(context, &protocols[WS_PROTOCOL_NOTIFY]);
|
||||
}
|
||||
}
|
||||
|
||||
lws_context_destroy(context);
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
int
|
||||
websocket_init(void)
|
||||
{
|
||||
struct lws_context_creation_info info;
|
||||
int ret;
|
||||
|
||||
websocket_port = cfg_getint(cfg_getsec(cfg, "general"), "websocket_port");
|
||||
|
||||
if (websocket_port <= 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Websocket disabled. To enable it, set websocket_port in config to a valid port number.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.port = websocket_port;
|
||||
info.protocols = protocols;
|
||||
info.gid = -1;
|
||||
info.uid = -1;
|
||||
|
||||
|
||||
context = lws_create_context(&info);
|
||||
if (context == NULL)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Failed to create websocket context\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
ret = pthread_create(&tid_websocket, NULL, websocket, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_WEB, "Could not spawn websocket thread: %s\n", strerror(errno));
|
||||
|
||||
lws_context_destroy(context);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
websocket_deinit(void)
|
||||
{
|
||||
if (websocket_port > 0)
|
||||
{
|
||||
ws_exit = true;
|
||||
pthread_join(tid_websocket, NULL);
|
||||
}
|
||||
}
|
29
src/websocket.h
Normal file
29
src/websocket.h
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Christian Meffert <christian.meffert@googlemail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
|
||||
#ifndef SRC_WEBSOCKET_H_
|
||||
#define SRC_WEBSOCKET_H_
|
||||
|
||||
int
|
||||
websocket_init(void);
|
||||
|
||||
void
|
||||
websocket_deinit(void);
|
||||
|
||||
#endif /* SRC_WEBSOCKET_H_ */
|
Loading…
x
Reference in New Issue
Block a user