Merge pull request #637 from chme/jsonapi_files

New JSON API endpoint for files view
This commit is contained in:
chme 2018-12-23 09:28:50 +01:00 committed by GitHub
commit 0eae6d710c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 376 additions and 28 deletions

View File

@ -587,7 +587,12 @@ POST /api/queue/items/add
**Response**
On success returns the HTTP `204 No Content` success status response code.
On success returns the HTTP `200 OK` success status response code.
| Key | Type | Value |
| --------------- | -------- | ----------------------------------------- |
| count | integer | number of tracks added to the queue |
**Example**
@ -595,6 +600,12 @@ On success returns the HTTP `204 No Content` success status response code.
curl -X POST "http://localhost:3689/api/queue/items/add?uris=library:playlist:68,library:artist:2932599850102967727"
```
```json
{
"count": 42
}
```
### Moving a queue item
@ -673,6 +684,7 @@ curl -X PUT "http://localhost:3689/api/queue/items/2"
| GET | [/api/library/albums/{id}/tracks](#list-album-tracks) | Get list of tracks for an album |
| GET | [/api/library/genres](#list-genres) | Get list of genres |
| GET | [/api/library/count](#get-count-of-tracks-artists-and-albums) | Get count of tracks, artists and albums |
| GET | [/api/library/files](#list-local-directories) | Get list of directories in the local library |
| GET | [/api/update](#trigger-rescan) | Trigger a library rescan |
@ -1205,7 +1217,7 @@ curl -X GET "http://localhost:3689/api/library/albums/1/tracks"
```
### list genres
### List genres
Get list of genres
@ -1377,6 +1389,105 @@ curl -X GET "http://localhost:3689/api/library/count?expression=data_kind+is+fil
```
### List local directories
List the local directories and the directory contents (tracks and playlists)
**Endpoint**
```http
GET /api/library/files
```
**Query parameters**
| Parameter | Value |
| --------------- | ----------------------------------------------------------- |
| directory | *(Optional)* A path to a directory in your local library. |
**Response**
| Key | Type | Value |
| --------------- | -------- | ----------------------------------------- |
| directories | array | Array of [`directory`](#directory-object) objects containing the sub directories |
| tracks | object | [`paging`](#paging-object) object containing [`track`](#track-object) objects that matches the `directory` |
| playlists | object | [`paging`](#paging-object) object containing [`playlist`](#playlist-object) objects that matches the `directory` |
**Example**
```shell
curl -X GET "http://localhost:3689/api/library/files?directory=/music/srv"
```
```json
{
"directories": [
{
"path": "/music/srv/Audiobooks"
},
{
"path": "/music/srv/Music"
},
{
"path": "/music/srv/Playlists"
},
{
"path": "/music/srv/Podcasts"
}
],
"tracks": {
"items": [
{
"id": 1,
"title": "input.pipe",
"artist": "Unknown artist",
"artist_sort": "Unknown artist",
"album": "Unknown album",
"album_sort": "Unknown album",
"album_id": "4201163758598356043",
"album_artist": "Unknown artist",
"album_artist_sort": "Unknown artist",
"album_artist_id": "4187901437947843388",
"genre": "Unknown genre",
"year": 0,
"track_number": 0,
"disc_number": 0,
"length_ms": 0,
"play_count": 0,
"skip_count": 0,
"time_added": "2018-11-24T08:41:35Z",
"seek_ms": 0,
"media_kind": "music",
"data_kind": "pipe",
"path": "/music/srv/input.pipe",
"uri": "library:track:1",
"artwork_url": "/artwork/item/1"
}
],
"total": 1,
"offset": 0,
"limit": -1
},
"playlists": {
"items": [
{
"id": 8,
"name": "radio",
"path": "/music/srv/radio.m3u",
"smart_playlist": true,
"uri": "library:playlist:8"
}
],
"total": 1,
"offset": 0,
"limit": -1
}
}
```
### Trigger rescan
Trigger a library rescan
@ -1774,6 +1885,7 @@ curl --include \
| ------------------ | -------- | ----------------------------------------- |
| id | string | Track id |
| title | string | Title |
| title_sort | string | Sort title |
| artist | string | Track artist name |
| artist_sort | string | Track artist sort name |
| album | string | Album name |
@ -1788,8 +1900,11 @@ curl --include \
| track_number | integer | Track number |
| disc_number | integer | Disc number |
| length_ms | integer | Track length in milliseconds |
| rating | integer | Track rating (ranges from 0 to 100) |
| play_count | integer | How many times the track was played |
| skip_count | integer | How many times the track was skipped |
| time_played | string | Timestamp in `ISO 8601` format |
| time_skipped | string | Timestamp in `ISO 8601` format |
| time_added | string | Timestamp in `ISO 8601` format |
| date_released | string | Date in the format `yyyy-mm-dd` |
| seek_ms | integer | Resume point in milliseconds (available only for podcasts and audiobooks) |
@ -1817,6 +1932,12 @@ curl --include \
| name | string | Name of genre |
### `directory` object
| Key | Type | Value |
| --------------- | -------- | ----------------------------------------- |
| path | string | Directory path |
### Artwork urls
@ -1825,4 +1946,4 @@ Absolute artwork urls are pointing to external artwork images (e. g. for radio s
It is possible to add the query parameters `maxwidth` and/or `maxheight` to relative artwork urls, in order to get a smaller image (forked-daapd only scales down never up).
Note that even if a relative artwork url attribute is present, it is not guaranteed to exist.
Note that even if a relative artwork url attribute is present, it is not guaranteed to exist.

View File

@ -3850,7 +3850,7 @@ db_group_persistentid_byid(int id, int64_t *persistentid)
/* Directories */
int
db_directory_id_byvirtualpath(char *virtual_path)
db_directory_id_byvirtualpath(const char *virtual_path)
{
#define Q_TMPL "SELECT d.id FROM directories d WHERE d.virtual_path = '%q';"
char *query;
@ -3873,6 +3873,30 @@ db_directory_id_byvirtualpath(char *virtual_path)
#undef Q_TMPL
}
int
db_directory_id_bypath(const char *path)
{
#define Q_TMPL "SELECT d.id FROM directories d WHERE d.path = '%q';"
char *query;
int ret;
query = sqlite3_mprintf(Q_TMPL, path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
return 0;
}
ret = db_file_id_byquery(query);
sqlite3_free(query);
return ret;
#undef Q_TMPL
}
int
db_directory_enum_start(struct directory_enum *de)
{
@ -3944,6 +3968,7 @@ db_directory_enum_fetch(struct directory_enum *de, struct directory_info *di)
disabled = sqlite3_column_int64(de->stmt, 3);
di->disabled = (disabled != 0);
di->parent_id = sqlite3_column_int(de->stmt, 4);
di->path = (char *)sqlite3_column_text(de->stmt, 5);
return 0;
}
@ -3961,8 +3986,8 @@ db_directory_enum_end(struct directory_enum *de)
static int
db_directory_add(struct directory_info *di, int *id)
{
#define QADD_TMPL "INSERT INTO directories (virtual_path, db_timestamp, disabled, parent_id)" \
" VALUES (TRIM(%Q), %d, %d, %d);"
#define QADD_TMPL "INSERT INTO directories (virtual_path, db_timestamp, disabled, parent_id, path)" \
" VALUES (TRIM(%Q), %d, %d, %d, TRIM(%Q));"
char *query;
char *errmsg;
@ -3977,7 +4002,7 @@ db_directory_add(struct directory_info *di, int *id)
DPRINTF(E_LOG, L_DB, "Directory name ends with space: '%s'\n", di->virtual_path);
}
query = sqlite3_mprintf(QADD_TMPL, di->virtual_path, di->db_timestamp, di->disabled, di->parent_id);
query = sqlite3_mprintf(QADD_TMPL, di->virtual_path, di->db_timestamp, di->disabled, di->parent_id, di->path);
if (!query)
{
@ -4016,14 +4041,14 @@ db_directory_add(struct directory_info *di, int *id)
static int
db_directory_update(struct directory_info *di)
{
#define QADD_TMPL "UPDATE directories SET virtual_path = TRIM(%Q), db_timestamp = %d, disabled = %d, parent_id = %d" \
#define QADD_TMPL "UPDATE directories SET virtual_path = TRIM(%Q), db_timestamp = %d, disabled = %d, parent_id = %d, path = TRIM(%Q)" \
" WHERE id = %d;"
char *query;
char *errmsg;
int ret;
/* Add */
query = sqlite3_mprintf(QADD_TMPL, di->virtual_path, di->db_timestamp, di->disabled, di->parent_id, di->id);
query = sqlite3_mprintf(QADD_TMPL, di->virtual_path, di->db_timestamp, di->disabled, di->parent_id, di->path, di->id);
if (!query)
{
@ -4053,7 +4078,7 @@ db_directory_update(struct directory_info *di)
}
int
db_directory_addorupdate(char *virtual_path, int disabled, int parent_id)
db_directory_addorupdate(char *virtual_path, char *path, int disabled, int parent_id)
{
struct directory_info di;
int id;
@ -4064,6 +4089,7 @@ db_directory_addorupdate(char *virtual_path, int disabled, int parent_id)
di.id = id;
di.parent_id = parent_id;
di.virtual_path = virtual_path;
di.path = path;
di.disabled = disabled;
di.db_timestamp = (uint64_t)time(NULL);

View File

@ -412,6 +412,7 @@ enum directory_ids {
struct directory_info {
uint32_t id;
char *virtual_path;
char *path;
uint32_t db_timestamp;
uint32_t disabled;
uint32_t parent_id;
@ -698,7 +699,10 @@ db_group_persistentid_byid(int id, int64_t *persistentid);
/* Directories */
int
db_directory_id_byvirtualpath(char *virtual_path);
db_directory_id_byvirtualpath(const char *virtual_path);
int
db_directory_id_bypath(const char *path);
int
db_directory_enum_start(struct directory_enum *de);
@ -710,7 +714,7 @@ void
db_directory_enum_end(struct directory_enum *de);
int
db_directory_addorupdate(char *virtual_path, int disabled, int parent_id);
db_directory_addorupdate(char *virtual_path, char *path, int disabled, int parent_id);
void
db_directory_ping_bymatch(char *virtual_path);

View File

@ -161,7 +161,8 @@
" virtual_path VARCHAR(4096) NOT NULL," \
" db_timestamp INTEGER DEFAULT 0," \
" disabled INTEGER DEFAULT 0," \
" parent_id INTEGER DEFAULT 0" \
" parent_id INTEGER DEFAULT 0," \
" path VARCHAR(4096) DEFAULT NULL" \
");"
#define T_QUEUE \
@ -239,17 +240,17 @@
#define Q_DIR1 \
"INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \
" VALUES (1, '/', 0, 0, 0);"
"INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id, path)" \
" VALUES (1, '/', 0, 0, 0, NULL);"
#define Q_DIR2 \
"INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \
" VALUES (2, '/file:', 0, 0, 1);"
"INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id, path)" \
" VALUES (2, '/file:', 0, 0, 1, '/');"
#define Q_DIR3 \
"INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \
" VALUES (3, '/http:', 0, 0, 1);"
"INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id, path)" \
" VALUES (3, '/http:', 0, 0, 1, NULL);"
#define Q_DIR4 \
"INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id)" \
" VALUES (4, '/spotify:', 0, 4294967296, 1);"
"INSERT INTO directories (id, virtual_path, db_timestamp, disabled, parent_id, path)" \
" VALUES (4, '/spotify:', 0, 4294967296, 1, NULL);"
#define Q_QUEUE_VERSION \
"INSERT INTO admin (key, value) VALUES ('queue_version', '0');"

View File

@ -26,7 +26,7 @@
* is a major upgrade. In other words minor version upgrades permit downgrading
* forked-daapd after the database was upgraded. */
#define SCHEMA_VERSION_MAJOR 19
#define SCHEMA_VERSION_MINOR 11
#define SCHEMA_VERSION_MINOR 12
int
db_init_indices(sqlite3 *hdl);

View File

@ -1698,6 +1698,27 @@ static const struct db_upgrade_query db_upgrade_v1911_queries[] =
};
#define U_V1912_ALTER_DIRECTORIES_ADD_PATH \
"ALTER TABLE directories ADD COLUMN path VARCHAR(4096) DEFAULT NULL;"
#define U_V1912_UPDATE_FILE_DIRECTORIES_PATH \
"UPDATE directories SET path = SUBSTR(path, 7) WHERE virtual_path like '/file:/%';"
#define U_V1912_UPDATE_FILE_ROOT_PATH \
"UPDATE directories SET path = '/' WHERE virtual_path = '/file:';"
#define U_V1912_SCVER_MINOR \
"UPDATE admin SET value = '12' WHERE key = 'schema_version_minor';"
static const struct db_upgrade_query db_upgrade_v1912_queries[] =
{
{ U_V1912_ALTER_DIRECTORIES_ADD_PATH, "alter table directories add column path" },
{ U_V1912_UPDATE_FILE_DIRECTORIES_PATH, "set paths for '/file:' directories" },
{ U_V1912_UPDATE_FILE_ROOT_PATH, "set path for '/file:' directory" },
{ U_V1912_SCVER_MINOR, "set schema_version_minor to 12" },
};
int
db_upgrade(sqlite3 *hdl, int db_ver)
{
@ -1884,6 +1905,14 @@ db_upgrade(sqlite3 *hdl, int db_ver)
ret = db_generic_upgrade(hdl, db_upgrade_v1911_queries, ARRAY_SIZE(db_upgrade_v1911_queries));
if (ret < 0)
return -1;
/* FALLTHROUGH */
case 1911:
ret = db_generic_upgrade(hdl, db_upgrade_v1912_queries, ARRAY_SIZE(db_upgrade_v1912_queries));
if (ret < 0)
return -1;
break;
default:

View File

@ -182,6 +182,7 @@ track_to_json(struct db_media_file_info *dbmfi)
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);
@ -197,6 +198,7 @@ track_to_json(struct db_media_file_info *dbmfi)
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);
@ -266,6 +268,24 @@ genre_to_json(const char *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)
@ -529,6 +549,38 @@ fetch_genres(struct query_params *query_params, json_object *items, int *total)
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)
@ -1275,6 +1327,7 @@ static int
play_item_at_position(const char *param)
{
uint32_t position;
struct player_status status;
struct db_queue_item *queue_item;
int ret;
@ -1286,7 +1339,9 @@ play_item_at_position(const char *param)
return HTTP_BADREQUEST;
}
queue_item = db_queue_fetch_bypos(position, 0);
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);
@ -1690,6 +1745,7 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq)
const char *id;
int pos = -1;
int count = 0;
json_object *reply;
int ret = 0;
@ -1759,10 +1815,19 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq)
free(uris);
if (ret == 0)
{
reply = json_object_new_object();
json_object_object_add(reply, "count", json_object_new_int(count));
ret = evbuffer_add_printf(hreq->reply, "%s", json_object_to_json_string(reply));
jparse_free(reply);
}
if (ret < 0)
return HTTP_INTERNAL;
return HTTP_NOCONTENT;
return HTTP_OK;
}
static int
@ -2609,6 +2674,107 @@ jsonapi_reply_library_count(struct httpd_request *hreq)
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_NAME;
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_PLAYLIST;
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)
{
@ -2977,6 +3143,7 @@ static struct httpd_uri_map adm_handlers[] =
{ EVHTTP_REQ_GET, "^/api/library/albums/[[:digit:]]+/tracks$", jsonapi_reply_library_album_tracks },
{ 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 },

View File

@ -749,7 +749,7 @@ process_directory(char *path, int parent_id, int flags)
if (ret < 0)
return;
dir_id = db_directory_addorupdate(virtual_path, 0, parent_id);
dir_id = db_directory_addorupdate(virtual_path, path, 0, parent_id);
if (dir_id <= 0)
{
DPRINTF(E_LOG, L_SCAN, "Insert or update of directory failed '%s'\n", virtual_path);
@ -876,7 +876,7 @@ process_parent_directories(char *path)
if (ret < 0)
return 0;
dir_id = db_directory_addorupdate(virtual_path, 0, dir_id);
dir_id = db_directory_addorupdate(virtual_path, buf, 0, dir_id);
if (dir_id <= 0)
{
DPRINTF(E_LOG, L_SCAN, "Insert or update of directory failed '%s'\n", virtual_path);

View File

@ -1250,7 +1250,7 @@ prepare_directories(const char *artist, const char *album)
DPRINTF(E_LOG, L_SPOTIFY, "Virtual path exceeds PATH_MAX (/spotify:/%s)\n", artist);
return -1;
}
dir_id = db_directory_addorupdate(virtual_path, 0, DIR_SPOTIFY);
dir_id = db_directory_addorupdate(virtual_path, NULL, 0, DIR_SPOTIFY);
if (dir_id <= 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Could not add or update directory '%s'\n", virtual_path);
@ -1262,7 +1262,7 @@ prepare_directories(const char *artist, const char *album)
DPRINTF(E_LOG, L_SPOTIFY, "Virtual path exceeds PATH_MAX (/spotify:/%s/%s)\n", artist, album);
return -1;
}
dir_id = db_directory_addorupdate(virtual_path, 0, dir_id);
dir_id = db_directory_addorupdate(virtual_path, NULL, 0, dir_id);
if (dir_id <= 0)
{
DPRINTF(E_LOG, L_SPOTIFY, "Could not add or update directory '%s'\n", virtual_path);