From ca99bf871839c5a9c37aa1e76014621730a55e9b Mon Sep 17 00:00:00 2001 From: chme Date: Thu, 27 Dec 2018 15:29:25 +0100 Subject: [PATCH 01/10] [jsonapi] Sort files by filename and return directories in api/config --- src/httpd_jsonapi.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 3a0dd53b..edc62f7d 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -632,6 +632,10 @@ jsonapi_reply_config(struct httpd_request *hreq) json_object *buildopts; int websocket_port; char **buildoptions; + cfg_t *lib; + int ndirs; + char *path; + json_object *directories; int i; CHECK_NULL(L_WEB, jreply = json_object_new_object()); @@ -662,6 +666,17 @@ jsonapi_reply_config(struct httpd_request *hreq) } 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); + json_object_array_add(directories, json_object_new_string(path)); + } + json_object_object_add(jreply, "directories", directories); + CHECK_ERRNO(L_WEB, evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(jreply))); jparse_free(jreply); @@ -2726,7 +2741,7 @@ jsonapi_reply_library_files(struct httpd_request *hreq) goto error; query_params.type = Q_ITEMS; - query_params.sort = S_NAME; + query_params.sort = S_VPATH; query_params.filter = db_mprintf("(f.directory_id = %d)", directory_id); ret = fetch_tracks(&query_params, tracks_items, &total); From 2ead24a2f7d73d6ef42f0b89252d20fe86ea8f92 Mon Sep 17 00:00:00 2001 From: chme Date: Sat, 29 Dec 2018 09:51:30 +0100 Subject: [PATCH 02/10] [jsonapi] Add optional expression parameter to queue/items/add endpoint Adds support to add tracks by a smart pl query expression --- src/httpd_jsonapi.c | 115 +++++++++++++++++++++++++++++++++----------- 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index edc62f7d..87eee26c 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -1752,39 +1752,15 @@ queue_tracks_add_playlist(const char *id, int pos) } static int -jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) +queue_tracks_add_byuris(const char *param, int pos, int *total_count) { - const char *param; char *uris; char *uri; const char *id; - int pos = -1; int count = 0; - int ttl_count = 0; - json_object *reply; int ret = 0; - - param = evhttp_find_header(hreq->query, "position"); - if (param) - { - if (safe_atoi32(param, &pos) < 0) - { - DPRINTF(E_LOG, L_WEB, "Invalid position parameter '%s'\n", param); - - return HTTP_BADREQUEST; - } - - DPRINTF(E_DBG, L_WEB, "Add tracks starting at position '%d\n", pos); - } - - param = evhttp_find_header(hreq->query, "uris"); - if (!param) - { - DPRINTF(E_LOG, L_WEB, "Missing query parameter 'uris'\n"); - - return HTTP_BADREQUEST; - } + *total_count = 0; uris = strdup(param); uri = strtok(uris, ","); @@ -1827,16 +1803,99 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) if (pos >= 0) pos += count; - ttl_count += count; + *total_count += count; } while ((uri = strtok(NULL, ","))); 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_ALBUM; + 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); + 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.filter); + + 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; + int pos = -1; + 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 (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(ttl_count)); + 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); From e3b0442fc7dee05c1ff1a91fc46d6dcd1394f0af Mon Sep 17 00:00:00 2001 From: chme Date: Sat, 29 Dec 2018 10:03:26 +0100 Subject: [PATCH 03/10] [README] Add documentation for expression parameter in queue/items/add --- README_JSON_API.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README_JSON_API.md b/README_JSON_API.md index 6a62cf88..3052b8de 100644 --- a/README_JSON_API.md +++ b/README_JSON_API.md @@ -583,8 +583,11 @@ POST /api/queue/items/add | Parameter | Value | | --------------- | ----------------------------------------------------------- | | uris | Comma seperated list of resource identifiers (`track`, `playlist`, `artist` or `album` object `uri`) | +| expression | A smart playlist query expression identifying the tracks that will be added to the queue. | | position | *(Optional)* If a position is given, new items are inserted starting from this position into the queue. | +Either the `uris` or the `expression` parameter must be set. If both are set the `uris` parameter takes presedence and the `expression` parameter will be ignored. + **Response** On success returns the HTTP `200 OK` success status response code. @@ -596,6 +599,8 @@ On success returns the HTTP `200 OK` success status response code. **Example** +Add new items by uri: + ```shell curl -X POST "http://localhost:3689/api/queue/items/add?uris=library:playlist:68,library:artist:2932599850102967727" ``` @@ -606,6 +611,18 @@ curl -X POST "http://localhost:3689/api/queue/items/add?uris=library:playlist:68 } ``` +Add new items by query language: + +```shell +curl -X POST "http://localhost:3689/api/queue/items/add?expression=media_kind+is+music" +``` + +```json +{ + "count": 42 +} +``` + ### Moving a queue item From b3853ef0f3db3da51ae5b364321c9e3a9ebc963d Mon Sep 17 00:00:00 2001 From: chme Date: Sun, 30 Dec 2018 08:33:31 +0100 Subject: [PATCH 04/10] [jsonapi] Check for empty "uris" parameter in queue/items/add (prevents a segfault) and log failed requests in error log --- src/httpd_jsonapi.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 87eee26c..f70ed1b5 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -1762,6 +1762,12 @@ queue_tracks_add_byuris(const char *param, int pos, int *total_count) *total_count = 0; + if (strlen(param) == 0) + { + DPRINTF(E_LOG, L_WEB, "Empty query parameter 'uris'\n"); + return -1; + } + uris = strdup(param); uri = strtok(uris, ","); @@ -3255,6 +3261,9 @@ jsonapi_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed) 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 */ From f23732945b392a87ee1207fd19b63ebf02d4c358 Mon Sep 17 00:00:00 2001 From: chme Date: Sun, 30 Dec 2018 09:04:00 +0100 Subject: [PATCH 05/10] [websocket] Support "database" notifications --- src/websocket.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/websocket.c b/src/websocket.c index 9fd17efd..6cb41a2b 100644 --- a/src/websocket.c +++ b/src/websocket.c @@ -129,6 +129,10 @@ process_notify_request(struct ws_session_data_notify *session_data, void *in, si { session_data->events |= LISTENER_UPDATE; } + else if (0 == strcmp(event_type, "database")) + { + session_data->events |= LISTENER_DATABASE; + } else if (0 == strcmp(event_type, "pairing")) { session_data->events |= LISTENER_PAIRING; @@ -193,6 +197,10 @@ send_notify_reply(short events, struct lws* wsi) { json_object_array_add(notify, json_object_new_string("update")); } + if (events & LISTENER_DATABASE) + { + json_object_array_add(notify, json_object_new_string("database")); + } if (events & LISTENER_PAIRING) { json_object_array_add(notify, json_object_new_string("pairing")); @@ -306,7 +314,7 @@ static struct lws_protocols protocols[] = static void * websocket(void *arg) { - listener_add(listener_cb, LISTENER_UPDATE | LISTENER_PAIRING | LISTENER_SPOTIFY | LISTENER_LASTFM | LISTENER_SPEAKER + listener_add(listener_cb, LISTENER_UPDATE | LISTENER_DATABASE | LISTENER_PAIRING | LISTENER_SPOTIFY | LISTENER_LASTFM | LISTENER_SPEAKER | LISTENER_PLAYER | LISTENER_OPTIONS | LISTENER_VOLUME | LISTENER_QUEUE); while(!ws_exit) From 3de2418b493649570b3eaa46b09caab9c917e84b Mon Sep 17 00:00:00 2001 From: chme Date: Sun, 30 Dec 2018 09:06:53 +0100 Subject: [PATCH 06/10] [README] Add "database" event to push notification documentation --- README_JSON_API.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README_JSON_API.md b/README_JSON_API.md index 3052b8de..0ba4f52b 100644 --- a/README_JSON_API.md +++ b/README_JSON_API.md @@ -1795,6 +1795,7 @@ will send a message each time one of the events occurred. | Type | Description | | --------------- | ----------------------------------------- | | update | Library update started or finished | +| database | Library database changed (new/modified/deleted tracks) | | outputs | An output was enabled or disabled | | player | Player state changes | | options | Playback option changes (shuffle, repeat, consume mode) | From 0f4cbb3375c2160cc68a9206213cec778184fda8 Mon Sep 17 00:00:00 2001 From: chme Date: Mon, 31 Dec 2018 08:26:40 +0100 Subject: [PATCH 07/10] [http/jsonapi] Use strtok_r instead of strtok --- src/http.c | 14 ++++++++------ src/httpd_jsonapi.c | 12 +++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/http.c b/src/http.c index 06315041..0cc85a23 100644 --- a/src/http.c +++ b/src/http.c @@ -597,6 +597,7 @@ metadata_packet_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) { uint8_t *buffer; char *icy_token; + char *save_pr; char *ptr; char *end; @@ -604,13 +605,13 @@ metadata_packet_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) if (!buffer) return -1; - icy_token = strtok((char *)buffer, ";"); + icy_token = strtok_r((char *)buffer, ";", &save_pr); while (icy_token != NULL) { ptr = strchr(icy_token, '='); if (!ptr || (ptr[1] == '\0')) { - icy_token = strtok(NULL, ";"); + icy_token = strtok_r(NULL, ";", &save_pr); continue; } @@ -647,7 +648,7 @@ metadata_packet_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) if (end) *end = '\''; - icy_token = strtok(NULL, ";"); + icy_token = strtok_r(NULL, ";", &save_pr); } av_free(buffer); @@ -663,6 +664,7 @@ metadata_header_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) uint8_t *buffer; uint8_t *utf; char *icy_token; + char *save_pr; char *ptr; av_opt_get(fmtctx, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN, &buffer); @@ -677,13 +679,13 @@ metadata_header_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) if (!utf) return -1; - icy_token = strtok((char *)utf, "\r\n"); + icy_token = strtok_r((char *)utf, "\r\n", &save_pr); while (icy_token != NULL) { ptr = strchr(icy_token, ':'); if (!ptr || (ptr[1] == '\0')) { - icy_token = strtok(NULL, "\r\n"); + icy_token = strtok_r(NULL, "\r\n", &save_pr); continue; } @@ -698,7 +700,7 @@ metadata_header_get(struct http_icy_metadata *metadata, AVFormatContext *fmtctx) else if ((strncmp(icy_token, "icy-genre", strlen("icy-genre")) == 0) && !metadata->genre) metadata->genre = strdup(ptr); - icy_token = strtok(NULL, "\r\n"); + icy_token = strtok_r(NULL, "\r\n", &save_pr); } free(utf); diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index f70ed1b5..0acf3751 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -1756,21 +1756,23 @@ 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; - if (strlen(param) == 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; } - uris = strdup(param); - uri = strtok(uris, ","); - do { count = 0; @@ -1811,7 +1813,7 @@ queue_tracks_add_byuris(const char *param, int pos, int *total_count) *total_count += count; } - while ((uri = strtok(NULL, ","))); + while ((uri = strtok_r(NULL, ",", &ptr))); free(uris); From 77a19a6df9dd48c84da72afc9938987a2706188c Mon Sep 17 00:00:00 2001 From: chme Date: Tue, 1 Jan 2019 10:15:23 +0100 Subject: [PATCH 08/10] [jsonapi] Add checks for failed playback start and return error code --- src/httpd_jsonapi.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 0acf3751..0e47cb19 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -1335,6 +1335,13 @@ play_item_with_id(const char *param) 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; } @@ -1368,6 +1375,13 @@ play_item_at_position(const char *param) 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; } From fb60a05228beb1acc59ace6cbbf3414cb03146f9 Mon Sep 17 00:00:00 2001 From: chme Date: Wed, 2 Jan 2019 09:46:47 +0100 Subject: [PATCH 09/10] [jsonapi] Add support for "playback", "clear" and "shuffle" parameters in "queue/items/add" endpoint --- src/httpd_jsonapi.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 0e47cb19..1c9ba23b 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -1876,7 +1876,9 @@ 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; @@ -1905,6 +1907,22 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) 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); @@ -1926,6 +1944,13 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) 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) + { + player_playback_start(); + } + return HTTP_OK; } From 98098698deff91b8d3f44114e93b385b37cdd980 Mon Sep 17 00:00:00 2001 From: chme Date: Wed, 2 Jan 2019 09:51:11 +0100 Subject: [PATCH 10/10] [README] Update documentation for queue/items/add endpoint --- README_JSON_API.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README_JSON_API.md b/README_JSON_API.md index 0ba4f52b..412e0d71 100644 --- a/README_JSON_API.md +++ b/README_JSON_API.md @@ -582,9 +582,12 @@ POST /api/queue/items/add | Parameter | Value | | --------------- | ----------------------------------------------------------- | -| uris | Comma seperated list of resource identifiers (`track`, `playlist`, `artist` or `album` object `uri`) | -| expression | A smart playlist query expression identifying the tracks that will be added to the queue. | -| position | *(Optional)* If a position is given, new items are inserted starting from this position into the queue. | +| uris | Comma seperated list of resource identifiers (`track`, `playlist`, `artist` or `album` object `uri`) | +| expression | A smart playlist query expression identifying the tracks that will be added to the queue. | +| position | *(Optional)* If a position is given, new items are inserted starting from this position into the queue. | +| playback | *(Optional)* If the `playback` parameter is set to `start`, playback will be started after adding the new items. | +| clear | *(Optional)* If the `clear` parameter is set to `true`, the queue will be cleared before adding the new items. | +| shuffle | *(Optional)* If the `shuffle` parameter is set to `true`, the shuffle mode is activated. If it is set to something else, the shuffle mode is deactivated. To leave the shuffle mode untouched the parameter should be ommited. | Either the `uris` or the `expression` parameter must be set. If both are set the `uris` parameter takes presedence and the `expression` parameter will be ignored.