diff --git a/README_JSON_API.md b/README_JSON_API.md index 4e3f06c5..9577cc99 100644 --- a/README_JSON_API.md +++ b/README_JSON_API.md @@ -8,6 +8,7 @@ Available API endpoints: * [Library](#library): list playlists, artists, albums and tracks from your library or trigger library rescan * [Search](#search): search for playlists, artists, albums and tracks * [Server info](#server-info): get server information +* [Settings](#settings): list and change settings for the player web interface * [Push notifications](#push-notifications): receive push notifications JSON-Object model: @@ -1916,6 +1917,163 @@ curl -X GET "http://localhost:3689/api/config" } ``` + +## Settings + +| Method | Endpoint | Description | +| --------- | ------------------------------------------------ | ------------------------------------ | +| GET | [/api/settings](#list-categories) | Get all available categories | +| GET | [/api/settings/{category-name}](#get-a-category) | Get all available options for a category | +| GET | [/api/settings/{category-name}/{option-name}](#get-a-option) | Get a single setting option | +| PUT | [/api/settings/{category-name}/{option-name}](#change-a-option-value) | Change the value of a setting option | + + + +### List categories + +List all settings categories with their options + +**Endpoint** + +```http +GET /api/settings +``` + +**Response** + +| Key | Type | Value | +| --------------- | -------- | ----------------------------------------- | +| categories | array | Array of settings [category](#category-object) objects | + + +**Example** + +```shell +curl -X GET "http://localhost:3689/api/settings" +``` + +```json +{ + "categories": [ + { + "name": "webinterface", + "options": [ + { + "name": "show_composer_now_playing", + "type": 1, + "value": true + }, + { + "name": "show_composer_for_genre", + "type": 2, + "value": "classical" + } + ] + } + ] +} +``` + + +### Get a category + +Get a settings category with their options + +**Endpoint** + +```http +GET /api/settings/{category-name} +``` + +**Response** + +Returns a settings [category](#category-object) object + + +**Example** + +```shell +curl -X GET "http://localhost:3689/api/settings/webinterface" +``` + +```json +{ + "name": "webinterface", + "options": [ + { + "name": "show_composer_now_playing", + "type": 1, + "value": true + }, + { + "name": "show_composer_for_genre", + "type": 2, + "value": "classical" + } + ] +} +``` + + +### Get a option + +Get a single settings option + +**Endpoint** + +```http +GET /api/settings/{category-name}/{option-name} +``` + +**Response** + +Returns a settings [option](#option-object) object + + +**Example** + +```shell +curl -X GET "http://localhost:3689/api/settings/webinterface/show_composer_now_playing" +``` + +```json +{ + "name": "show_composer_now_playing", + "type": 1, + "value": true +} +``` + + +### Change a option value + +Get a single settings option + +**Endpoint** + +```http +PUT /api/settings/{category-name}/{option-name} +``` + +**Request** + +| Key | Type | Value | +| --------------- | -------- | ----------------------------------------- | +| name | string | Option name | +| value | (integer / boolean / string) | New option value | + +**Response** + +On success returns the HTTP `204 No Content` success status response code. + + +**Example** + +```shell +curl -X PUT "http://localhost:3689/api/settings/webinterface/show_composer_now_playing" --data "{\"name\":\"show_composer_now_playing\",\"value\":true}" +``` + + ## Push notifications If forked-daapd was built with websocket support, forked-daapd exposes a websocket at `localhost:3688` to inform clients of changes (e. g. player state or library updates). @@ -2103,6 +2261,23 @@ curl --include \ | path | string | Directory path | +### `category` object + +| Key | Type | Value | +| --------------- | -------- | ----------------------------------------- | +| name | string | Category name | +| options | array | Array of option in this category | + + +### `option` object + +| Key | Type | Value | +| --------------- | -------- | ----------------------------------------- | +| name | string | Option name | +| type | integer | The type of the value for this option (`0`: integer, `1`: boolean, `2`: string) | +| value | (integer / boolean / string) | Current value for this option | + + ### Artwork urls Artwork urls in `queue item`, `artist`, `album` and `track` objects can be either relative urls or absolute urls to the artwork image. diff --git a/src/Makefile.am b/src/Makefile.am index 36bfea6f..807f9525 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -122,6 +122,7 @@ forked_daapd_SOURCES = main.c \ smartpl_query.c smartpl_query.h \ player.c player.h \ worker.c worker.h \ + settings.c settings.h \ input.h input.c \ inputs/file_http.c inputs/pipe.c \ outputs.h outputs.c \ diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index f9c919d2..3e5754dd 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -53,6 +53,7 @@ #include "misc_json.h" #include "player.h" #include "remote_pairing.h" +#include "settings.h" #include "smartpl_query.h" #ifdef HAVE_SPOTIFY_H # include "spotify_webapi.h" @@ -729,6 +730,259 @@ jsonapi_reply_config(struct httpd_request *hreq) return HTTP_OK; } +static json_object * +option_get_json(struct settings_option *option) +{ + const char *optionname; + json_object *json_option; + int intval; + bool boolval; + char *strval; + + + optionname = option->name; + + CHECK_NULL(L_WEB, json_option = json_object_new_object()); + json_object_object_add(json_option, "name", json_object_new_string(option->name)); + json_object_object_add(json_option, "type", json_object_new_int(option->type)); + + if (option->type == SETTINGS_TYPE_INT) + { + intval = settings_option_getint(option); + json_object_object_add(json_option, "value", json_object_new_int(intval)); + } + else if (option->type == SETTINGS_TYPE_BOOL) + { + boolval = settings_option_getbool(option); + json_object_object_add(json_option, "value", json_object_new_boolean(boolval)); + } + else if (option->type == SETTINGS_TYPE_STR) + { + strval = settings_option_getstr(option); + if (strval) + { + json_object_object_add(json_option, "value", json_object_new_string(strval)); + free(strval); + } + } + else + { + DPRINTF(E_LOG, L_WEB, "Option '%s' has unknown type %d\n", optionname, option->type); + jparse_free(json_option); + return NULL; + } + + return json_option; +} + +static json_object * +category_get_json(struct settings_category *category) +{ + json_object *json_category; + json_object *json_options; + json_object *json_option; + struct settings_option *option; + int count; + int i; + + json_category = json_object_new_object(); + + json_object_object_add(json_category, "name", json_object_new_string(category->name)); + + json_options = json_object_new_array(); + + count = settings_option_count(category); + for (i = 0; i < count; i++) + { + option = settings_option_get_byindex(category, i); + json_option = option_get_json(option); + if (json_option) + json_object_array_add(json_options, json_option); + } + + json_object_object_add(json_category, "options", json_options); + + return json_category; +} + +static int +jsonapi_reply_settings_get(struct httpd_request *hreq) +{ + struct settings_category *category; + json_object *jreply; + json_object *json_categories; + json_object *json_category; + int count; + int i; + + CHECK_NULL(L_WEB, jreply = json_object_new_object()); + + json_categories = json_object_new_array(); + + count = settings_categories_count(); + for (i = 0; i < count; i++) + { + category = settings_category_get_byindex(i); + json_category = category_get_json(category); + if (json_category) + json_object_array_add(json_categories, json_category); + } + + json_object_object_add(jreply, "categories", json_categories); + + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + + jparse_free(jreply); + + return HTTP_OK; +} + +static int +jsonapi_reply_settings_category_get(struct httpd_request *hreq) +{ + const char *categoryname; + struct settings_category *category; + json_object *jreply; + + + categoryname = hreq->uri_parsed->path_parts[2]; + + category = settings_category_get(categoryname); + if (!category) + { + DPRINTF(E_LOG, L_WEB, "Invalid category name '%s' given\n", categoryname); + return HTTP_NOTFOUND; + } + + jreply = category_get_json(category); + + if (!jreply) + { + DPRINTF(E_LOG, L_WEB, "Error getting value for category '%s'\n", categoryname); + return HTTP_INTERNAL; + } + + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + + jparse_free(jreply); + + return HTTP_OK; +} + +static int +jsonapi_reply_settings_option_get(struct httpd_request *hreq) +{ + const char *categoryname; + const char *optionname; + struct settings_category *category; + struct settings_option *option; + json_object *jreply; + + + categoryname = hreq->uri_parsed->path_parts[2]; + optionname = hreq->uri_parsed->path_parts[3]; + + category = settings_category_get(categoryname); + if (!category) + { + DPRINTF(E_LOG, L_WEB, "Invalid category name '%s' given\n", categoryname); + return HTTP_NOTFOUND; + } + + option = settings_option_get(category, optionname); + if (!option) + { + DPRINTF(E_LOG, L_WEB, "Invalid option name '%s' given\n", optionname); + return HTTP_NOTFOUND; + } + + jreply = option_get_json(option); + + if (!jreply) + { + DPRINTF(E_LOG, L_WEB, "Error getting value for option '%s'\n", optionname); + return HTTP_INTERNAL; + } + + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); + + jparse_free(jreply); + + return HTTP_OK; +} + +static int +jsonapi_reply_settings_option_put(struct httpd_request *hreq) +{ + const char *categoryname; + const char *optionname; + struct settings_category *category; + struct settings_option *option; + struct evbuffer *in_evbuf; + json_object* request; + int intval; + bool boolval; + const char *strval; + int ret; + + + categoryname = hreq->uri_parsed->path_parts[2]; + optionname = hreq->uri_parsed->path_parts[3]; + + category = settings_category_get(categoryname); + if (!category) + { + DPRINTF(E_LOG, L_WEB, "Invalid category name '%s' given\n", categoryname); + return HTTP_NOTFOUND; + } + + option = settings_option_get(category, optionname); + + if (!option) + { + DPRINTF(E_LOG, L_WEB, "Invalid option name '%s' given\n", optionname); + return HTTP_NOTFOUND; + } + + in_evbuf = evhttp_request_get_input_buffer(hreq->req); + request = jparse_obj_from_evbuffer(in_evbuf); + if (!request) + { + DPRINTF(E_LOG, L_WEB, "Missing request body for setting option '%s' (type %d)\n", optionname, option->type); + return HTTP_BADREQUEST; + } + + if (option->type == SETTINGS_TYPE_INT && jparse_contains_key(request, "value", json_type_int)) + { + intval = jparse_int_from_obj(request, "value"); + ret = settings_option_setint(option, intval); + } + else if (option->type == SETTINGS_TYPE_BOOL && jparse_contains_key(request, "value", json_type_boolean)) + { + boolval = jparse_bool_from_obj(request, "value"); + ret = settings_option_setbool(option, boolval); + } + else if (option->type == SETTINGS_TYPE_STR && jparse_contains_key(request, "value", json_type_string)) + { + strval = jparse_str_from_obj(request, "value"); + ret = settings_option_setstr(option, strval); + } + else + { + DPRINTF(E_LOG, L_WEB, "Invalid value given for option '%s' (type %d): '%s'\n", optionname, option->type, json_object_to_json_string(request)); + return HTTP_BADREQUEST; + } + + if (ret < 0) + { + DPRINTF(E_LOG, L_WEB, "Error changing setting '%s' (type %d) to '%s'\n", optionname, option->type, json_object_to_json_string(request)); + return HTTP_INTERNAL; + } + + DPRINTF(E_INFO, L_WEB, "Setting option '%s.%s' changed to '%s'\n", categoryname, optionname, json_object_to_json_string(request)); + return HTTP_NOCONTENT; +} + /* * Endpoint to retrieve informations about the library * @@ -3541,6 +3795,10 @@ jsonapi_reply_search(struct httpd_request *hreq) static struct httpd_uri_map adm_handlers[] = { { EVHTTP_REQ_GET, "^/api/config$", jsonapi_reply_config }, + { EVHTTP_REQ_GET, "^/api/settings$", jsonapi_reply_settings_get }, + { EVHTTP_REQ_GET, "^/api/settings/[A-Za-z0-9_]+$", jsonapi_reply_settings_category_get }, + { EVHTTP_REQ_GET, "^/api/settings/[A-Za-z0-9_]+/[A-Za-z0-9_]+$", jsonapi_reply_settings_option_get }, + { EVHTTP_REQ_PUT, "^/api/settings/[A-Za-z0-9_]+/[A-Za-z0-9_]+$", jsonapi_reply_settings_option_put }, { EVHTTP_REQ_GET, "^/api/library$", jsonapi_reply_library }, { EVHTTP_REQ_GET | EVHTTP_REQ_PUT, "^/api/update$", jsonapi_reply_update }, diff --git a/src/settings.c b/src/settings.c new file mode 100644 index 00000000..3bdb7244 --- /dev/null +++ b/src/settings.c @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2019 Christian Meffert + * + * 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 "settings.h" + +#include +#include + +#include "db.h" + + + +static struct settings_option webinterface_options[] = + { + { "show_composer_now_playing", SETTINGS_TYPE_BOOL }, + { "show_composer_for_genre", SETTINGS_TYPE_STR }, + }; + +static struct settings_category categories[] = + { + { "webinterface", webinterface_options, ARRAY_SIZE(webinterface_options) }, + }; + + +int +settings_categories_count() +{ + return ARRAY_SIZE(categories); +} + +struct settings_category * +settings_category_get_byindex(int index) +{ + if (index < 0 || settings_categories_count() <= index) + return NULL; + return &categories[index]; +} + +struct settings_category * +settings_category_get(const char *name) +{ + int i; + + for (i = 0; i < settings_categories_count(); i++) + { + if (strcasecmp(name, categories[i].name) == 0) + return &categories[i]; + } + + return NULL; +} + +int +settings_option_count(struct settings_category *category) +{ + return category->count_options; +} + +struct settings_option * +settings_option_get_byindex(struct settings_category *category, int index) +{ + if (index < 0 || !category || category->count_options <= index) + return NULL; + + return &category->options[index]; +} + +struct settings_option * +settings_option_get(struct settings_category *category, const char *name) +{ + int i; + + if (!category || !name) + return NULL; + + for (i = 0; i < category->count_options; i++) + { + if (strcasecmp(name, category->options[i].name) == 0) + return &category->options[i]; + } + + return NULL; +} + +int +settings_option_getint(struct settings_option *option) +{ + if (!option || option->type != SETTINGS_TYPE_INT) + return 0; + + return db_admin_getint(option->name); +} + + +bool +settings_option_getbool(struct settings_option *option) +{ + if (!option || option->type != SETTINGS_TYPE_BOOL) + return false; + + return db_admin_getint(option->name) > 0; +} + +char * +settings_option_getstr(struct settings_option *option) +{ + if (!option || option->type != SETTINGS_TYPE_STR) + return NULL; + + return db_admin_get(option->name); +} + +int +settings_option_setint(struct settings_option *option, int value) +{ + if (!option || option->type != SETTINGS_TYPE_INT) + return -1; + + return db_admin_setint(option->name, value); +} + +int +settings_option_setbool(struct settings_option *option, bool value) +{ + if (!option || option->type != SETTINGS_TYPE_BOOL) + return -1; + + return db_admin_setint(option->name, value); +} + +int +settings_option_setstr(struct settings_option *option, const char *value) +{ + if (!option || option->type != SETTINGS_TYPE_STR) + return -1; + + return db_admin_set(option->name, value); +} diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 00000000..bc4f7b24 --- /dev/null +++ b/src/settings.h @@ -0,0 +1,65 @@ + +#ifndef __SETTINGS_H__ +#define __SETTINGS_H__ + +#include + + +enum settings_type { + SETTINGS_TYPE_INT, + SETTINGS_TYPE_BOOL, + SETTINGS_TYPE_STR, + SETTINGS_TYPE_CATEGORY, +}; + +struct settings_option { + const char *name; + enum settings_type type; +}; + +struct settings_category { + const char *name; + struct settings_option *options; + int count_options; +}; + + +int +settings_categories_count(); + +struct settings_category * +settings_category_get_byindex(int index); + +struct settings_category * +settings_category_get(const char *name); + +int +settings_option_count(struct settings_category *category); + +struct settings_option * +settings_option_get_byindex(struct settings_category *category, int index); + +struct settings_option * +settings_option_get(struct settings_category *category, const char *name); + +int +settings_option_getint(struct settings_option *option); + + +bool +settings_option_getbool(struct settings_option *option); + +char * +settings_option_getstr(struct settings_option *option); + +int +settings_option_setint(struct settings_option *option, int value); + +int +settings_option_setbool(struct settings_option *option, bool value); + +int +settings_option_setstr(struct settings_option *option, const char *value); + + +#endif /* __SETTINGS_H__ */ diff --git a/web-src/src/App.vue b/web-src/src/App.vue index b84c1e18..b6a069bb 100644 --- a/web-src/src/App.vue +++ b/web-src/src/App.vue @@ -106,6 +106,7 @@ export default { vm.update_outputs() vm.update_player_status() vm.update_library_stats() + vm.update_settings() vm.update_queue() vm.update_spotify() } @@ -166,6 +167,12 @@ export default { }) }, + update_settings: function () { + webapi.settings().then(({ data }) => { + this.$store.commit(types.UPDATE_SETTINGS, data) + }) + }, + update_spotify: function () { webapi.spotify().then(({ data }) => { this.$store.commit(types.UPDATE_SPOTIFY, data) diff --git a/web-src/src/components/NavbarTop.vue b/web-src/src/components/NavbarTop.vue index 12c20907..1ce5e267 100644 --- a/web-src/src/components/NavbarTop.vue +++ b/web-src/src/components/NavbarTop.vue @@ -121,17 +121,13 @@ diff --git a/web-src/src/mystyles.css b/web-src/src/mystyles.css index 9a27978e..f72731f0 100644 --- a/web-src/src/mystyles.css +++ b/web-src/src/mystyles.css @@ -144,8 +144,6 @@ section.fd-tabs-section + section.fd-content { } /* Now playing progress bar */ -.fd-progress-now-playing { -} .seek-slider { min-width: 250px; max-width: 500px; diff --git a/web-src/src/pages/PageNowPlaying.vue b/web-src/src/pages/PageNowPlaying.vue index 9537b2da..f312eb80 100644 --- a/web-src/src/pages/PageNowPlaying.vue +++ b/web-src/src/pages/PageNowPlaying.vue @@ -8,6 +8,9 @@

{{ now_playing.artist }}

+

+ {{ composer }} +

{{ now_playing.album }}

@@ -102,12 +105,34 @@ export default { state () { return this.$store.state.player }, + now_playing () { return this.$store.getters.now_playing }, artwork_url: function () { return webapi.artwork_url_append_size_params(this.now_playing.artwork_url) + }, + + settings_option_show_composer_now_playing () { + return this.$store.getters.settings_option_show_composer_now_playing + }, + + settings_option_show_composer_for_genre () { + return this.$store.getters.settings_option_show_composer_for_genre + }, + + composer () { + if (this.settings_option_show_composer_now_playing) { + if (!this.settings_option_show_composer_for_genre || + (this.now_playing.genre && + this.settings_option_show_composer_for_genre.toLowerCase() + .split(',') + .findIndex(elem => this.now_playing.genre.toLowerCase().indexOf(elem.trim()) >= 0) >= 0)) { + return this.now_playing.composer + } + } + return null } }, diff --git a/web-src/src/pages/SettingsPageWebinterface.vue b/web-src/src/pages/SettingsPageWebinterface.vue new file mode 100644 index 00000000..13c09cbc --- /dev/null +++ b/web-src/src/pages/SettingsPageWebinterface.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/web-src/src/router/index.js b/web-src/src/router/index.js index cb1c9682..153067df 100644 --- a/web-src/src/router/index.js +++ b/web-src/src/router/index.js @@ -31,6 +31,7 @@ import SpotifyPageArtist from '@/pages/SpotifyPageArtist' import SpotifyPageAlbum from '@/pages/SpotifyPageAlbum' import SpotifyPagePlaylist from '@/pages/SpotifyPagePlaylist' import SpotifyPageSearch from '@/pages/SpotifyPageSearch' +import SettingsPageWebinterface from '@/pages/SettingsPageWebinterface' Vue.use(VueRouter) @@ -212,6 +213,11 @@ export const router = new VueRouter({ path: '/search/spotify', name: 'Spotify Search', component: SpotifyPageSearch + }, + { + path: '/settings/webinterface', + name: 'Settings Webinterface', + component: SettingsPageWebinterface } ], scrollBehavior (to, from, savedPosition) { diff --git a/web-src/src/store/index.js b/web-src/src/store/index.js index e274d1c2..7a38e9b8 100644 --- a/web-src/src/store/index.js +++ b/web-src/src/store/index.js @@ -11,6 +11,9 @@ export default new Vuex.Store({ 'version': '', 'buildoptions': [ ] }, + settings: { + 'categories': [] + }, library: { 'artists': 0, 'albums': 0, @@ -58,6 +61,33 @@ export default new Vuex.Store({ return item.id === state.player.item_id }) return (item === undefined) ? {} : item + }, + + settings_webinterface: state => { + if (state.settings) { + return state.settings.categories.find(elem => elem.name === 'webinterface') + } + return null + }, + + settings_option_show_composer_now_playing: (state, getters) => { + if (getters.settings_webinterface) { + const option = getters.settings_webinterface.options.find(elem => elem.name === 'show_composer_now_playing') + if (option) { + return option.value + } + } + return false + }, + + settings_option_show_composer_for_genre: (state, getters) => { + if (getters.settings_webinterface) { + const option = getters.settings_webinterface.options.find(elem => elem.name === 'show_composer_for_genre') + if (option) { + return option.value + } + } + return null } }, @@ -65,6 +95,14 @@ export default new Vuex.Store({ [types.UPDATE_CONFIG] (state, config) { state.config = config }, + [types.UPDATE_SETTINGS] (state, settings) { + state.settings = settings + }, + [types.UPDATE_SETTINGS_OPTION] (state, option) { + const settingCategory = state.settings.categories.find(elem => elem.name === option.category) + const settingOption = settingCategory.options.find(elem => elem.name === option.name) + settingOption.value = option.value + }, [types.UPDATE_LIBRARY_STATS] (state, libraryStats) { state.library = libraryStats }, diff --git a/web-src/src/store/mutation_types.js b/web-src/src/store/mutation_types.js index af556d3a..38b02e05 100644 --- a/web-src/src/store/mutation_types.js +++ b/web-src/src/store/mutation_types.js @@ -1,4 +1,6 @@ export const UPDATE_CONFIG = 'UPDATE_CONFIG' +export const UPDATE_SETTINGS = 'UPDATE_SETTINGS' +export const UPDATE_SETTINGS_OPTION = 'UPDATE_SETTINGS_OPTION' export const UPDATE_LIBRARY_STATS = 'UPDATE_LIBRARY_STATS' export const UPDATE_LIBRARY_AUDIOBOOKS_COUNT = 'UPDATE_LIBRARY_AUDIOBOOKS_COUNT' export const UPDATE_LIBRARY_PODCASTS_COUNT = 'UPDATE_LIBRARY_PODCASTS_COUNT' diff --git a/web-src/src/webapi/index.js b/web-src/src/webapi/index.js index 4b41878f..cb4cccd0 100644 --- a/web-src/src/webapi/index.js +++ b/web-src/src/webapi/index.js @@ -13,6 +13,14 @@ export default { return axios.get('/api/config') }, + settings () { + return axios.get('/api/settings') + }, + + settings_update (categoryName, option) { + return axios.put('/api/settings/' + categoryName + '/' + option.name, option) + }, + library_stats () { return axios.get('/api/library') },