[mpd] Refactor parsing filter/window arguments and add support for

"group" argument in "list" command
This commit is contained in:
chme 2018-09-19 19:36:39 +02:00 committed by ejurgensen
parent e015032292
commit f77c216650

532
src/mpd.c
View File

@ -59,6 +59,13 @@
#include "remote_pairing.h"
enum mpd_type {
MPD_TYPE_INT,
MPD_TYPE_STRING,
MPD_TYPE_SPECIAL,
};
#define MPD_ALL_IDLE_LISTENER_EVENTS (LISTENER_PLAYER | LISTENER_QUEUE | LISTENER_VOLUME | LISTENER_SPEAKER | LISTENER_OPTIONS | LISTENER_DATABASE | LISTENER_UPDATE | LISTENER_STORED_PLAYLIST | LISTENER_RATING)
#define MPD_RATING_FACTOR 10.0
@ -145,6 +152,65 @@ static const char * const ffmpeg_mime_types[] = { "application/flv", "applicatio
NULL
};
struct mpd_tagtype
{
char *tag;
char *field;
char *sort_field;
char *group_field;
enum mpd_type type;
ssize_t mfi_offset;
/*
* This allows omitting the "group" fields in the created group by clause to improve
* performance in the "list" command. For example listing albums and artists already
* groups by their persistent id, an additional group clause by artist/album will
* decrease performance of the select query and will in general not change the result
* (e. g. album persistent id is generated by artist and album and listing albums
* grouped by artist is therefor not necessary).
*/
bool group_in_listcommand;
};
static struct mpd_tagtype tagtypes[] =
{
/* tag | db field | db sort field | db group field | type | media_file offset | group_in_listcommand */
// We treat the artist tag as album artist, this allows grouping over the artist-persistent-id index and increases performance
// { "Artist", "f.artist", "f.artist", "f.artist", MPD_TYPE_STRING, dbmfi_offsetof(artist), },
{ "Artist", "f.album_artist", "f.album_artist_sort, f.album_artist", "f.songartistid", MPD_TYPE_STRING, dbmfi_offsetof(album_artist), false, },
{ "ArtistSort", "f.album_artist_sort", "f.album_artist_sort, f.album_artist", "f.songartistid", MPD_TYPE_STRING, dbmfi_offsetof(album_artist_sort), false, },
{ "AlbumArtist", "f.album_artist", "f.album_artist_sort, f.album_artist", "f.songartistid", MPD_TYPE_STRING, dbmfi_offsetof(album_artist), false, },
{ "AlbumArtistSort", "f.album_artist_sort", "f.album_artist_sort, f.album_artist", "f.songartistid", MPD_TYPE_STRING, dbmfi_offsetof(album_artist_sort), false, },
{ "Album", "f.album", "f.album_sort, f.album", "f.songalbumid", MPD_TYPE_STRING, dbmfi_offsetof(album), false, },
{ "Title", "f.title", "f.title", "f.title", MPD_TYPE_STRING, dbmfi_offsetof(title), true, },
{ "Track", "f.track", "f.track", "f.track", MPD_TYPE_INT, dbmfi_offsetof(track), true, },
{ "Genre", "f.genre", "f.genre", "f.genre", MPD_TYPE_STRING, dbmfi_offsetof(genre), true, },
{ "Disc", "f.disc", "f.disc", "f.disc", MPD_TYPE_INT, dbmfi_offsetof(disc), true, },
{ "Date", "f.year", "f.year", "f.year", MPD_TYPE_INT, dbmfi_offsetof(year), true, },
{ "file", NULL, NULL, NULL, MPD_TYPE_SPECIAL, -1, true, },
{ "base", NULL, NULL, NULL, MPD_TYPE_SPECIAL, -1, true, },
{ "any", NULL, NULL, NULL, MPD_TYPE_SPECIAL, -1, true, },
};
static struct mpd_tagtype *
find_tagtype(const char *tag)
{
int i;
if (!tag)
return 0;
for (i = 0; i < ARRAY_SIZE(tagtypes); i++)
{
if (strcasecmp(tag, tagtypes[i].tag) == 0)
return &tagtypes[i];
}
return NULL;
}
/*
* MPD client connection data
*/
@ -586,11 +652,34 @@ mpd_add_db_media_file_info(struct evbuffer *evbuf, struct db_media_file_info *db
return ret;
}
static int
mpd_get_query_params_find(int argc, char **argv, struct query_params *qp)
static void
append_string(char **a, const char *b, const char *separator)
{
char *temp;
if (*a)
temp = db_mprintf("%s%s%s", *a, (separator ? separator : ""), b);
else
temp = db_mprintf("%s", b);
free(*a);
*a = temp;
}
/*
* Sets the filter (where clause) and the window (limit clause) in the given query_params
* based on the given arguments
*
* @param argc Number of arguments in argv
* @param argv Pointer to the first filter parameter
* @param exact_match If true, creates filter for exact matches (e. g. find command) otherwise matches substrings (e. g. search command)
* @param qp Query parameters
*/
static int
parse_filter_window_params(int argc, char **argv, bool exact_match, struct query_params *qp)
{
struct mpd_tagtype *tagtype;
char *c1;
char *c2;
int start_pos;
int end_pos;
int i;
@ -598,221 +687,139 @@ mpd_get_query_params_find(int argc, char **argv, struct query_params *qp)
int ret;
c1 = NULL;
c2 = NULL;
for (i = 0; i < argc; i += 2)
{
if (0 == strcasecmp(argv[i], "any"))
{
c1 = db_mprintf("(f.artist LIKE '%%%q%%' OR f.album LIKE '%%%q%%' OR f.title LIKE '%%%q%%')", argv[i + 1], argv[i + 1], argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "file"))
{
c1 = db_mprintf("(f.virtual_path = '/%q')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "base"))
{
c1 = db_mprintf("(f.virtual_path LIKE '/%q%%')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "modified-since"))
{
DPRINTF(E_WARN, L_MPD, "Special parameter 'modified-since' is not supported by forked-daapd and will be ignored\n");
}
else if (0 == strcasecmp(argv[i], "window"))
{
ret = mpd_pars_range_arg(argv[i + 1], &start_pos, &end_pos);
if (ret == 0)
// End of filter key/value pairs reached, if keywords "window" or "group" found
if (0 == strcasecmp(argv[i], "window") || 0 == strcasecmp(argv[i], "group"))
break;
// Process filter key/value pair
if ((i + 1) < argc)
{
tagtype = find_tagtype(argv[i]);
if (!tagtype)
{
qp->idx_type = I_SUB;
qp->limit = end_pos - start_pos;
qp->offset = start_pos;
DPRINTF(E_WARN, L_MPD, "Parameter '%s' is not supported by forked-daapd and will be ignored\n", argv[i]);
continue;
}
else
if (tagtype->type == MPD_TYPE_STRING)
{
DPRINTF(E_LOG, L_MPD, "Window argument doesn't convert to integer or range: '%s'\n", argv[i + 1]);
if (exact_match)
c1 = db_mprintf("(%s = '%q')", tagtype->field, argv[i + 1]);
else
c1 = db_mprintf("(%s LIKE '%%%q%%')", tagtype->field, argv[i + 1]);
}
else if (tagtype->type == MPD_TYPE_INT)
{
ret = safe_atou32(argv[i + 1], &num);
if (ret < 0)
DPRINTF(E_WARN, L_MPD, "%s parameter '%s' is not an integer and will be ignored\n", tagtype->tag, argv[i + 1]);
else
c1 = db_mprintf("(%s = %d)", tagtype->field, num);
}
else if (tagtype->type == MPD_TYPE_SPECIAL)
{
if (0 == strcasecmp(tagtype->tag, "any"))
{
c1 = db_mprintf("(f.artist LIKE '%%%q%%' OR f.album LIKE '%%%q%%' OR f.title LIKE '%%%q%%')", argv[i + 1], argv[i + 1], argv[i + 1]);
}
else if (0 == strcasecmp(tagtype->tag, "file"))
{
if (exact_match)
c1 = db_mprintf("(f.virtual_path = '/%q')", argv[i + 1]);
else
c1 = db_mprintf("(f.virtual_path LIKE '%%%q%%')", argv[i + 1]);
}
else if (0 == strcasecmp(tagtype->tag, "base"))
{
c1 = db_mprintf("(f.virtual_path LIKE '/%q%%')", argv[i + 1]);
}
else
{
DPRINTF(E_WARN, L_MPD, "Unknown special parameter '%s' will be ignored\n", tagtype->tag);
}
}
}
else if (0 == strcasecmp(argv[i], "artist"))
{
c1 = db_mprintf("(f.artist = '%q')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "albumartist"))
{
c1 = db_mprintf("(f.album_artist = '%q')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "album"))
{
c1 = db_mprintf("(f.album = '%q')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "title"))
{
c1 = db_mprintf("(f.title = '%q')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "genre"))
{
c1 = db_mprintf("(f.genre = '%q')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "disc"))
{
ret = safe_atou32(argv[i + 1], &num);
if (ret < 0)
DPRINTF(E_WARN, L_MPD, "Disc parameter '%s' is not an integer and will be ignored\n", argv[i + 1]);
else
c1 = db_mprintf("(f.disc = %d)", num);
}
else if (0 == strcasecmp(argv[i], "track"))
{
ret = safe_atou32(argv[i + 1], &num);
if (ret < 0)
DPRINTF(E_WARN, L_MPD, "Track parameter '%s' is not an integer and will be ignored\n", argv[i + 1]);
else
c1 = db_mprintf("(f.track = %d)", num);
}
else if (0 == strcasecmp(argv[i], "date"))
{
ret = safe_atou32(argv[i + 1], &num);
if (ret < 0)
c1 = db_mprintf("(f.year = 0 OR f.year IS NULL)");
else
c1 = db_mprintf("(f.year = %d)", num);
}
else if (i == 0 && argc == 1)
{
{
// Special case: a single token is allowed if listing albums for an artist
c1 = db_mprintf("(f.album_artist = '%q')", argv[i]);
}
else
{
DPRINTF(E_WARN, L_MPD, "Parameter '%s' is not supported by forked-daapd and will be ignored\n", argv[i]);
{
DPRINTF(E_WARN, L_MPD, "Missing value for parameter '%s', ignoring '%s'\n", argv[i], argv[i]);
}
if (c1)
{
if (qp->filter)
c2 = db_mprintf("%s AND %s", qp->filter, c1);
else
c2 = db_mprintf("%s", c1);
{
append_string(&qp->filter, c1, " AND ");
free(qp->filter);
qp->filter = c2;
c2 = NULL;
free(c1);
c1 = NULL;
}
}
if ((i + 1) < argc && 0 == strcasecmp(argv[i], "window"))
{
ret = mpd_pars_range_arg(argv[i + 1], &start_pos, &end_pos);
if (ret == 0)
{
qp->idx_type = I_SUB;
qp->limit = end_pos - start_pos;
qp->offset = start_pos;
}
else
{
DPRINTF(E_LOG, L_MPD, "Window argument doesn't convert to integer or range: '%s'\n", argv[i + 1]);
}
i += 2;
}
return 0;
}
static int
mpd_get_query_params_search(int argc, char **argv, struct query_params *qp)
parse_group_params(int argc, char **argv, bool group_in_listcommand, struct query_params *qp, struct mpd_tagtype ***group, int *groupsize)
{
char *c1;
char *c2;
int start_pos;
int end_pos;
int first_group;
int i;
uint32_t num;
int ret;
int j;
struct mpd_tagtype *tagtype;
c1 = NULL;
c2 = NULL;
*groupsize = 0;
*group = NULL;
for (i = 0; i < argc; i += 2)
// Iterate through arguments to the first "group" argument
for (first_group = 0; first_group < argc; first_group++)
{
if (0 == strcasecmp(argv[i], "any"))
{
c1 = db_mprintf("(f.artist LIKE '%%%q%%' OR f.album LIKE '%%%q%%' OR f.title LIKE '%%%q%%')", argv[i + 1], argv[i + 1], argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "file"))
{
c1 = db_mprintf("(f.virtual_path LIKE '%%%q%%')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "base"))
{
c1 = db_mprintf("(f.virtual_path LIKE '/%q%%')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "modified-since"))
{
DPRINTF(E_WARN, L_MPD, "Special parameter 'modified-since' is not supported by forked-daapd and will be ignored\n");
}
else if (0 == strcasecmp(argv[i], "window"))
{
ret = mpd_pars_range_arg(argv[i + 1], &start_pos, &end_pos);
if (ret == 0)
if (0 == strcasecmp(argv[first_group], "group"))
break;
}
// Early return if no group keyword in arguments (or group keyword not followed by field argument)
if ((first_group + 1) >= argc || (argc - first_group) % 2 != 0)
return 0;
*groupsize = (argc - first_group) / 2;
*group = calloc(*groupsize, sizeof(struct mpd_tagtype *));
// Now process all group/field arguments
for (j = 0; j < (*groupsize); j++)
{
i = first_group + (j * 2);
if ((i + 1) < argc && 0 == strcasecmp(argv[i], "group"))
{
tagtype = find_tagtype(argv[i + 1]);
if (tagtype && tagtype->type != MPD_TYPE_SPECIAL)
{
qp->idx_type = I_SUB;
qp->limit = end_pos - start_pos;
qp->offset = start_pos;
if (group_in_listcommand)
append_string(&qp->group, tagtype->group_field, ", ");
(*group)[j] = tagtype;
}
else
{
DPRINTF(E_LOG, L_MPD, "Window argument doesn't convert to integer or range: '%s'\n", argv[i + 1]);
}
}
else if (0 == strcasecmp(argv[i], "artist"))
{
c1 = db_mprintf("(f.artist LIKE '%%%q%%')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "albumartist"))
{
c1 = db_mprintf("(f.album_artist LIKE '%%%q%%')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "album"))
{
c1 = db_mprintf("(f.album LIKE '%%%q%%')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "title"))
{
c1 = db_mprintf("(f.title LIKE '%%%q%%')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "genre"))
{
c1 = db_mprintf("(f.genre LIKE '%%%q%%')", argv[i + 1]);
}
else if (0 == strcasecmp(argv[i], "disc"))
{
ret = safe_atou32(argv[i + 1], &num);
if (ret < 0)
DPRINTF(E_WARN, L_MPD, "Disc parameter '%s' is not an integer and will be ignored\n", argv[i + 1]);
else
c1 = db_mprintf("(f.disc = %d)", num);
}
else if (0 == strcasecmp(argv[i], "track"))
{
ret = safe_atou32(argv[i + 1], &num);
if (ret < 0)
DPRINTF(E_WARN, L_MPD, "Track parameter '%s' is not an integer and will be ignored\n", argv[i + 1]);
else
c1 = db_mprintf("(f.track = %d)", num);
}
else if (0 == strcasecmp(argv[i], "date"))
{
ret = safe_atou32(argv[i + 1], &num);
if (ret < 0)
c1 = db_mprintf("(f.year = 0 OR f.year IS NULL)");
else
c1 = db_mprintf("(f.year = %d)", num);
}
else
{
DPRINTF(E_WARN, L_MPD, "Parameter '%s' is not supported by forked-daapd and will be ignored\n", argv[i]);
}
if (c1)
{
if (qp->filter)
c2 = db_mprintf("%s AND %s", qp->filter, c1);
else
c2 = db_mprintf("%s", c1);
free(qp->filter);
qp->filter = c2;
c2 = NULL;
free(c1);
c1 = NULL;
}
}
@ -2054,7 +2061,7 @@ mpd_command_playlistfind(struct evbuffer *evbuf, int argc, char **argv, char **e
return ACK_ERROR_ARG;
}
mpd_get_query_params_find(argc - 1, argv + 1, &query_params);
parse_filter_window_params(argc - 1, argv + 1, true, &query_params);
ret = db_queue_enum_start(&query_params);
if (ret < 0)
@ -2098,7 +2105,7 @@ mpd_command_playlistsearch(struct evbuffer *evbuf, int argc, char **argv, char *
return ACK_ERROR_ARG;
}
mpd_get_query_params_search(argc - 1, argv + 1, &query_params);
parse_filter_window_params(argc - 1, argv + 1, false, &query_params);
ret = db_queue_enum_start(&query_params);
if (ret < 0)
@ -2598,7 +2605,7 @@ mpd_command_count(struct evbuffer *evbuf, int argc, char **argv, char **errmsg,
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_COUNT_ITEMS;
mpd_get_query_params_find(argc - 1, argv + 1, &qp);
parse_filter_window_params(argc - 1, argv + 1, true, &qp);
ret = db_filecount_get(&fci, &qp);
if (ret < 0)
@ -2640,7 +2647,7 @@ mpd_command_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s
qp.sort = S_NAME;
qp.idx_type = I_NONE;
mpd_get_query_params_find(argc - 1, argv + 1, &qp);
parse_filter_window_params(argc - 1, argv + 1, true, &qp);
ret = db_query_start(&qp);
if (ret < 0)
@ -2686,7 +2693,7 @@ mpd_command_findadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg
qp.sort = S_ARTIST;
qp.idx_type = I_NONE;
mpd_get_query_params_find(argc - 1, argv + 1, &qp);
parse_filter_window_params(argc - 1, argv + 1, true, &qp);
player_get_status(&status);
@ -2704,11 +2711,13 @@ mpd_command_findadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg
static int
mpd_command_list(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx)
{
struct mpd_tagtype *tagtype;
struct query_params qp;
struct db_group_info dbgri;
char *type;
char *browse_item;
char *sort_item;
struct mpd_tagtype **group;
int groupsize;
struct db_media_file_info dbmfi;
char **strval;
int i;
int ret;
if (argc < 2 || ((argc % 2) != 0))
@ -2720,111 +2729,77 @@ mpd_command_list(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s
}
}
memset(&qp, 0, sizeof(struct query_params));
tagtype = find_tagtype(argv[1]);
if (0 == strcasecmp(argv[1], "artist"))
{
qp.type = Q_GROUP_ARTISTS;
qp.sort = S_ARTIST;
type = "Artist: ";
}
else if (0 == strcasecmp(argv[1], "albumartist"))
{
qp.type = Q_GROUP_ARTISTS;
qp.sort = S_ARTIST;
type = "AlbumArtist: ";
}
else if (0 == strcasecmp(argv[1], "album"))
{
qp.type = Q_GROUP_ALBUMS;
qp.sort = S_ALBUM;
type = "Album: ";
}
else if (0 == strcasecmp(argv[1], "date"))
{
qp.type = Q_BROWSE_YEARS;
type = "Date: ";
}
else if (0 == strcasecmp(argv[1], "genre"))
{
qp.type = Q_BROWSE_GENRES;
type = "Genre: ";
}
else if (0 == strcasecmp(argv[1], "disc"))
{
qp.type = Q_BROWSE_DISCS;
type = "Disc: ";
}
else if (0 == strcasecmp(argv[1], "track"))
{
qp.type = Q_BROWSE_TRACKS;
type = "Track: ";
}
else if (0 == strcasecmp(argv[1], "file"))
{
qp.type = Q_BROWSE_VPATH;
type = "file: ";
}
else
if (!tagtype || tagtype->type == MPD_TYPE_SPECIAL) //FIXME allow "file" tagtype
{
DPRINTF(E_WARN, L_MPD, "Unsupported type argument for command 'list': %s\n", argv[1]);
return 0;
}
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_ITEMS;
qp.idx_type = I_NONE;
qp.order = tagtype->sort_field;
qp.group = strdup(tagtype->group_field);
if (argc > 2)
{
mpd_get_query_params_find(argc - 2, argv + 2, &qp);
parse_filter_window_params(argc - 2, argv + 2, true, &qp);
}
group = NULL;
groupsize = 0;
parse_group_params(argc - 2, argv + 2, tagtype->group_in_listcommand, &qp, &group, &groupsize);
ret = db_query_start(&qp);
if (ret < 0)
{
db_query_end(&qp);
free(qp.filter);
free(qp.group);
free(group);
*errmsg = safe_asprintf("Could not start query");
return ACK_ERROR_UNKNOWN;
}
if (qp.type & Q_F_BROWSE)
while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id))
{
if (qp.type == Q_BROWSE_VPATH)
strval = (char **) ((char *)&dbmfi + tagtype->mfi_offset);
if (!(*strval) || (**strval == '\0'))
continue;
evbuffer_add_printf(evbuf,
"%s: %s\n",
tagtype->tag,
*strval);
if (group && groupsize > 0)
{
while (((ret = db_query_fetch_string_sort(&qp, &browse_item, &sort_item)) == 0) && (browse_item))
for (i = 0; i < groupsize; i++)
{
// Remove the first "/" from the virtual_path
evbuffer_add_printf(evbuf,
"%s%s\n",
type,
(browse_item + 1));
if (!group[i])
continue;
strval = (char **) ((char *)&dbmfi + group[i]->mfi_offset);
if (!(*strval) || (**strval == '\0'))
continue;
evbuffer_add_printf(evbuf,
"%s: %s\n",
group[i]->tag,
*strval);
}
}
else
{
while (((ret = db_query_fetch_string_sort(&qp, &browse_item, &sort_item)) == 0) && (browse_item))
{
evbuffer_add_printf(evbuf,
"%s%s\n",
type,
browse_item);
}
}
}
else
{
while ((ret = db_query_fetch_group(&qp, &dbgri)) == 0)
{
evbuffer_add_printf(evbuf,
"%s%s\n",
type,
dbgri.itemname);
}
}
db_query_end(&qp);
free(qp.filter);
free(qp.group);
free(group);
return 0;
}
@ -3140,7 +3115,7 @@ mpd_command_search(struct evbuffer *evbuf, int argc, char **argv, char **errmsg,
qp.sort = S_NAME;
qp.idx_type = I_NONE;
mpd_get_query_params_search(argc - 1, argv + 1, &qp);
parse_filter_window_params(argc - 1, argv + 1, false, &qp);
ret = db_query_start(&qp);
if (ret < 0)
@ -3186,7 +3161,7 @@ mpd_command_searchadd(struct evbuffer *evbuf, int argc, char **argv, char **errm
qp.sort = S_ARTIST;
qp.idx_type = I_NONE;
mpd_get_query_params_search(argc - 1, argv + 1, &qp);
parse_filter_window_params(argc - 1, argv + 1, false, &qp);
player_get_status(&status);
@ -3962,16 +3937,13 @@ mpd_command_commands(struct evbuffer *evbuf, int argc, char **argv, char **errms
static int
mpd_command_tagtypes(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx)
{
evbuffer_add_printf(evbuf,
"tagtype: Artist\n"
"tagtype: AlbumArtist\n"
"tagtype: ArtistSort\n"
"tagtype: AlbumArtistSort\n"
"tagtype: Album\n"
"tagtype: Title\n"
"tagtype: Track\n"
"tagtype: Genre\n"
"tagtype: Disc\n");
int i;
for (i = 0; i < ARRAY_SIZE(tagtypes); i++)
{
if (tagtypes[i].type != MPD_TYPE_SPECIAL)
evbuffer_add_printf(evbuf, "tagtype: %s\n", tagtypes[i].tag);
}
return 0;
}