/* * Copyright (C) 2017 Christian Meffert * * Adapted from httpd_adm.c: * Copyright (C) 2015 Stuart NAIFEH * * Adapted from httpd_daap.c and httpd.c: * Copyright (C) 2009-2011 Julien BLACHE * Copyright (C) 2010 Kai Elwert * * Adapted from mt-daapd: * Copyright (C) 2003-2007 Ron Pedde * * 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 #endif #include #include #include #include #include #include #include #include #include #include #include "httpd_jsonapi.h" #include "conffile.h" #include "db.h" #ifdef LASTFM # include "lastfm.h" #endif #include "library.h" #include "logger.h" #include "misc.h" #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" # include "spotify.h" #endif static bool allow_modifying_stored_playlists; static char *default_playlist_directory; /* -------------------------------- HELPERS --------------------------------- */ static inline void safe_json_add_string(json_object *obj, const char *key, const char *value) { if (value) json_object_object_add(obj, key, json_object_new_string(value)); } static inline void safe_json_add_string_from_int64(json_object *obj, const char *key, int64_t value) { char tmp[100]; int ret; if (value > 0) { ret = snprintf(tmp, sizeof(tmp), "%" PRIi64, value); if (ret < sizeof(tmp)) json_object_object_add(obj, key, json_object_new_string(tmp)); } } static inline void safe_json_add_int_from_string(json_object *obj, const char *key, const char *value) { int intval; int ret; if (!value) return; ret = safe_atoi32(value, &intval); if (ret == 0) json_object_object_add(obj, key, json_object_new_int(intval)); } static inline void safe_json_add_time_from_string(json_object *obj, const char *key, const char *value, bool with_time) { uint32_t tmp; time_t timestamp; struct tm tm; char result[32]; if (!value) return; if (safe_atou32(value, &tmp) != 0) { DPRINTF(E_LOG, L_WEB, "Error converting timestamp to uint32_t: %s\n", value); return; } if (!tmp) return; timestamp = tmp; if (gmtime_r(×tamp, &tm) == NULL) { DPRINTF(E_LOG, L_WEB, "Error converting timestamp to gmtime: %s\n", value); return; } if (with_time) strftime(result, sizeof(result), "%FT%TZ", &tm); else strftime(result, sizeof(result), "%F", &tm); json_object_object_add(obj, key, json_object_new_string(result)); } static json_object * artist_to_json(struct db_group_info *dbgri) { json_object *item; char uri[100]; char artwork_url[100]; int ret; item = json_object_new_object(); safe_json_add_string(item, "id", dbgri->persistentid); safe_json_add_string(item, "name", dbgri->itemname); safe_json_add_string(item, "name_sort", dbgri->itemname_sort); safe_json_add_int_from_string(item, "album_count", dbgri->groupalbumcount); safe_json_add_int_from_string(item, "track_count", dbgri->itemcount); safe_json_add_int_from_string(item, "length_ms", dbgri->song_length); ret = snprintf(uri, sizeof(uri), "%s:%s:%s", "library", "artist", dbgri->persistentid); if (ret < sizeof(uri)) json_object_object_add(item, "uri", json_object_new_string(uri)); ret = snprintf(artwork_url, sizeof(artwork_url), "/artwork/group/%s", dbgri->id); if (ret < sizeof(artwork_url)) json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url)); return item; } static json_object * album_to_json(struct db_group_info *dbgri) { json_object *item; char uri[100]; char artwork_url[100]; int ret; item = json_object_new_object(); safe_json_add_string(item, "id", dbgri->persistentid); safe_json_add_string(item, "name", dbgri->itemname); safe_json_add_string(item, "name_sort", dbgri->itemname_sort); safe_json_add_string(item, "artist", dbgri->songalbumartist); safe_json_add_string(item, "artist_id", dbgri->songartistid); safe_json_add_int_from_string(item, "track_count", dbgri->itemcount); safe_json_add_int_from_string(item, "length_ms", dbgri->song_length); ret = snprintf(uri, sizeof(uri), "%s:%s:%s", "library", "album", dbgri->persistentid); if (ret < sizeof(uri)) json_object_object_add(item, "uri", json_object_new_string(uri)); ret = snprintf(artwork_url, sizeof(artwork_url), "/artwork/group/%s", dbgri->id); if (ret < sizeof(artwork_url)) json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url)); return item; } static json_object * track_to_json(struct db_media_file_info *dbmfi) { json_object *item; char uri[100]; char artwork_url[100]; int intval; int ret; item = json_object_new_object(); safe_json_add_int_from_string(item, "id", dbmfi->id); safe_json_add_string(item, "title", dbmfi->title); safe_json_add_string(item, "title_sort", dbmfi->title_sort); safe_json_add_string(item, "artist", dbmfi->artist); safe_json_add_string(item, "artist_sort", dbmfi->artist_sort); safe_json_add_string(item, "album", dbmfi->album); safe_json_add_string(item, "album_sort", dbmfi->album_sort); safe_json_add_string(item, "album_id", dbmfi->songalbumid); safe_json_add_string(item, "album_artist", dbmfi->album_artist); safe_json_add_string(item, "album_artist_sort", dbmfi->album_artist_sort); safe_json_add_string(item, "album_artist_id", dbmfi->songartistid); safe_json_add_string(item, "composer", dbmfi->composer); safe_json_add_string(item, "genre", dbmfi->genre); safe_json_add_int_from_string(item, "year", dbmfi->year); safe_json_add_int_from_string(item, "track_number", dbmfi->track); safe_json_add_int_from_string(item, "disc_number", dbmfi->disc); safe_json_add_int_from_string(item, "length_ms", dbmfi->song_length); safe_json_add_int_from_string(item, "rating", dbmfi->rating); safe_json_add_int_from_string(item, "play_count", dbmfi->play_count); safe_json_add_int_from_string(item, "skip_count", dbmfi->skip_count); safe_json_add_time_from_string(item, "time_played", dbmfi->time_played, true); safe_json_add_time_from_string(item, "time_skipped", dbmfi->time_skipped, true); safe_json_add_time_from_string(item, "time_added", dbmfi->time_added, true); safe_json_add_time_from_string(item, "date_released", dbmfi->date_released, false); safe_json_add_int_from_string(item, "seek_ms", dbmfi->seek); safe_json_add_string(item, "type", dbmfi->type); safe_json_add_int_from_string(item, "samplerate", dbmfi->samplerate); safe_json_add_int_from_string(item, "bitrate", dbmfi->bitrate); safe_json_add_int_from_string(item, "channels", dbmfi->channels); ret = safe_atoi32(dbmfi->media_kind, &intval); if (ret == 0) safe_json_add_string(item, "media_kind", db_media_kind_label(intval)); ret = safe_atoi32(dbmfi->data_kind, &intval); if (ret == 0) safe_json_add_string(item, "data_kind", db_data_kind_label(intval)); safe_json_add_string(item, "path", dbmfi->path); ret = snprintf(uri, sizeof(uri), "%s:%s:%s", "library", "track", dbmfi->id); if (ret < sizeof(uri)) json_object_object_add(item, "uri", json_object_new_string(uri)); ret = snprintf(artwork_url, sizeof(artwork_url), "/artwork/item/%s", dbmfi->id); if (ret < sizeof(artwork_url)) json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url)); return item; } static json_object * playlist_to_json(struct db_playlist_info *dbpli) { json_object *item; char uri[100]; int intval; int ret; item = json_object_new_object(); safe_json_add_int_from_string(item, "id", dbpli->id); safe_json_add_string(item, "name", dbpli->title); safe_json_add_string(item, "path", dbpli->path); ret = safe_atoi32(dbpli->type, &intval); if (ret == 0) json_object_object_add(item, "smart_playlist", json_object_new_boolean(intval == PL_SMART)); ret = snprintf(uri, sizeof(uri), "%s:%s:%s", "library", "playlist", dbpli->id); if (ret < sizeof(uri)) json_object_object_add(item, "uri", json_object_new_string(uri)); return item; } static json_object * genre_to_json(const char *genre) { json_object *item; if (genre == NULL) { return NULL; } item = json_object_new_object(); safe_json_add_string(item, "name", genre); return item; } static json_object * directory_to_json(struct directory_info *directory_info) { json_object *item; if (directory_info == NULL) { return NULL; } item = json_object_new_object(); safe_json_add_string(item, "path", directory_info->path); // json_object_object_add(item, "id", json_object_new_int(directory_info->id)); // json_object_object_add(item, "parent_id", json_object_new_int(directory_info->parent_id)); return item; } static int fetch_tracks(struct query_params *query_params, json_object *items, int *total) { struct db_media_file_info dbmfi; json_object *item; int ret; ret = db_query_start(query_params); if (ret < 0) goto error; while (((ret = db_query_fetch_file(query_params, &dbmfi)) == 0) && (dbmfi.id)) { item = track_to_json(&dbmfi); if (!item) { ret = -1; goto error; } json_object_array_add(items, item); } if (total) *total = query_params->results; error: db_query_end(query_params); return ret; } static int fetch_artists(struct query_params *query_params, json_object *items, int *total) { struct db_group_info dbgri; json_object *item; int ret = 0; ret = db_query_start(query_params); if (ret < 0) goto error; while ((ret = db_query_fetch_group(query_params, &dbgri)) == 0) { /* Don't add item if no name (eg blank album name) */ if (strlen(dbgri.itemname) == 0) continue; item = artist_to_json(&dbgri); if (!item) { ret = -1; goto error; } json_object_array_add(items, item); } if (total) *total = query_params->results; error: db_query_end(query_params); return ret; } static json_object * fetch_artist(const char *artist_id) { struct query_params query_params; json_object *artist; struct db_group_info dbgri; int ret = 0; memset(&query_params, 0, sizeof(struct query_params)); artist = NULL; query_params.type = Q_GROUP_ARTISTS; query_params.sort = S_ARTIST; query_params.filter = db_mprintf("(f.songartistid = %s)", artist_id); ret = db_query_start(&query_params); if (ret < 0) goto error; if ((ret = db_query_fetch_group(&query_params, &dbgri)) == 0) { artist = artist_to_json(&dbgri); } error: db_query_end(&query_params); free(query_params.filter); return artist; } static int fetch_albums(struct query_params *query_params, json_object *items, int *total) { struct db_group_info dbgri; json_object *item; int ret = 0; ret = db_query_start(query_params); if (ret < 0) goto error; while ((ret = db_query_fetch_group(query_params, &dbgri)) == 0) { /* Don't add item if no name (eg blank album name) */ if (strlen(dbgri.itemname) == 0) continue; item = album_to_json(&dbgri); if (!item) { ret = -1; goto error; } json_object_array_add(items, item); } if (total) *total = query_params->results; error: db_query_end(query_params); return ret; } static json_object * fetch_album(const char *album_id) { struct query_params query_params; json_object *album; struct db_group_info dbgri; int ret = 0; memset(&query_params, 0, sizeof(struct query_params)); album = NULL; query_params.type = Q_GROUP_ALBUMS; query_params.sort = S_ALBUM; query_params.filter = db_mprintf("(f.songalbumid = %s)", album_id); ret = db_query_start(&query_params); if (ret < 0) goto error; if ((ret = db_query_fetch_group(&query_params, &dbgri)) == 0) { album = album_to_json(&dbgri); } error: db_query_end(&query_params); free(query_params.filter); return album; } static int fetch_playlists(struct query_params *query_params, json_object *items, int *total) { struct db_playlist_info dbpli; json_object *item; int ret = 0; ret = db_query_start(query_params); if (ret < 0) goto error; while (((ret = db_query_fetch_pl(query_params, &dbpli)) == 0) && (dbpli.id)) { item = playlist_to_json(&dbpli); if (!item) { ret = -1; goto error; } json_object_array_add(items, item); } if (total) *total = query_params->results; error: db_query_end(query_params); return ret; } static json_object * fetch_playlist(const char *playlist_id) { struct query_params query_params; json_object *playlist; struct db_playlist_info dbpli; int ret = 0; memset(&query_params, 0, sizeof(struct query_params)); playlist = NULL; query_params.type = Q_PL; query_params.sort = S_PLAYLIST; query_params.filter = db_mprintf("(f.id = %s)", playlist_id); ret = db_query_start(&query_params); if (ret < 0) goto error; if (((ret = db_query_fetch_pl(&query_params, &dbpli)) == 0) && (dbpli.id)) { playlist = playlist_to_json(&dbpli); } error: db_query_end(&query_params); free(query_params.filter); return playlist; } static int fetch_genres(struct query_params *query_params, json_object *items, int *total) { json_object *item; int ret; char *genre; char *sort_item; ret = db_query_start(query_params); if (ret < 0) goto error; while (((ret = db_query_fetch_string_sort(query_params, &genre, &sort_item)) == 0) && (genre)) { item = genre_to_json(genre); if (!item) { ret = -1; goto error; } json_object_array_add(items, item); } if (total) *total = query_params->results; error: db_query_end(query_params); return ret; } static int fetch_directories(int parent_id, json_object *items) { json_object *item; int ret; struct directory_info subdir; struct directory_enum dir_enum; memset(&dir_enum, 0, sizeof(struct directory_enum)); dir_enum.parent_id = parent_id; ret = db_directory_enum_start(&dir_enum); if (ret < 0) goto error; while ((ret = db_directory_enum_fetch(&dir_enum, &subdir)) == 0 && subdir.id > 0) { item = directory_to_json(&subdir); if (!item) { ret = -1; goto error; } json_object_array_add(items, item); } error: db_directory_enum_end(&dir_enum); return ret; } static int query_params_limit_set(struct query_params *query_params, struct httpd_request *hreq) { const char *param; query_params->idx_type = I_NONE; query_params->limit = -1; query_params->offset = 0; param = evhttp_find_header(hreq->query, "limit"); if (param) { query_params->idx_type = I_SUB; if (safe_atoi32(param, &query_params->limit) < 0) { DPRINTF(E_LOG, L_WEB, "Invalid value for query parameter 'limit' (%s)\n", param); return -1; } param = evhttp_find_header(hreq->query, "offset"); if (param && safe_atoi32(param, &query_params->offset) < 0) { DPRINTF(E_LOG, L_WEB, "Invalid value for query parameter 'offset' (%s)\n", param); return -1; } } return 0; } /* --------------------------- REPLY HANDLERS ------------------------------- */ /* * Endpoint to retrieve configuration values * * Example response: * * { * "websocket_port": 6603, * "version": "25.0" * } */ static int jsonapi_reply_config(struct httpd_request *hreq) { json_object *jreply; json_object *buildopts; int websocket_port; char **buildoptions; cfg_t *lib; int ndirs; char *path; char *deref; json_object *directories; int i; CHECK_NULL(L_WEB, jreply = json_object_new_object()); // library name json_object_object_add(jreply, "library_name", json_object_new_string(cfg_getstr(cfg_getsec(cfg, "library"), "name"))); // hide singles json_object_object_add(jreply, "hide_singles", json_object_new_boolean(cfg_getbool(cfg_getsec(cfg, "library"), "hide_singles"))); // Websocket port #ifdef HAVE_LIBWEBSOCKETS websocket_port = cfg_getint(cfg_getsec(cfg, "general"), "websocket_port"); #else websocket_port = 0; #endif json_object_object_add(jreply, "websocket_port", json_object_new_int(websocket_port)); // forked-daapd version json_object_object_add(jreply, "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(jreply, "buildoptions", buildopts); // Library directories lib = cfg_getsec(cfg, "library"); ndirs = cfg_size(lib, "directories"); directories = json_object_new_array(); for (i = 0; i < ndirs; i++) { path = cfg_getnstr(lib, "directories", i); // The path in the conf file may have a trailing slash character. Return the realpath like it is done in the bulk_scan function in filescanner.c deref = realpath(path, NULL); if (deref) { json_object_array_add(directories, json_object_new_string(deref)); free(deref); } else { DPRINTF(E_LOG, L_WEB, "Skipping library directory %s, could not dereference: %s\n", path, strerror(errno)); } } json_object_object_add(jreply, "directories", directories); // Config for creating/modifying stored playlists json_object_object_add(jreply, "allow_modifying_stored_playlists", json_object_new_boolean(allow_modifying_stored_playlists)); safe_json_add_string(jreply, "default_playlist_directory", default_playlist_directory); CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); 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 * * Example response: * * { * "artists": 84, * "albums": 151, * "songs": 3085, * "db_playtime": 687824, * "updating": false *} */ static int jsonapi_reply_library(struct httpd_request *hreq) { struct query_params qp; struct filecount_info fci; json_object *jreply; int ret; char *s; CHECK_NULL(L_WEB, jreply = json_object_new_object()); memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_COUNT_ITEMS; ret = db_filecount_get(&fci, &qp); if (ret == 0) { json_object_object_add(jreply, "songs", json_object_new_int(fci.count)); json_object_object_add(jreply, "db_playtime", json_object_new_int64((fci.length / 1000))); json_object_object_add(jreply, "artists", json_object_new_int(fci.artist_count)); json_object_object_add(jreply, "albums", json_object_new_int(fci.album_count)); } else { DPRINTF(E_LOG, L_WEB, "library: failed to get file count info\n"); } safe_json_add_time_from_string(jreply, "started_at", (s = db_admin_get(DB_ADMIN_START_TIME)), true); free(s); safe_json_add_time_from_string(jreply, "updated_at", (s = db_admin_get(DB_ADMIN_DB_UPDATE)), true); free(s); json_object_object_add(jreply, "updating", json_object_new_boolean(library_is_scanning())); CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); return HTTP_OK; } /* * Endpoint to trigger a library rescan */ static int jsonapi_reply_update(struct httpd_request *hreq) { library_rescan(); return HTTP_NOCONTENT; } static int jsonapi_reply_meta_rescan(struct httpd_request *hreq) { library_metarescan(); return HTTP_NOCONTENT; } /* * 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 httpd_request *hreq) { json_object *jreply; CHECK_NULL(L_WEB, jreply = json_object_new_object()); #ifdef HAVE_SPOTIFY_H int httpd_port; char redirect_uri[256]; char *oauth_uri; struct spotify_status_info info; struct spotifywebapi_status_info webapi_info; struct spotifywebapi_access_token webapi_token; json_object_object_add(jreply, "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 (!oauth_uri) { DPRINTF(E_LOG, L_WEB, "Cannot display Spotify oauth interface (http_form_uriencode() failed)\n"); jparse_free(jreply); return HTTP_INTERNAL; } json_object_object_add(jreply, "oauth_uri", json_object_new_string(oauth_uri)); free(oauth_uri); spotify_status_info_get(&info); json_object_object_add(jreply, "libspotify_installed", json_object_new_boolean(info.libspotify_installed)); json_object_object_add(jreply, "libspotify_logged_in", json_object_new_boolean(info.libspotify_logged_in)); safe_json_add_string(jreply, "libspotify_user", info.libspotify_user); spotifywebapi_status_info_get(&webapi_info); json_object_object_add(jreply, "webapi_token_valid", json_object_new_boolean(webapi_info.token_valid)); safe_json_add_string(jreply, "webapi_user", webapi_info.user); safe_json_add_string(jreply, "webapi_country", webapi_info.country); safe_json_add_string(jreply, "webapi_granted_scope", webapi_info.granted_scope); safe_json_add_string(jreply, "webapi_required_scope", webapi_info.required_scope); spotifywebapi_access_token_get(&webapi_token); safe_json_add_string(jreply, "webapi_token", webapi_token.token); json_object_object_add(jreply, "webapi_token_expires_in", json_object_new_int(webapi_token.expires_in)); #else json_object_object_add(jreply, "enabled", json_object_new_boolean(false)); #endif 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_spotify_login(struct httpd_request *hreq) { #ifdef HAVE_SPOTIFY_H struct evbuffer *in_evbuf; json_object* request; const char *user; const char *password; char *errmsg = NULL; json_object* jreply; json_object* errors; int ret; DPRINTF(E_DBG, L_WEB, "Received Spotify login request\n"); in_evbuf = evhttp_request_get_input_buffer(hreq->req); request = jparse_obj_from_evbuffer(in_evbuf); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); return HTTP_BADREQUEST; } CHECK_NULL(L_WEB, jreply = 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(jreply, "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(jreply, "errors", errors); } else { json_object_object_add(jreply, "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(jreply, "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(jreply, "errors", errors); } CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); #else DPRINTF(E_LOG, L_WEB, "Received spotify login request but was not compiled with enable-spotify\n"); #endif return HTTP_OK; } static int jsonapi_reply_lastfm(struct httpd_request *hreq) { json_object *jreply; bool enabled = false; bool scrobbling_enabled = false; #ifdef LASTFM enabled = true; scrobbling_enabled = lastfm_is_enabled(); #endif CHECK_NULL(L_WEB, jreply = json_object_new_object()); json_object_object_add(jreply, "enabled", json_object_new_boolean(enabled)); json_object_object_add(jreply, "scrobbling_enabled", json_object_new_boolean(scrobbling_enabled)); CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); return HTTP_OK; } /* * Endpoint to log into LastFM */ static int jsonapi_reply_lastfm_login(struct httpd_request *hreq) { #ifdef LASTFM struct evbuffer *in_evbuf; json_object *request; const char *user; const char *password; char *errmsg = NULL; json_object *jreply; json_object *errors; int ret; DPRINTF(E_DBG, L_WEB, "Received LastFM login request\n"); in_evbuf = evhttp_request_get_input_buffer(hreq->req); request = jparse_obj_from_evbuffer(in_evbuf); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); return HTTP_BADREQUEST; } CHECK_NULL(L_WEB, jreply = 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 = lastfm_login_user(user, password, &errmsg); if (ret < 0) { json_object_object_add(jreply, "success", json_object_new_boolean(false)); errors = json_object_new_object(); if (errmsg) json_object_object_add(errors, "error", json_object_new_string(errmsg)); else json_object_object_add(errors, "error", json_object_new_string("Unknown error")); json_object_object_add(jreply, "errors", errors); } else { json_object_object_add(jreply, "success", json_object_new_boolean(true)); } free(errmsg); } else { DPRINTF(E_LOG, L_WEB, "No user or password in LastFM login post request\n"); json_object_object_add(jreply, "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(jreply, "errors", errors); } CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); #else DPRINTF(E_LOG, L_WEB, "Received LastFM login request but was not compiled with enable-lastfm\n"); #endif return HTTP_OK; } static int jsonapi_reply_lastfm_logout(struct httpd_request *hreq) { #ifdef LASTFM lastfm_logout(); #endif return HTTP_NOCONTENT; } /* * Kicks off pairing of a daap/dacp client * * Expects the paring pin to be present in the post request body, e. g.: * * { * "pin": "1234" * } */ static int jsonapi_reply_pairing_kickoff(struct httpd_request *hreq) { struct evbuffer *evbuf; json_object* request; const char* message; evbuf = evhttp_request_get_input_buffer(hreq->req); request = jparse_obj_from_evbuffer(evbuf); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); return HTTP_BADREQUEST; } 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 HTTP_NOCONTENT; } /* * Retrieves pairing information * * Example response: * * { * "active": true, * "remote": "remote name" * } */ static int jsonapi_reply_pairing_get(struct httpd_request *hreq) { char *remote_name; json_object *jreply; remote_name = remote_pairing_get_name(); CHECK_NULL(L_WEB, jreply = json_object_new_object()); if (remote_name) { json_object_object_add(jreply, "active", json_object_new_boolean(true)); json_object_object_add(jreply, "remote", json_object_new_string(remote_name)); } else { json_object_object_add(jreply, "active", json_object_new_boolean(false)); } CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); free(remote_name); return HTTP_OK; } struct outputs_param { json_object *output; uint64_t output_id; int output_volume; }; static json_object * speaker_to_json(struct player_speaker_info *spk) { json_object *output; char output_id[21]; output = json_object_new_object(); snprintf(output_id, sizeof(output_id), "%" PRIu64, spk->id); json_object_object_add(output, "id", json_object_new_string(output_id)); json_object_object_add(output, "name", json_object_new_string(spk->name)); json_object_object_add(output, "type", json_object_new_string(spk->output_type)); json_object_object_add(output, "selected", json_object_new_boolean(spk->selected)); json_object_object_add(output, "has_password", json_object_new_boolean(spk->has_password)); json_object_object_add(output, "requires_auth", json_object_new_boolean(spk->requires_auth)); json_object_object_add(output, "needs_auth_key", json_object_new_boolean(spk->needs_auth_key)); json_object_object_add(output, "volume", json_object_new_int(spk->absvol)); return output; } static void speaker_enum_cb(struct player_speaker_info *spk, void *arg) { json_object *outputs; json_object *output; outputs = arg; output = speaker_to_json(spk); json_object_array_add(outputs, output); } /* * GET /api/outputs/[output_id] */ static int jsonapi_reply_outputs_get_byid(struct httpd_request *hreq) { struct player_speaker_info speaker_info; uint64_t output_id; json_object *jreply; int ret; ret = safe_atou64(hreq->uri_parsed->path_parts[2], &output_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->uri_parsed->path); return HTTP_BADREQUEST; } ret = player_speaker_get_byid(output_id, &speaker_info); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No output found for '%s'\n", hreq->uri_parsed->path); return HTTP_BADREQUEST; } jreply = speaker_to_json(&speaker_info); CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); return HTTP_OK; } /* * PUT /api/outputs/[output_id] */ static int jsonapi_reply_outputs_put_byid(struct httpd_request *hreq) { uint64_t output_id; struct evbuffer *in_evbuf; json_object* request; bool selected; int volume; int ret; ret = safe_atou64(hreq->uri_parsed->path_parts[2], &output_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->uri_parsed->path); return HTTP_BADREQUEST; } in_evbuf = evhttp_request_get_input_buffer(hreq->req); request = jparse_obj_from_evbuffer(in_evbuf); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); return HTTP_BADREQUEST; } ret = 0; if (jparse_contains_key(request, "selected", json_type_boolean)) { selected = jparse_bool_from_obj(request, "selected"); if (selected) ret = player_speaker_enable(output_id); else ret = player_speaker_disable(output_id); } if (ret == 0 && jparse_contains_key(request, "volume", json_type_int)) { volume = jparse_int_from_obj(request, "volume"); ret = player_volume_setabs_speaker(output_id, volume); } jparse_free(request); if (ret < 0) return HTTP_INTERNAL; return HTTP_NOCONTENT; } /* * PUT /api/outputs/[output_id]/toggle */ static int jsonapi_reply_outputs_toggle_byid(struct httpd_request *hreq) { uint64_t output_id; struct player_speaker_info spk; int ret; ret = safe_atou64(hreq->uri_parsed->path_parts[2], &output_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No valid output id given to outputs endpoint '%s'\n", hreq->uri_parsed->path); return HTTP_BADREQUEST; } ret = player_speaker_get_byid(output_id, &spk); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No output found for the given output id, toggle failed for '%s'\n", hreq->uri_parsed->path); return HTTP_BADREQUEST; } if (spk.selected) ret = player_speaker_disable(output_id); else ret = player_speaker_enable(output_id); if (ret < 0) return HTTP_INTERNAL; return HTTP_NOCONTENT; } /* * Endpoint "/api/outputs" */ static int jsonapi_reply_outputs(struct httpd_request *hreq) { json_object *outputs; json_object *jreply; outputs = json_object_new_array(); player_speaker_enumerate(speaker_enum_cb, outputs); jreply = json_object_new_object(); json_object_object_add(jreply, "outputs", outputs); 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_verification(struct httpd_request *hreq) { struct evbuffer *in_evbuf; json_object* request; const char* message; in_evbuf = evhttp_request_get_input_buffer(hreq->req); request = jparse_obj_from_evbuffer(in_evbuf); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); return HTTP_BADREQUEST; } DPRINTF(E_DBG, L_WEB, "Received verification post request: %s\n", json_object_to_json_string(request)); message = jparse_str_from_obj(request, "pin"); if (message) player_raop_verification_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 HTTP_NOCONTENT; } static int jsonapi_reply_outputs_set(struct httpd_request *hreq) { struct evbuffer *in_evbuf; json_object *request; json_object *outputs; json_object *output_id; int nspk, i, ret; uint64_t *ids; in_evbuf = evhttp_request_get_input_buffer(hreq->req); request = jparse_obj_from_evbuffer(in_evbuf); if (!request) { DPRINTF(E_LOG, L_WEB, "Failed to parse incoming request\n"); return HTTP_BADREQUEST; } DPRINTF(E_DBG, L_WEB, "Received select-outputs post request: %s\n", json_object_to_json_string(request)); ret = jparse_array_from_obj(request, "outputs", &outputs); if (ret == 0) { nspk = json_object_array_length(outputs); ids = calloc((nspk + 1), sizeof(uint64_t)); ids[0] = nspk; ret = 0; for (i = 0; i < nspk; i++) { output_id = json_object_array_get_idx(outputs, i); ret = safe_atou64(json_object_get_string(output_id), &ids[i + 1]); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Failed to convert output id: %s\n", json_object_to_json_string(request)); break; } } if (ret == 0) player_speaker_set(ids); free(ids); } else DPRINTF(E_LOG, L_WEB, "Missing outputs in request body: %s\n", json_object_to_json_string(request)); jparse_free(request); return HTTP_NOCONTENT; } static int play_item_with_id(const char *param) { uint32_t item_id; struct db_queue_item *queue_item; int ret; ret = safe_atou32(param, &item_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No valid item id given '%s'\n", param); return HTTP_BADREQUEST; } queue_item = db_queue_fetch_byitemid(item_id); if (!queue_item) { DPRINTF(E_LOG, L_WEB, "No queue item with item id '%d'\n", item_id); return HTTP_BADREQUEST; } player_playback_stop(); ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Failed to start playback from item with id '%d'\n", item_id); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int play_item_at_position(const char *param) { uint32_t position; struct player_status status; struct db_queue_item *queue_item; int ret; ret = safe_atou32(param, &position); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No valid position given '%s'\n", param); return HTTP_BADREQUEST; } player_get_status(&status); queue_item = db_queue_fetch_bypos(position, status.shuffle); if (!queue_item) { DPRINTF(E_LOG, L_WEB, "No queue item at position '%d'\n", position); return HTTP_BADREQUEST; } player_playback_stop(); ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Failed to start playback from position '%d'\n", position); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_player_play(struct httpd_request *hreq) { const char *param; int ret; if ((param = evhttp_find_header(hreq->query, "item_id"))) { return play_item_with_id(param); } else if ((param = evhttp_find_header(hreq->query, "position"))) { return play_item_at_position(param); } ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error starting playback.\n"); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_player_pause(struct httpd_request *hreq) { int ret; ret = player_playback_pause(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error pausing playback.\n"); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_player_stop(struct httpd_request *hreq) { int ret; ret = player_playback_stop(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error stopping playback.\n"); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_player_toggle(struct httpd_request *hreq) { struct player_status status; int ret; player_get_status(&status); DPRINTF(E_DBG, L_WEB, "Toggle playback request with current state %d.\n", status.status); if (status.status == PLAY_PLAYING) { ret = player_playback_pause(); } else { ret = player_playback_start(); } if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error toggling playback state.\n"); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_player_next(struct httpd_request *hreq) { int ret; ret = player_playback_next(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error switching to next item.\n"); return HTTP_INTERNAL; } ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error starting playback after switching to next item.\n"); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_player_previous(struct httpd_request *hreq) { int ret; ret = player_playback_prev(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error switching to previous item.\n"); return HTTP_INTERNAL; } ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error starting playback after switching to previous item.\n"); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_player_seek(struct httpd_request *hreq) { const char *param_pos; const char *param_seek; int position_ms; int seek_ms; int ret; param_pos = evhttp_find_header(hreq->query, "position_ms"); param_seek = evhttp_find_header(hreq->query, "seek_ms"); if (!param_pos && !param_seek) return HTTP_BADREQUEST; if (param_pos) { ret = safe_atoi32(param_pos, &position_ms); if (ret < 0) return HTTP_BADREQUEST; ret = player_playback_seek(position_ms, PLAYER_SEEK_POSITION); } else { ret = safe_atoi32(param_seek, &seek_ms); if (ret < 0) return HTTP_BADREQUEST; ret = player_playback_seek(seek_ms, PLAYER_SEEK_RELATIVE); } if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error seeking (position_ms=%s, seek_ms=%s).\n", (param_pos ? param_pos : ""), (param_seek ? param_seek : "")); return HTTP_INTERNAL; } ret = player_playback_start(); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Error starting playback after seeking (position_ms=%s, seek_ms=%s).\n", (param_pos ? param_pos : ""), (param_seek ? param_seek : "")); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_player(struct httpd_request *hreq) { struct player_status status; struct db_queue_item *queue_item; json_object *reply; player_get_status(&status); reply = json_object_new_object(); switch (status.status) { case PLAY_PAUSED: json_object_object_add(reply, "state", json_object_new_string("pause")); break; case PLAY_PLAYING: json_object_object_add(reply, "state", json_object_new_string("play")); break; default: json_object_object_add(reply, "state", json_object_new_string("stop")); break; } switch (status.repeat) { case REPEAT_SONG: json_object_object_add(reply, "repeat", json_object_new_string("single")); break; case REPEAT_ALL: json_object_object_add(reply, "repeat", json_object_new_string("all")); break; default: json_object_object_add(reply, "repeat", json_object_new_string("off")); break; } json_object_object_add(reply, "consume", json_object_new_boolean(status.consume)); json_object_object_add(reply, "shuffle", json_object_new_boolean(status.shuffle)); json_object_object_add(reply, "volume", json_object_new_int(status.volume)); if (status.item_id) { json_object_object_add(reply, "item_id", json_object_new_int(status.item_id)); json_object_object_add(reply, "item_length_ms", json_object_new_int(status.len_ms)); json_object_object_add(reply, "item_progress_ms", json_object_new_int(status.pos_ms)); json_object_object_add(reply, "artwork_url", json_object_new_string("/artwork/nowplaying")); } else { queue_item = db_queue_fetch_bypos(0, status.shuffle); if (queue_item) { json_object_object_add(reply, "item_id", json_object_new_int(queue_item->id)); json_object_object_add(reply, "item_length_ms", json_object_new_int(queue_item->song_length)); json_object_object_add(reply, "item_progress_ms", json_object_new_int(0)); free_queue_item(queue_item, 0); } else { json_object_object_add(reply, "item_id", json_object_new_int(0)); json_object_object_add(reply, "item_length_ms", json_object_new_int(0)); json_object_object_add(reply, "item_progress_ms", json_object_new_int(0)); } } CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply))); jparse_free(reply); return HTTP_OK; } static json_object * queue_item_to_json(struct db_queue_item *queue_item, char shuffle) { json_object *item; char uri[100]; char artwork_url[100]; char chbuf[6]; const char *ch; int ret; item = json_object_new_object(); json_object_object_add(item, "id", json_object_new_int(queue_item->id)); if (shuffle) json_object_object_add(item, "position", json_object_new_int(queue_item->shuffle_pos)); else json_object_object_add(item, "position", json_object_new_int(queue_item->pos)); if (queue_item->file_id > 0 && queue_item->file_id != DB_MEDIA_FILE_NON_PERSISTENT_ID) json_object_object_add(item, "track_id", json_object_new_int(queue_item->file_id)); safe_json_add_string(item, "title", queue_item->title); safe_json_add_string(item, "artist", queue_item->artist); safe_json_add_string(item, "artist_sort", queue_item->artist_sort); safe_json_add_string(item, "album", queue_item->album); safe_json_add_string(item, "album_sort", queue_item->album_sort); safe_json_add_string_from_int64(item, "album_id", queue_item->songalbumid); safe_json_add_string(item, "album_artist", queue_item->album_artist); safe_json_add_string(item, "album_artist_sort", queue_item->album_artist_sort); safe_json_add_string_from_int64(item, "album_artist_id", queue_item->songartistid); safe_json_add_string(item, "composer", queue_item->composer); safe_json_add_string(item, "genre", queue_item->genre); json_object_object_add(item, "year", json_object_new_int(queue_item->year)); json_object_object_add(item, "track_number", json_object_new_int(queue_item->track)); json_object_object_add(item, "disc_number", json_object_new_int(queue_item->disc)); json_object_object_add(item, "length_ms", json_object_new_int(queue_item->song_length)); safe_json_add_string(item, "media_kind", db_media_kind_label(queue_item->media_kind)); safe_json_add_string(item, "data_kind", db_data_kind_label(queue_item->data_kind)); safe_json_add_string(item, "path", queue_item->path); if (queue_item->file_id > 0 && queue_item->file_id != DB_MEDIA_FILE_NON_PERSISTENT_ID) { ret = snprintf(uri, sizeof(uri), "%s:%s:%d", "library", "track", queue_item->file_id); if (ret < sizeof(uri)) json_object_object_add(item, "uri", json_object_new_string(uri)); } else { safe_json_add_string(item, "uri", queue_item->path); } if (queue_item->artwork_url && (strncmp(queue_item->artwork_url, "http://", strlen("http://")) == 0 || strncmp(queue_item->artwork_url, "https://", strlen("https://")) == 0)) { // The queue item contains a valid http url for an artwork image, there is no need // for the client to request the image through the forked-daapd artwork handler. // Directly pass the artwork url to the client. safe_json_add_string(item, "artwork_url", queue_item->artwork_url); } else if (queue_item->file_id > 0 && queue_item->file_id != DB_MEDIA_FILE_NON_PERSISTENT_ID) { if (queue_item->data_kind != DATA_KIND_PIPE) { // Queue item does not have a valid artwork url, construct artwork url to // get the image through the httpd_artworkapi (uses the artwork handlers). ret = snprintf(artwork_url, sizeof(artwork_url), "/artwork/item/%d", queue_item->file_id); if (ret < sizeof(artwork_url)) json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url)); } else { // Pipe metadata can change if the queue version changes. Construct artwork url // similar to non-pipe items, but append the queue version to the url to force // clients to reload image if the queue version changes (additional metadata was found). ret = snprintf(artwork_url, sizeof(artwork_url), "/artwork/item/%d?v=%d", queue_item->file_id, queue_item->queue_version); if (ret < sizeof(artwork_url)) json_object_object_add(item, "artwork_url", json_object_new_string(artwork_url)); } } safe_json_add_string(item, "type", queue_item->type); json_object_object_add(item, "bitrate", json_object_new_int(queue_item->bitrate)); json_object_object_add(item, "samplerate", json_object_new_int(queue_item->samplerate)); switch (queue_item->channels) { case 1: ch = "mono"; break; case 2: ch = "stereo"; break; default: snprintf(chbuf, sizeof(chbuf), "%d ch", queue_item->channels); ch = chbuf; } safe_json_add_string(item, "channels", ch); return item; } static int queue_tracks_add_artist(const char *id, int pos) { struct query_params query_params; struct player_status status; int count = 0; int ret = 0; memset(&query_params, 0, sizeof(struct query_params)); query_params.type = Q_ITEMS; query_params.sort = S_ALBUM; query_params.idx_type = I_NONE; query_params.filter = db_mprintf("(f.songartistid = %q)", id); player_get_status(&status); ret = db_queue_add_by_query(&query_params, status.shuffle, status.item_id, pos, &count, NULL); free(query_params.filter); if (ret == 0) return count; return ret; } static int queue_tracks_add_album(const char *id, int pos) { struct query_params query_params; struct player_status status; int count = 0; int ret = 0; memset(&query_params, 0, sizeof(struct query_params)); query_params.type = Q_ITEMS; query_params.sort = S_ALBUM; query_params.idx_type = I_NONE; query_params.filter = db_mprintf("(f.songalbumid = %q)", id); player_get_status(&status); ret = db_queue_add_by_query(&query_params, status.shuffle, status.item_id, pos, &count, NULL); free(query_params.filter); if (ret == 0) return count; return ret; } static int queue_tracks_add_track(const char *id, int pos) { struct query_params query_params; struct player_status status; int count = 0; int ret = 0; memset(&query_params, 0, sizeof(struct query_params)); query_params.type = Q_ITEMS; query_params.sort = S_ALBUM; query_params.idx_type = I_NONE; query_params.filter = db_mprintf("(f.id = %q)", id); player_get_status(&status); ret = db_queue_add_by_query(&query_params, status.shuffle, status.item_id, pos, &count, NULL); free(query_params.filter); if (ret == 0) return count; return ret; } static int queue_tracks_add_playlist(const char *id, int pos) { struct player_status status; int playlist_id; int count = 0; int ret; ret = safe_atoi32(id, &playlist_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No valid playlist id given '%s'\n", id); return HTTP_BADREQUEST; } player_get_status(&status); ret = db_queue_add_by_playlistid(playlist_id, status.shuffle, status.item_id, pos, &count, NULL); if (ret == 0) return count; return ret; } static int queue_tracks_add_byuris(const char *param, int pos, int *total_count) { char *uris; char *uri; char *ptr; const char *id; int count = 0; int ret = 0; *total_count = 0; CHECK_NULL(L_WEB, uris = strdup(param)); uri = strtok_r(uris, ",", &ptr); if (!uri) { DPRINTF(E_LOG, L_WEB, "Empty query parameter 'uris'\n"); free(uris); return -1; } do { count = 0; if (strncmp(uri, "library:artist:", strlen("library:artist:")) == 0) { id = uri + (strlen("library:artist:")); count = queue_tracks_add_artist(id, pos); } else if (strncmp(uri, "library:album:", strlen("library:album:")) == 0) { id = uri + (strlen("library:album:")); count = queue_tracks_add_album(id, pos); } else if (strncmp(uri, "library:track:", strlen("library:track:")) == 0) { id = uri + (strlen("library:track:")); count = queue_tracks_add_track(id, pos); } else if (strncmp(uri, "library:playlist:", strlen("library:playlist:")) == 0) { id = uri + (strlen("library:playlist:")); count = queue_tracks_add_playlist(id, pos); } else { ret = library_queue_add(uri, pos, &count, NULL); if (ret != LIBRARY_OK) { DPRINTF(E_LOG, L_WEB, "Invalid uri '%s'\n", uri); break; } pos += count; } if (pos >= 0) pos += count; *total_count += count; } while ((uri = strtok_r(NULL, ",", &ptr))); free(uris); return ret; } static int queue_tracks_add_byexpression(const char *param, int pos, int *total_count) { char *expression; struct smartpl smartpl_expression; struct query_params query_params; struct player_status status; int ret; memset(&query_params, 0, sizeof(struct query_params)); query_params.type = Q_ITEMS; query_params.sort = S_NAME; query_params.idx_type = I_NONE; memset(&smartpl_expression, 0, sizeof(struct smartpl)); expression = safe_asprintf("\"query\" { %s }", param); ret = smartpl_query_parse_string(&smartpl_expression, expression); free(expression); if (ret < 0) return -1; query_params.filter = strdup(smartpl_expression.query_where); query_params.order = safe_strdup(smartpl_expression.order); free_smartpl(&smartpl_expression, 1); player_get_status(&status); ret = db_queue_add_by_query(&query_params, status.shuffle, status.item_id, pos, total_count, NULL); free_query_params(&query_params, 1); return ret; } static int jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) { const char *param_pos; const char *param_uris; const char *param_expression; const char *param; int pos = -1; bool shuffle; int total_count = 0; json_object *reply; int ret = 0; param_pos = evhttp_find_header(hreq->query, "position"); if (param_pos) { if (safe_atoi32(param_pos, &pos) < 0) { DPRINTF(E_LOG, L_WEB, "Invalid position parameter '%s'\n", param_pos); return HTTP_BADREQUEST; } DPRINTF(E_DBG, L_WEB, "Add tracks starting at position '%d\n", pos); } param_uris = evhttp_find_header(hreq->query, "uris"); param_expression = evhttp_find_header(hreq->query, "expression"); if (!param_uris && !param_expression) { DPRINTF(E_LOG, L_WEB, "Missing query parameter 'uris' or 'expression'\n"); return HTTP_BADREQUEST; } // if query parameter "clear" is "true", stop playback and clear the queue before adding new queue items param = evhttp_find_header(hreq->query, "clear"); if (param && strcmp(param, "true") == 0) { player_playback_stop(); db_queue_clear(0); } // if query parameter "shuffle" is present, update the shuffle state before adding new queue items param = evhttp_find_header(hreq->query, "shuffle"); if (param) { shuffle = (strcmp(param, "true") == 0); player_shuffle_set(shuffle); } if (param_uris) { ret = queue_tracks_add_byuris(param_uris, pos, &total_count); } else { ret = queue_tracks_add_byexpression(param_expression, pos, &total_count); } if (ret == 0) { reply = json_object_new_object(); json_object_object_add(reply, "count", json_object_new_int(total_count)); ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); jparse_free(reply); } if (ret < 0) return HTTP_INTERNAL; // If query parameter "playback" is "start", start playback after successfully adding new items param = evhttp_find_header(hreq->query, "playback"); if (param && strcmp(param, "start") == 0) { if ((param = evhttp_find_header(hreq->query, "playback_from_position"))) play_item_at_position(param); else player_playback_start(); } return HTTP_OK; } static int jsonapi_reply_queue_tracks_move(struct httpd_request *hreq) { uint32_t item_id; uint32_t new_position; const char *param; struct player_status status; int ret; ret = safe_atou32(hreq->uri_parsed->path_parts[3], &item_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No valid item id given '%s'\n", hreq->uri_parsed->path); return HTTP_BADREQUEST; } param = evhttp_find_header(hreq->query, "new_position"); if (!param) { DPRINTF(E_LOG, L_WEB, "Missing parameter 'new_position'\n"); return HTTP_BADREQUEST; } if (safe_atou32(param, &new_position) < 0) { DPRINTF(E_LOG, L_WEB, "No valid item new_position '%s'\n", param); return HTTP_BADREQUEST; } player_get_status(&status); ret = db_queue_move_byitemid(item_id, new_position, status.shuffle); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Moving item '%d' to new position %d failed\n", item_id, new_position); return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_queue_tracks_delete(struct httpd_request *hreq) { uint32_t item_id; int ret; ret = safe_atou32(hreq->uri_parsed->path_parts[3], &item_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No valid item id given '%s'\n", hreq->uri_parsed->path); return HTTP_BADREQUEST; } ret = db_queue_delete_byitemid(item_id); if (ret < 0) { return HTTP_INTERNAL; } return HTTP_NOCONTENT; } static int jsonapi_reply_queue_clear(struct httpd_request *hreq) { player_playback_stop(); db_queue_clear(0); return HTTP_NOCONTENT; } static int jsonapi_reply_queue(struct httpd_request *hreq) { struct query_params query_params; const char *param; uint32_t item_id; uint32_t count; int start_pos, end_pos; int version; char etag[21]; struct player_status status; struct db_queue_item queue_item; json_object *reply; json_object *items; json_object *item; int ret = 0; version = db_admin_getint(DB_ADMIN_QUEUE_VERSION); db_queue_get_count(&count); snprintf(etag, sizeof(etag), "%d", version); if (httpd_request_etag_matches(hreq->req, etag)) return HTTP_NOTMODIFIED; memset(&query_params, 0, sizeof(struct query_params)); reply = json_object_new_object(); json_object_object_add(reply, "version", json_object_new_int(version)); json_object_object_add(reply, "count", json_object_new_int((int)count)); items = json_object_new_array(); json_object_object_add(reply, "items", items); player_get_status(&status); if (status.shuffle) query_params.sort = S_SHUFFLE_POS; param = evhttp_find_header(hreq->query, "id"); if (param && safe_atou32(param, &item_id) == 0) { query_params.filter = db_mprintf("id = %d", item_id); } else { param = evhttp_find_header(hreq->query, "start"); if (param && safe_atoi32(param, &start_pos) == 0) { param = evhttp_find_header(hreq->query, "end"); if (!param || safe_atoi32(param, &end_pos) != 0) { end_pos = start_pos + 1; } if (query_params.sort == S_SHUFFLE_POS) query_params.filter = db_mprintf("shuffle_pos >= %d AND shuffle_pos < %d", start_pos, end_pos); else query_params.filter = db_mprintf("pos >= %d AND pos < %d", start_pos, end_pos); } } ret = db_queue_enum_start(&query_params); if (ret < 0) goto db_start_error; while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) { item = queue_item_to_json(&queue_item, status.shuffle); if (!item) goto error; json_object_array_add(items, item); } ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "outputs: Couldn't add outputs to response buffer.\n"); error: db_queue_enum_end(&query_params); db_start_error: jparse_free(reply); free(query_params.filter); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_player_repeat(struct httpd_request *hreq) { const char *param; param = evhttp_find_header(hreq->query, "state"); if (!param) return HTTP_BADREQUEST; if (strcmp(param, "single") == 0) { player_repeat_set(REPEAT_SONG); } else if (strcmp(param, "all") == 0) { player_repeat_set(REPEAT_ALL); } else if (strcmp(param, "off") == 0) { player_repeat_set(REPEAT_OFF); } return HTTP_NOCONTENT; } static int jsonapi_reply_player_shuffle(struct httpd_request *hreq) { const char *param; bool shuffle; param = evhttp_find_header(hreq->query, "state"); if (!param) return HTTP_BADREQUEST; shuffle = (strcmp(param, "true") == 0); player_shuffle_set(shuffle); return HTTP_NOCONTENT; } static int jsonapi_reply_player_consume(struct httpd_request *hreq) { const char *param; bool consume; param = evhttp_find_header(hreq->query, "state"); if (!param) return HTTP_BADREQUEST; consume = (strcmp(param, "true") == 0); player_consume_set(consume); return HTTP_NOCONTENT; } static int volume_set(int volume, int step) { int new_volume; struct player_status status; int ret; new_volume = volume; if (step != 0) { // Calculate new volume from given step value player_get_status(&status); new_volume = status.volume + step; } // Make sure we are setting a correct value new_volume = new_volume > 100 ? 100 : new_volume; new_volume = new_volume < 0 ? 0 : new_volume; ret = player_volume_set(new_volume); return ret; } static int output_volume_set(int volume, int step, uint64_t output_id) { int new_volume; struct player_speaker_info speaker_info; int ret; new_volume = volume; if (step != 0) { // Calculate new output volume from the given step value ret = player_speaker_get_byid(output_id, &speaker_info); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No output found for the given output id .\n"); return -1; } new_volume = speaker_info.absvol + step; } // Make sure we are setting a correct value new_volume = new_volume > 100 ? 100 : new_volume; new_volume = new_volume < 0 ? 0 : new_volume; ret = player_volume_setabs_speaker(output_id, new_volume); return ret; } static int jsonapi_reply_player_volume(struct httpd_request *hreq) { const char *param_volume; const char *param_step; const char *param; uint64_t output_id; int volume; int step; int ret; volume = 0; step = 0; // Parse and validate parameters param_volume = evhttp_find_header(hreq->query, "volume"); if (param_volume) { ret = safe_atoi32(param_volume, &volume); if (ret < 0) return HTTP_BADREQUEST; } param_step = evhttp_find_header(hreq->query, "step"); if (param_step) { ret = safe_atoi32(param_step, &step); if (ret < 0) return HTTP_BADREQUEST; } if ((!param_volume && !param_step) || (param_volume && param_step)) { DPRINTF(E_LOG, L_WEB, "Invalid parameters for player/volume request. Either 'volume' or 'step' parameter required.\n"); return HTTP_BADREQUEST; } param = evhttp_find_header(hreq->query, "output_id"); if (param) { // Update volume for individual output ret = safe_atou64(param, &output_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "Invalid value for parameter 'output_id'. Output id must be an integer (output_id='%s').\n", param); return HTTP_BADREQUEST; } ret = output_volume_set(volume, step, output_id); } else { // Update master volume ret = volume_set(volume, step); } if (ret < 0) return HTTP_INTERNAL; return HTTP_NOCONTENT; } static int jsonapi_reply_library_artists(struct httpd_request *hreq) { time_t db_update; struct query_params query_params; const char *param; enum media_kind media_kind; json_object *reply; json_object *items; int total; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_UPDATE); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; media_kind = 0; param = evhttp_find_header(hreq->query, "media_kind"); if (param) { media_kind = db_media_kind_enum(param); if (!media_kind) { DPRINTF(E_LOG, L_WEB, "Invalid media kind '%s'\n", param); return HTTP_BADREQUEST; } } reply = json_object_new_object(); items = json_object_new_array(); json_object_object_add(reply, "items", items); memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto error; query_params.type = Q_GROUP_ARTISTS; query_params.sort = S_ARTIST; if (media_kind) query_params.filter = db_mprintf("(f.media_kind = %d)", media_kind); ret = fetch_artists(&query_params, items, &total); if (ret < 0) goto error; json_object_object_add(reply, "total", json_object_new_int(total)); json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add artists to response buffer.\n"); error: free_query_params(&query_params, 1); jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_artist(struct httpd_request *hreq) { time_t db_update; const char *artist_id; json_object *reply; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_UPDATE); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; artist_id = hreq->uri_parsed->path_parts[3]; reply = fetch_artist(artist_id); if (!reply) { ret = -1; goto error; } ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add artists to response buffer.\n"); error: jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_artist_albums(struct httpd_request *hreq) { time_t db_update; struct query_params query_params; const char *artist_id; json_object *reply; json_object *items; int total; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_UPDATE); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; artist_id = hreq->uri_parsed->path_parts[3]; reply = json_object_new_object(); items = json_object_new_array(); json_object_object_add(reply, "items", items); memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto error; query_params.type = Q_GROUP_ALBUMS; query_params.sort = S_ALBUM; query_params.filter = db_mprintf("(f.songartistid = %q)", artist_id); ret = fetch_albums(&query_params, items, &total); free(query_params.filter); if (ret < 0) goto error; json_object_object_add(reply, "total", json_object_new_int(total)); json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add albums to response buffer.\n"); error: jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_albums(struct httpd_request *hreq) { time_t db_update; struct query_params query_params; const char *param; enum media_kind media_kind; json_object *reply; json_object *items; int total; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_UPDATE); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; media_kind = 0; param = evhttp_find_header(hreq->query, "media_kind"); if (param) { media_kind = db_media_kind_enum(param); if (!media_kind) { DPRINTF(E_LOG, L_WEB, "Invalid media kind '%s'\n", param); return HTTP_BADREQUEST; } } reply = json_object_new_object(); items = json_object_new_array(); json_object_object_add(reply, "items", items); memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto error; query_params.type = Q_GROUP_ALBUMS; query_params.sort = S_ALBUM; if (media_kind) query_params.filter = db_mprintf("(f.media_kind = %d)", media_kind); ret = fetch_albums(&query_params, items, &total); if (ret < 0) goto error; json_object_object_add(reply, "total", json_object_new_int(total)); json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add albums to response buffer.\n"); error: free_query_params(&query_params, 1); jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_album(struct httpd_request *hreq) { time_t db_update; const char *album_id; json_object *reply; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_UPDATE); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; album_id = hreq->uri_parsed->path_parts[3]; reply = fetch_album(album_id); if (!reply) { ret = -1; goto error; } ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add artists to response buffer.\n"); error: jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_album_tracks(struct httpd_request *hreq) { time_t db_update; struct query_params query_params; const char *album_id; json_object *reply; json_object *items; int total; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_MODIFIED); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; album_id = hreq->uri_parsed->path_parts[3]; reply = json_object_new_object(); items = json_object_new_array(); json_object_object_add(reply, "items", items); memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto error; query_params.type = Q_ITEMS; query_params.sort = S_ALBUM; query_params.filter = db_mprintf("(f.songalbumid = %q)", album_id); ret = fetch_tracks(&query_params, items, &total); free(query_params.filter); if (ret < 0) goto error; json_object_object_add(reply, "total", json_object_new_int(total)); json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add tracks to response buffer.\n"); error: jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_tracks_get_byid(struct httpd_request *hreq) { time_t db_update; struct query_params query_params; const char *track_id; struct db_media_file_info dbmfi; json_object *reply = NULL; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_MODIFIED); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; track_id = hreq->uri_parsed->path_parts[3]; memset(&query_params, 0, sizeof(struct query_params)); query_params.type = Q_ITEMS; query_params.filter = db_mprintf("(f.id = %q)", track_id); ret = db_query_start(&query_params); if (ret < 0) goto error; ret = db_query_fetch_file(&query_params, &dbmfi); if (ret < 0) goto error; if (dbmfi.id == 0) { DPRINTF(E_LOG, L_WEB, "Track with id '%s' not found.\n", track_id); ret = -1; goto error; } reply = track_to_json(&dbmfi); ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add track to response buffer.\n"); error: db_query_end(&query_params); free(query_params.filter); jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_tracks_put_byid(struct httpd_request *hreq) { int track_id; const char *param; int val; int ret; ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &track_id); if (ret < 0) return HTTP_INTERNAL; // Update play_count/skip_count param = evhttp_find_header(hreq->query, "play_count"); if (param) { if (strcmp(param, "increment") == 0) { db_file_inc_playcount(track_id); } else if (strcmp(param, "reset") == 0) { db_file_reset_playskip_count(track_id); } else { DPRINTF(E_WARN, L_WEB, "Ignoring invalid play_count value '%s' for track '%d'.\n", param, track_id); } } // Update rating param = evhttp_find_header(hreq->query, "rating"); if (param) { ret = safe_atoi32(param, &val); if (ret < 0) return HTTP_BADREQUEST; if (val >= 0 && val <= DB_FILES_RATING_MAX) ret = db_file_rating_update_byid(track_id, val); else DPRINTF(E_WARN, L_WEB, "Ignoring invalid rating value '%d' for track '%d'.\n", val, track_id); if (ret < 0) return HTTP_INTERNAL; } return HTTP_OK; } static int jsonapi_reply_library_playlists(struct httpd_request *hreq) { time_t db_update; struct query_params query_params; json_object *reply; json_object *items; int total; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_UPDATE); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; reply = json_object_new_object(); items = json_object_new_array(); json_object_object_add(reply, "items", items); memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto error; query_params.type = Q_PL; query_params.sort = S_PLAYLIST; query_params.filter = db_mprintf("(f.type = %d OR f.type = %d)", PL_PLAIN, PL_SMART); ret = fetch_playlists(&query_params, items, &total); free(query_params.filter); if (ret < 0) goto error; json_object_object_add(reply, "total", json_object_new_int(total)); json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add playlists to response buffer.\n"); error: jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_playlist(struct httpd_request *hreq) { time_t db_update; const char *playlist_id; json_object *reply; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_UPDATE); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; playlist_id = hreq->uri_parsed->path_parts[3]; reply = fetch_playlist(playlist_id); if (!reply) { ret = -1; goto error; } ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add playlist to response buffer.\n"); error: jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_playlist_tracks(struct httpd_request *hreq) { time_t db_update; struct query_params query_params; json_object *reply; json_object *items; int playlist_id; int total; int ret = 0; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_MODIFIED); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; ret = safe_atoi32(hreq->uri_parsed->path_parts[3], &playlist_id); if (ret < 0) { DPRINTF(E_LOG, L_WEB, "No valid playlist id given '%s'\n", hreq->uri_parsed->path); return HTTP_BADREQUEST; } reply = json_object_new_object(); items = json_object_new_array(); json_object_object_add(reply, "items", items); memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto error; query_params.type = Q_PLITEMS; query_params.id = playlist_id; ret = fetch_tracks(&query_params, items, &total); if (ret < 0) goto error; json_object_object_add(reply, "total", json_object_new_int(total)); json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "playlist tracks: Couldn't add tracks to response buffer.\n"); error: free_query_params(&query_params, 1); jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_queue_save(struct httpd_request *hreq) { const char *param; char buf[PATH_MAX+7]; char *playlist_name = NULL; int ret = 0; if ((param = evhttp_find_header(hreq->query, "name")) == NULL) { DPRINTF(E_LOG, L_WEB, "Invalid argument, missing 'name'\n"); return HTTP_BADREQUEST; } if (!allow_modifying_stored_playlists) { DPRINTF(E_LOG, L_WEB, "Modifying stored playlists is not enabled in the config file\n"); return 403; } if (access(default_playlist_directory, W_OK) < 0) { DPRINTF(E_LOG, L_WEB, "Invalid playlist save directory '%s'\n", default_playlist_directory); return 403; } playlist_name = atrim(param); if (strlen(playlist_name) < 1) { free(playlist_name); DPRINTF(E_LOG, L_WEB, "Empty playlist name parameter is not allowed\n"); return HTTP_BADREQUEST; } snprintf(buf, sizeof(buf), "/file:%s/%s", default_playlist_directory, playlist_name); free(playlist_name); ret = library_queue_save(buf); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_genres(struct httpd_request *hreq) { time_t db_update; struct query_params query_params; const char *param; enum media_kind media_kind; json_object *reply; json_object *items; int total; int ret; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_UPDATE); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; media_kind = 0; param = evhttp_find_header(hreq->query, "media_kind"); if (param) { media_kind = db_media_kind_enum(param); if (!media_kind) { DPRINTF(E_LOG, L_WEB, "Invalid media kind '%s'\n", param); return HTTP_BADREQUEST; } } reply = json_object_new_object(); items = json_object_new_array(); json_object_object_add(reply, "items", items); memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto error; query_params.type = Q_BROWSE_GENRES; query_params.idx_type = I_NONE; if (media_kind) query_params.filter = db_mprintf("(f.media_kind = %d)", media_kind); ret = fetch_genres(&query_params, items, NULL); if (ret < 0) goto error; else total = json_object_array_length(items); json_object_object_add(reply, "total", json_object_new_int(total)); json_object_object_add(reply, "offset", json_object_new_int(query_params.offset)); json_object_object_add(reply, "limit", json_object_new_int(query_params.limit)); ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add genres to response buffer.\n"); error: jparse_free(reply); free_query_params(&query_params, 1); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int jsonapi_reply_library_count(struct httpd_request *hreq) { time_t db_update; const char *param_expression; char *expression; struct smartpl smartpl_expression; struct query_params qp; struct filecount_info fci; json_object *jreply; int ret; db_update = (time_t) db_admin_getint64(DB_ADMIN_DB_UPDATE); if (db_update && httpd_request_not_modified_since(hreq->req, &db_update)) return HTTP_NOTMODIFIED; memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_COUNT_ITEMS; param_expression = evhttp_find_header(hreq->query, "expression"); if (param_expression) { memset(&smartpl_expression, 0, sizeof(struct smartpl)); expression = safe_asprintf("\"query\" { %s }", param_expression); ret = smartpl_query_parse_string(&smartpl_expression, expression); free(expression); if (ret < 0) return HTTP_BADREQUEST; qp.filter = strdup(smartpl_expression.query_where); free_smartpl(&smartpl_expression, 1); } CHECK_NULL(L_WEB, jreply = json_object_new_object()); ret = db_filecount_get(&fci, &qp); if (ret == 0) { json_object_object_add(jreply, "tracks", json_object_new_int(fci.count)); json_object_object_add(jreply, "artists", json_object_new_int(fci.artist_count)); json_object_object_add(jreply, "albums", json_object_new_int(fci.album_count)); json_object_object_add(jreply, "db_playtime", json_object_new_int64((fci.length / 1000))); } else { DPRINTF(E_LOG, L_WEB, "library: failed to get count info\n"); } free(qp.filter); 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_library_files(struct httpd_request *hreq) { const char *param; int directory_id; json_object *reply; json_object *directories; struct query_params query_params; json_object *tracks; json_object *tracks_items; json_object *playlists; json_object *playlists_items; int total; int ret; param = evhttp_find_header(hreq->query, "directory"); directory_id = DIR_FILE; if (param) { directory_id = db_directory_id_bypath(param); if (directory_id <= 0) return HTTP_INTERNAL; } reply = json_object_new_object(); // Add sub directories to response directories = json_object_new_array(); json_object_object_add(reply, "directories", directories); ret = fetch_directories(directory_id, directories); if (ret < 0) { goto error; } // Add tracks to response tracks = json_object_new_object(); json_object_object_add(reply, "tracks", tracks); tracks_items = json_object_new_array(); json_object_object_add(tracks, "items", tracks_items); memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto error; query_params.type = Q_ITEMS; query_params.sort = S_VPATH; query_params.filter = db_mprintf("(f.directory_id = %d)", directory_id); ret = fetch_tracks(&query_params, tracks_items, &total); free(query_params.filter); if (ret < 0) goto error; json_object_object_add(tracks, "total", json_object_new_int(total)); json_object_object_add(tracks, "offset", json_object_new_int(query_params.offset)); json_object_object_add(tracks, "limit", json_object_new_int(query_params.limit)); // Add playlists playlists = json_object_new_object(); json_object_object_add(reply, "playlists", playlists); playlists_items = json_object_new_array(); json_object_object_add(playlists, "items", playlists_items); memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto error; query_params.type = Q_PL; query_params.sort = S_VPATH; query_params.filter = db_mprintf("(f.directory_id = %d)", directory_id); ret = fetch_playlists(&query_params, playlists_items, &total); free(query_params.filter); if (ret < 0) goto error; json_object_object_add(playlists, "total", json_object_new_int(total)); json_object_object_add(playlists, "offset", json_object_new_int(query_params.offset)); json_object_object_add(playlists, "limit", json_object_new_int(query_params.limit)); // Build JSON response ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "browse: Couldn't add directories to response buffer.\n"); error: jparse_free(reply); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } static int search_tracks(json_object *reply, struct httpd_request *hreq, const char *param_query, struct smartpl *smartpl_expression, enum media_kind media_kind) { json_object *type; json_object *items; struct query_params query_params; int total; int ret; memset(&query_params, 0, sizeof(struct query_params)); type = json_object_new_object(); json_object_object_add(reply, "tracks", type); items = json_object_new_array(); json_object_object_add(type, "items", items); query_params.type = Q_ITEMS; query_params.sort = S_NAME; ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto out; if (param_query) { if (media_kind) query_params.filter = db_mprintf("(f.title LIKE '%%%q%%' AND f.media_kind = %d)", param_query, media_kind); else query_params.filter = db_mprintf("(f.title LIKE '%%%q%%')", param_query); } else { query_params.filter = strdup(smartpl_expression->query_where); query_params.order = safe_strdup(smartpl_expression->order); if (smartpl_expression->limit > 0) { query_params.idx_type = I_SUB; query_params.limit = smartpl_expression->limit; query_params.offset = 0; } } ret = fetch_tracks(&query_params, items, &total); if (ret < 0) goto out; json_object_object_add(type, "total", json_object_new_int(total)); json_object_object_add(type, "offset", json_object_new_int(query_params.offset)); json_object_object_add(type, "limit", json_object_new_int(query_params.limit)); out: free_query_params(&query_params, 1); return ret; } static int search_artists(json_object *reply, struct httpd_request *hreq, const char *param_query, struct smartpl *smartpl_expression, enum media_kind media_kind) { json_object *type; json_object *items; struct query_params query_params; int total; int ret; memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto out; type = json_object_new_object(); json_object_object_add(reply, "artists", type); items = json_object_new_array(); json_object_object_add(type, "items", items); query_params.type = Q_GROUP_ARTISTS; query_params.sort = S_ARTIST; ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto out; if (param_query) { if (media_kind) query_params.filter = db_mprintf("(f.album_artist LIKE '%%%q%%' AND f.media_kind = %d)", param_query, media_kind); else query_params.filter = db_mprintf("(f.album_artist LIKE '%%%q%%')", param_query); } else { query_params.filter = strdup(smartpl_expression->query_where); query_params.having = safe_strdup(smartpl_expression->having); query_params.order = safe_strdup(smartpl_expression->order); if (smartpl_expression->limit > 0) { query_params.idx_type = I_SUB; query_params.limit = smartpl_expression->limit; query_params.offset = 0; } } ret = fetch_artists(&query_params, items, &total); if (ret < 0) goto out; json_object_object_add(type, "total", json_object_new_int(total)); json_object_object_add(type, "offset", json_object_new_int(query_params.offset)); json_object_object_add(type, "limit", json_object_new_int(query_params.limit)); out: free_query_params(&query_params, 1); return ret; } static int search_albums(json_object *reply, struct httpd_request *hreq, const char *param_query, struct smartpl *smartpl_expression, enum media_kind media_kind) { json_object *type; json_object *items; struct query_params query_params; int total; int ret; memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto out; type = json_object_new_object(); json_object_object_add(reply, "albums", type); items = json_object_new_array(); json_object_object_add(type, "items", items); query_params.type = Q_GROUP_ALBUMS; query_params.sort = S_ALBUM; ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto out; if (param_query) { if (media_kind) query_params.filter = db_mprintf("(f.album LIKE '%%%q%%' AND f.media_kind = %d)", param_query, media_kind); else query_params.filter = db_mprintf("(f.album LIKE '%%%q%%')", param_query); } else { query_params.filter = strdup(smartpl_expression->query_where); query_params.having = safe_strdup(smartpl_expression->having); query_params.order = safe_strdup(smartpl_expression->order); if (smartpl_expression->limit > 0) { query_params.idx_type = I_SUB; query_params.limit = smartpl_expression->limit; query_params.offset = 0; } } ret = fetch_albums(&query_params, items, &total); if (ret < 0) goto out; json_object_object_add(type, "total", json_object_new_int(total)); json_object_object_add(type, "offset", json_object_new_int(query_params.offset)); json_object_object_add(type, "limit", json_object_new_int(query_params.limit)); out: free_query_params(&query_params, 1); return ret; } static int search_playlists(json_object *reply, struct httpd_request *hreq, const char *param_query) { json_object *type; json_object *items; struct query_params query_params; int total; int ret; if (!param_query) return 0; memset(&query_params, 0, sizeof(struct query_params)); ret = query_params_limit_set(&query_params, hreq); if (ret < 0) goto out; type = json_object_new_object(); json_object_object_add(reply, "playlists", type); items = json_object_new_array(); json_object_object_add(type, "items", items); query_params.type = Q_PL; query_params.sort = S_PLAYLIST; query_params.filter = db_mprintf("((f.type = %d OR f.type = %d) AND f.title LIKE '%%%q%%')", PL_PLAIN, PL_SMART, param_query); ret = fetch_playlists(&query_params, items, &total); if (ret < 0) goto out; json_object_object_add(type, "total", json_object_new_int(total)); json_object_object_add(type, "offset", json_object_new_int(query_params.offset)); json_object_object_add(type, "limit", json_object_new_int(query_params.limit)); out: free_query_params(&query_params, 1); return ret; } static int jsonapi_reply_search(struct httpd_request *hreq) { const char *param_type; const char *param_query; const char *param_expression; const char *param_media_kind; enum media_kind media_kind; char *expression; struct smartpl smartpl_expression; json_object *reply; int ret = 0; reply = NULL; param_type = evhttp_find_header(hreq->query, "type"); param_query = evhttp_find_header(hreq->query, "query"); param_expression = evhttp_find_header(hreq->query, "expression"); if (!param_type || (!param_query && !param_expression)) { DPRINTF(E_LOG, L_WEB, "Missing request parameter\n"); return HTTP_BADREQUEST; } media_kind = 0; param_media_kind = evhttp_find_header(hreq->query, "media_kind"); if (param_media_kind) { media_kind = db_media_kind_enum(param_media_kind); if (!media_kind) { DPRINTF(E_LOG, L_WEB, "Invalid media kind '%s'\n", param_media_kind); return HTTP_BADREQUEST; } } memset(&smartpl_expression, 0, sizeof(struct smartpl)); if (param_expression) { expression = safe_asprintf("\"query\" { %s }", param_expression); ret = smartpl_query_parse_string(&smartpl_expression, expression); free(expression); if (ret < 0) return HTTP_BADREQUEST; } reply = json_object_new_object(); if (strstr(param_type, "track")) { ret = search_tracks(reply, hreq, param_query, &smartpl_expression, media_kind); if (ret < 0) goto error; } if (strstr(param_type, "artist")) { ret = search_artists(reply, hreq, param_query, &smartpl_expression, media_kind); if (ret < 0) goto error; } if (strstr(param_type, "album")) { ret = search_albums(reply, hreq, param_query, &smartpl_expression, media_kind); if (ret < 0) goto error; } if (strstr(param_type, "playlist") && param_query) { ret = search_playlists(reply, hreq, param_query); if (ret < 0) goto error; } ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply)); if (ret < 0) DPRINTF(E_LOG, L_WEB, "playlist tracks: Couldn't add tracks to response buffer.\n"); error: jparse_free(reply); free_smartpl(&smartpl_expression, 1); if (ret < 0) return HTTP_INTERNAL; return HTTP_OK; } 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 }, { EVHTTP_REQ_PUT, "^/api/rescan$", jsonapi_reply_meta_rescan }, { EVHTTP_REQ_POST, "^/api/spotify-login$", jsonapi_reply_spotify_login }, { EVHTTP_REQ_GET, "^/api/spotify$", jsonapi_reply_spotify }, { EVHTTP_REQ_GET, "^/api/pairing$", jsonapi_reply_pairing_get }, { EVHTTP_REQ_POST, "^/api/pairing$", jsonapi_reply_pairing_kickoff }, { EVHTTP_REQ_POST, "^/api/lastfm-login$", jsonapi_reply_lastfm_login }, { EVHTTP_REQ_GET, "^/api/lastfm-logout$", jsonapi_reply_lastfm_logout }, { EVHTTP_REQ_GET, "^/api/lastfm$", jsonapi_reply_lastfm }, { EVHTTP_REQ_POST, "^/api/verification$", jsonapi_reply_verification }, { EVHTTP_REQ_GET, "^/api/outputs$", jsonapi_reply_outputs }, { EVHTTP_REQ_PUT, "^/api/outputs/set$", jsonapi_reply_outputs_set }, { EVHTTP_REQ_POST, "^/api/select-outputs$", jsonapi_reply_outputs_set }, // deprecated: use "/api/outputs/set" { EVHTTP_REQ_GET, "^/api/outputs/[[:digit:]]+$", jsonapi_reply_outputs_get_byid }, { EVHTTP_REQ_PUT, "^/api/outputs/[[:digit:]]+$", jsonapi_reply_outputs_put_byid }, { EVHTTP_REQ_PUT, "^/api/outputs/[[:digit:]]+/toggle$", jsonapi_reply_outputs_toggle_byid }, { EVHTTP_REQ_GET, "^/api/player$", jsonapi_reply_player }, { EVHTTP_REQ_PUT, "^/api/player/play$", jsonapi_reply_player_play }, { EVHTTP_REQ_PUT, "^/api/player/pause$", jsonapi_reply_player_pause }, { EVHTTP_REQ_PUT, "^/api/player/stop$", jsonapi_reply_player_stop }, { EVHTTP_REQ_PUT, "^/api/player/toggle$", jsonapi_reply_player_toggle }, { EVHTTP_REQ_PUT, "^/api/player/next$", jsonapi_reply_player_next }, { EVHTTP_REQ_PUT, "^/api/player/previous$", jsonapi_reply_player_previous }, { EVHTTP_REQ_PUT, "^/api/player/shuffle$", jsonapi_reply_player_shuffle }, { EVHTTP_REQ_PUT, "^/api/player/repeat$", jsonapi_reply_player_repeat }, { EVHTTP_REQ_PUT, "^/api/player/consume$", jsonapi_reply_player_consume }, { EVHTTP_REQ_PUT, "^/api/player/volume$", jsonapi_reply_player_volume }, { EVHTTP_REQ_PUT, "^/api/player/seek$", jsonapi_reply_player_seek }, { EVHTTP_REQ_GET, "^/api/queue$", jsonapi_reply_queue }, { EVHTTP_REQ_PUT, "^/api/queue/clear$", jsonapi_reply_queue_clear }, { EVHTTP_REQ_POST, "^/api/queue/items/add$", jsonapi_reply_queue_tracks_add }, { EVHTTP_REQ_PUT, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_move }, { EVHTTP_REQ_DELETE, "^/api/queue/items/[[:digit:]]+$", jsonapi_reply_queue_tracks_delete }, { EVHTTP_REQ_POST, "^/api/queue/save$", jsonapi_reply_queue_save}, { EVHTTP_REQ_GET, "^/api/library/playlists$", jsonapi_reply_library_playlists }, { EVHTTP_REQ_GET, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist }, { EVHTTP_REQ_GET, "^/api/library/playlists/[[:digit:]]+/tracks$", jsonapi_reply_library_playlist_tracks }, // { EVHTTP_REQ_POST, "^/api/library/playlists/[[:digit:]]+/tracks$", jsonapi_reply_library_playlists_tracks }, // { EVHTTP_REQ_DELETE, "^/api/library/playlists/[[:digit:]]+$", jsonapi_reply_library_playlist_tracks }, { EVHTTP_REQ_GET, "^/api/library/artists$", jsonapi_reply_library_artists }, { EVHTTP_REQ_GET, "^/api/library/artists/[[:digit:]]+$", jsonapi_reply_library_artist }, { EVHTTP_REQ_GET, "^/api/library/artists/[[:digit:]]+/albums$", jsonapi_reply_library_artist_albums }, { EVHTTP_REQ_GET, "^/api/library/albums$", jsonapi_reply_library_albums }, { EVHTTP_REQ_GET, "^/api/library/albums/[[:digit:]]+$", jsonapi_reply_library_album }, { EVHTTP_REQ_GET, "^/api/library/albums/[[:digit:]]+/tracks$", jsonapi_reply_library_album_tracks }, { EVHTTP_REQ_GET, "^/api/library/tracks/[[:digit:]]+$", jsonapi_reply_library_tracks_get_byid }, { EVHTTP_REQ_PUT, "^/api/library/tracks/[[:digit:]]+$", jsonapi_reply_library_tracks_put_byid }, { EVHTTP_REQ_GET, "^/api/library/genres$", jsonapi_reply_library_genres}, { EVHTTP_REQ_GET, "^/api/library/count$", jsonapi_reply_library_count }, { EVHTTP_REQ_GET, "^/api/library/files$", jsonapi_reply_library_files }, { EVHTTP_REQ_GET, "^/api/search$", jsonapi_reply_search }, { 0, NULL, NULL } }; /* ------------------------------- JSON API --------------------------------- */ void jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) { struct httpd_request *hreq; struct evkeyvalq *headers; int status_code; DPRINTF(E_DBG, L_WEB, "JSON api request: '%s'\n", uri_parsed->uri); if (!httpd_admin_check_auth(req)) return; hreq = httpd_request_parse(req, uri_parsed, NULL, adm_handlers); if (!hreq) { DPRINTF(E_LOG, L_WEB, "Unrecognized path '%s' in JSON api request: '%s'\n", uri_parsed->path, uri_parsed->uri); httpd_send_error(req, HTTP_BADREQUEST, "Bad Request"); return; } CHECK_NULL(L_WEB, hreq->reply = evbuffer_new()); status_code = hreq->handler(hreq); if (status_code >= 400) DPRINTF(E_LOG, L_WEB, "JSON api request failed with error code %d (%s)\n", status_code, uri_parsed->uri); switch (status_code) { case HTTP_OK: /* 200 OK */ headers = evhttp_request_get_output_headers(req); evhttp_add_header(headers, "Content-Type", "application/json"); httpd_send_reply(req, status_code, "OK", hreq->reply, HTTPD_SEND_NO_GZIP); break; case HTTP_NOCONTENT: /* 204 No Content */ httpd_send_reply(req, status_code, "No Content", hreq->reply, HTTPD_SEND_NO_GZIP); break; case HTTP_NOTMODIFIED: /* 304 Not Modified */ httpd_send_reply(req, HTTP_NOTMODIFIED, NULL, NULL, HTTPD_SEND_NO_GZIP); break; case HTTP_BADREQUEST: /* 400 Bad Request */ httpd_send_error(req, status_code, "Bad Request"); break; case 403: httpd_send_error(req, status_code, "Forbidden"); break; case HTTP_NOTFOUND: /* 404 Not Found */ httpd_send_error(req, status_code, "Not Found"); break; case HTTP_INTERNAL: /* 500 Internal Server Error */ default: httpd_send_error(req, HTTP_INTERNAL, "Internal Server Error"); break; } evbuffer_free(hreq->reply); free(hreq); } int jsonapi_is_request(const char *path) { if (strncmp(path, "/api/", strlen("/api/")) == 0) return 1; if (strcmp(path, "/api") == 0) return 1; return 0; } int jsonapi_init(void) { char buf[64]; char *temp_path; 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, "JSON api init failed; regexp error: %s\n", buf); return -1; } } default_playlist_directory = NULL; allow_modifying_stored_playlists = cfg_getbool(cfg_getsec(cfg, "library"), "allow_modifying_stored_playlists"); if (allow_modifying_stored_playlists) { temp_path = cfg_getstr(cfg_getsec(cfg, "library"), "default_playlist_directory"); if (temp_path) { // The path in the conf file may have a trailing slash character. Return the realpath like it is done for the library directories. default_playlist_directory = realpath(temp_path, NULL); if (default_playlist_directory) { if (access(default_playlist_directory, W_OK) < 0) DPRINTF(E_WARN, L_WEB, "Non-writable playlist save directory '%s'\n", default_playlist_directory); } } if (!default_playlist_directory) { DPRINTF(E_LOG, L_WEB, "Invalid playlist save directory, disabling modifying stored playlists\n"); allow_modifying_stored_playlists = false; } } return 0; } void jsonapi_deinit(void) { int i; for (i = 0; adm_handlers[i].handler; i++) regfree(&adm_handlers[i].preg); free(default_playlist_directory); }