diff --git a/README_JSON_API.md b/README_JSON_API.md index 6a62cf88..412e0d71 100644 --- a/README_JSON_API.md +++ b/README_JSON_API.md @@ -582,8 +582,14 @@ POST /api/queue/items/add | Parameter | Value | | --------------- | ----------------------------------------------------------- | -| uris | Comma seperated list of resource identifiers (`track`, `playlist`, `artist` or `album` object `uri`) | -| 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. **Response** @@ -596,6 +602,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 +614,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 @@ -1778,6 +1798,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) | 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 3a0dd53b..1c9ba23b 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); @@ -1320,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; } @@ -1353,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; } @@ -1737,43 +1766,27 @@ 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; + char *ptr; const char *id; - int pos = -1; int count = 0; - int ttl_count = 0; - json_object *reply; int ret = 0; + *total_count = 0; - param = evhttp_find_header(hreq->query, "position"); - if (param) + CHECK_NULL(L_WEB, uris = strdup(param)); + uri = strtok_r(uris, ",", &ptr); + + if (!uri) { - 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); + DPRINTF(E_LOG, L_WEB, "Empty query parameter 'uris'\n"); + free(uris); + return -1; } - 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 { count = 0; @@ -1812,16 +1825,117 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) if (pos >= 0) pos += count; - ttl_count += count; + *total_count += count; } - while ((uri = strtok(NULL, ","))); + 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_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; + 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(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); @@ -1830,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; } @@ -2726,7 +2847,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); @@ -3181,6 +3302,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 */ 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)