From a78ed208483756ee16b25d66f6d4fd39a879ff5a Mon Sep 17 00:00:00 2001 From: chme Date: Wed, 27 Dec 2017 10:01:36 +0100 Subject: [PATCH] [jsonapi] Add new JSON api endpoints Allows - browsing artists/albums/playlists - search - moving a queue item --- src/httpd_jsonapi.c | 1375 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 1258 insertions(+), 117 deletions(-) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 8be6366c..879e2420 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -33,6 +33,7 @@ #include #include #include +#include #include "httpd_jsonapi.h" #include "conffile.h" @@ -54,81 +55,340 @@ /* -------------------------------- HELPERS --------------------------------- */ -/* - * 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) +static inline void +safe_json_add_string(json_object *obj, const char *key, const char *value) { - 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; + if (value) + json_object_object_add(obj, key, json_object_new_string(value)); } -/* - * Retrieves pairing information - * - * Example response: - * - * { - * "active": true, - * "remote": "remote name" - * } - */ -static int -jsonapi_reply_pairing_get(struct httpd_request *hreq) +static inline void +safe_json_add_int_from_string(json_object *obj, const char *key, const char *value) { - char *remote_name; - json_object *jreply; + int intval; + int ret; - remote_name = remote_pairing_get_name(); + if (!value) + return; - 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; + 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) +{ + 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; + } + + strftime(result, sizeof(result), "%FT%TZ", &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]; + 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)); + + return item; +} + +static json_object * +album_to_json(struct db_group_info *dbgri) +{ + json_object *item; + char uri[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)); + + return item; +} + +static json_object * +track_to_json(struct db_media_file_info *dbmfi) +{ + json_object *item; + char uri[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, "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_artist", dbmfi->album_artist); + safe_json_add_string(item, "album_artist_sort", dbmfi->album_artist_sort); + 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, "play_count", dbmfi->play_count); + safe_json_add_time_from_string(item, "time_played", dbmfi->time_played); + + 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)); + + 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 int +fetch_tracks(struct query_params *query_params, json_object *items, int *total) +{ + struct db_media_file_info dbmfi; + json_object *item; + int ret = 0; + + 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 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 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)) == 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 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 ------------------------------- */ @@ -153,6 +413,12 @@ jsonapi_reply_config(struct httpd_request *hreq) 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"); @@ -477,6 +743,81 @@ jsonapi_reply_lastfm_logout(struct httpd_request *hreq) 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; @@ -723,11 +1064,81 @@ jsonapi_reply_outputs_set(struct httpd_request *hreq) 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); + + return HTTP_NOCONTENT; +} + +static int +play_item_at_position(const char *param) +{ + uint32_t position; + 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; + } + + queue_item = db_queue_fetch_bypos(position, 0); + 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); + + 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) { @@ -816,6 +1227,7 @@ 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); @@ -856,9 +1268,30 @@ jsonapi_reply_player(struct httpd_request *hreq) json_object_object_add(reply, "shuffle", json_object_new_boolean(status.shuffle)); json_object_object_add(reply, "volume", json_object_new_int(status.volume)); - 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)); + 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)); + } + 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))); @@ -868,34 +1301,278 @@ jsonapi_reply_player(struct httpd_request *hreq) } static json_object * -queue_item_to_json(struct db_queue_item *queue_item) +queue_item_to_json(struct db_queue_item *queue_item, char shuffle) { json_object *item; + char uri[100]; + int ret; item = json_object_new_object(); json_object_object_add(item, "id", json_object_new_int(queue_item->id)); - json_object_object_add(item, "position", json_object_new_int(queue_item->pos)); - json_object_object_add(item, "shuffle_position", json_object_new_int(queue_item->shuffle_pos)); + 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)); - json_object_object_add(item, "file_id", json_object_new_int(queue_item->file_id)); - json_object_object_add(item, "path", json_object_new_string(queue_item->path)); - json_object_object_add(item, "virtual_path", json_object_new_string(queue_item->virtual_path)); + 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(item, "album_artist", queue_item->album_artist); + safe_json_add_string(item, "album_artist_sort", queue_item->album_artist_sort); + safe_json_add_string(item, "genre", queue_item->genre); - json_object_object_add(item, "title", json_object_new_string(queue_item->title)); - json_object_object_add(item, "artist", json_object_new_string(queue_item->artist)); - json_object_object_add(item, "albumartist", json_object_new_string(queue_item->album_artist)); - json_object_object_add(item, "album", json_object_new_string(queue_item->album)); - json_object_object_add(item, "genre", json_object_new_string(queue_item->genre)); - json_object_object_add(item, "artist_sort", json_object_new_string(queue_item->artist_sort)); - json_object_object_add(item, "albumartist_sort", json_object_new_string(queue_item->album_artist_sort)); - json_object_object_add(item, "album_sort", json_object_new_string(queue_item->album_sort)); 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) + { + 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); + } + return item; } +static int +queue_tracks_add_artist(const char *id) +{ + struct query_params query_params; + struct player_status status; + 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); + + free(query_params.filter); + + return ret; +} + +static int +queue_tracks_add_album(const char *id) +{ + struct query_params query_params; + struct player_status status; + 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); + + free(query_params.filter); + + return ret; +} + +static int +queue_tracks_add_track(const char *id) +{ + struct query_params query_params; + struct player_status status; + 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); + + free(query_params.filter); + + return ret; +} + +static int +queue_tracks_add_playlist(const char *id) +{ + struct player_status status; + int playlist_id; + 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); + + return ret; +} + +static int +jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) +{ + const char *param; + char *uris; + char *uri; + const char *id; + int ret = 0; + + param = evhttp_find_header(hreq->query, "uris"); + if (!param) + { + DPRINTF(E_LOG, L_WEB, "Missing query parameter 'uris'\n"); + + return HTTP_BADREQUEST; + } + + uris = strdup(param); + uri = strtok(uris, ","); + + do + { + if (strncmp(uri, "library:artist:", strlen("library:artist:")) == 0) + { + id = uri + (strlen("library:artist:")); + queue_tracks_add_artist(id); + } + else if (strncmp(uri, "library:album:", strlen("library:album:")) == 0) + { + id = uri + (strlen("library:album:")); + queue_tracks_add_album(id); + } + else if (strncmp(uri, "library:track:", strlen("library:track:")) == 0) + { + id = uri + (strlen("library:track:")); + queue_tracks_add_track(id); + } + else if (strncmp(uri, "library:playlist:", strlen("library:playlist:")) == 0) + { + id = uri + (strlen("library:playlist:")); + queue_tracks_add_playlist(id); + } + else + { + DPRINTF(E_LOG, L_WEB, "Invalid uri '%s'\n", uri); + } + } + while ((uri = strtok(NULL, ","))); + + free(uris); + + if (ret < 0) + return HTTP_INTERNAL; + + return HTTP_NOCONTENT; +} + +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) { @@ -905,6 +1582,7 @@ jsonapi_reply_queue(struct httpd_request *hreq) int start_pos, end_pos; int version; int count; + struct player_status status; struct db_queue_item queue_item; json_object *reply; json_object *items; @@ -923,11 +1601,9 @@ jsonapi_reply_queue(struct httpd_request *hreq) items = json_object_new_array(); json_object_object_add(reply, "items", items); - param = evhttp_find_header(hreq->query, "sort"); - if (param && strcmp(param, "shuffle") == 0) - { - query_params.sort = S_SHUFFLE_POS; - } + 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) @@ -958,7 +1634,7 @@ jsonapi_reply_queue(struct httpd_request *hreq) while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) { - item = queue_item_to_json(&queue_item); + item = queue_item_to_json(&queue_item, status.shuffle); if (!item) goto error; @@ -1077,38 +1753,503 @@ jsonapi_reply_player_volume(struct httpd_request *hreq) return HTTP_NOCONTENT; } +static int +jsonapi_reply_library_artists(struct httpd_request *hreq) +{ + struct query_params query_params; + const char *param; + enum media_kind media_kind; + json_object *reply; + json_object *items; + int total; + int ret = 0; + + 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: + jparse_free(reply); + + if (ret < 0) + return HTTP_INTERNAL; + + return HTTP_OK; +} + +static int +jsonapi_reply_library_artist_albums(struct httpd_request *hreq) +{ + struct query_params query_params; + const char *artist_id; + json_object *reply; + json_object *items; + int total; + int ret = 0; + + 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) +{ + struct query_params query_params; + const char *param; + enum media_kind media_kind; + json_object *reply; + json_object *items; + int total; + int ret = 0; + + 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: + jparse_free(reply); + + if (ret < 0) + return HTTP_INTERNAL; + + return HTTP_OK; +} + +static int +jsonapi_reply_library_album_tracks(struct httpd_request *hreq) +{ + struct query_params query_params; + const char *album_id; + json_object *reply; + json_object *items; + int total; + int ret = 0; + + 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_playlists(struct httpd_request *hreq) +{ + struct query_params query_params; + json_object *reply; + json_object *items; + int total; + int ret = 0; + + 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_tracks(struct httpd_request *hreq) +{ + struct query_params query_params; + json_object *reply; + json_object *items; + int playlist_id; + int total; + int ret = 0; + + 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: + jparse_free(reply); + + if (ret < 0) + return HTTP_INTERNAL; + + return HTTP_OK; +} + +static int +jsonapi_reply_search(struct httpd_request *hreq) +{ + const char *param_type; + const char *param_query; + struct query_params query_params; + json_object *reply; + json_object *type; + json_object *items; + int total; + int ret = 0; + + reply = NULL; + + param_type = evhttp_find_header(hreq->query, "type"); + param_query = evhttp_find_header(hreq->query, "query"); + + if (!param_type || !param_query) + { + DPRINTF(E_LOG, L_WEB, "Missing request parameter\n"); + + return HTTP_BADREQUEST; + } + + memset(&query_params, 0, sizeof(struct query_params)); + + ret = query_params_limit_set(&query_params, hreq); + if (ret < 0) + goto error; + + reply = json_object_new_object(); + + if (strstr(param_type, "track")) + { + 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; + query_params.filter = db_mprintf("(f.title LIKE '%%%q%%')", param_query); + + ret = fetch_tracks(&query_params, items, &total); + free(query_params.filter); + + if (ret < 0) + goto error; + + 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)); + } + + if (strstr(param_type, "artist")) + { + 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; + query_params.filter = db_mprintf("(f.album_artist LIKE '%%%q%%')", param_query); + + ret = fetch_artists(&query_params, items, &total); + free(query_params.filter); + + if (ret < 0) + goto error; + + 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)); + } + + if (strstr(param_type, "album")) + { + 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; + query_params.filter = db_mprintf("(f.album LIKE '%%%q%%')", param_query); + + ret = fetch_albums(&query_params, items, &total); + free(query_params.filter); + + if (ret < 0) + goto error; + + 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)); + } + + if (strstr(param_type, "playlist")) + { + 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); + free(query_params.filter); + + if (ret < 0) + goto error; + + 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)); + } + + 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); + + 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/library$", jsonapi_reply_library }, - { EVHTTP_REQ_GET, "^/api/update$", jsonapi_reply_update }, - { 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/config$", jsonapi_reply_config }, + { EVHTTP_REQ_GET, "^/api/library$", jsonapi_reply_library }, + { EVHTTP_REQ_GET, "^/api/update$", jsonapi_reply_update }, + { 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_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/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/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_GET, "^/api/player$", jsonapi_reply_player }, + { 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/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_GET, "^/api/queue$", jsonapi_reply_queue }, + { 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_GET, "^/api/library/playlists$", jsonapi_reply_library_playlists }, + { 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:]]+/albums", jsonapi_reply_library_artist_albums }, + { EVHTTP_REQ_GET, "^/api/library/albums$", jsonapi_reply_library_albums }, + { EVHTTP_REQ_GET, "^/api/library/albums/[[:digit:]]+/tracks$", jsonapi_reply_library_album_tracks }, + + { EVHTTP_REQ_GET, "^/api/search$", jsonapi_reply_search }, { 0, NULL, NULL } };