diff --git a/src/mpd.c b/src/mpd.c index d7acdd70..6c098fcd 100644 --- a/src/mpd.c +++ b/src/mpd.c @@ -113,6 +113,13 @@ enum command_list_type COMMAND_LIST_NONE = 4 }; +enum position_type +{ + POSITION_ABSOLUTE = 1, + POSITION_RELATIVE_BEFORE, + POSITION_RELATIVE_AFTER +}; + /** * This lists for ffmpeg suffixes and mime types are taken from the ffmpeg decoder plugin from mpd * (FfmpegDecoderPlugin.cxx, git revision 9fb351a139a56fc7b1ece549894f8fc31fa887cd). @@ -174,6 +181,7 @@ struct mpd_tagtype bool group_in_listcommand; }; +/* https://mpd.readthedocs.io/en/latest/protocol.html#tags */ static struct mpd_tagtype tagtypes[] = { /* tag | db field | db sort field | db group field | type | media_file offset | group_in_listcommand */ @@ -182,14 +190,34 @@ static struct mpd_tagtype tagtypes[] = // { "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, }, + { "Album", "f.album", "f.album_sort, f.album", "f.songalbumid", MPD_TYPE_STRING, dbmfi_offsetof(album), false, }, + { "AlbumSort", "f.album_sort", "f.album_sort, f.album", "f.songalbumid", MPD_TYPE_STRING, dbmfi_offsetof(album), 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, }, + { "Title", "f.title", "f.title", "f.title_sort", MPD_TYPE_STRING, dbmfi_offsetof(title), true, }, + { "TitleSort", "f.title_sort", "f.title", "f.title_sort", MPD_TYPE_STRING, dbmfi_offsetof(title), true, }, { "Track", "f.track", "f.track", "f.track", MPD_TYPE_INT, dbmfi_offsetof(track), true, }, + { "Name", "f.title", "f.title_sort", "f.title", MPD_TYPE_STRING, dbmfi_offsetof(genre), 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, }, + /* mood */ { "Date", "f.year", "f.year", "f.year", MPD_TYPE_INT, dbmfi_offsetof(year), true, }, + { "OriginalDate", "f.date_released", "f.date_released", "f.date_released", MPD_TYPE_INT, dbmfi_offsetof(date_released), true, }, + { "Composer", "f.composer", "f.composer_sort", "f.composer", MPD_TYPE_STRING, dbmfi_offsetof(composer), true, }, + { "ComposerSort", "f.composer_sort", "f.composer_sort", "f.composer_sort", MPD_TYPE_STRING, dbmfi_offsetof(composer_sort), true, }, + /* performer */ + { "Conductor", "f.conductor", "f.conductor", "f.conductor", MPD_TYPE_STRING, dbmfi_offsetof(conductor), true, }, + /* work */ + /* ensemble */ + /* movement */ + /* movementnumber */ + /* location */ + { "Grouping", "f.grouping", "f.grouping", "f.grouping", MPD_TYPE_STRING, dbmfi_offsetof(grouping), true, }, + { "Comment", "f.comment", "f.comment", "f.comment", MPD_TYPE_STRING, dbmfi_offsetof(comment), true, }, + { "Disc", "f.disc", "f.disc", "f.disc", MPD_TYPE_INT, dbmfi_offsetof(disc), true, }, + /* label */ + /* musicbrainz_* */ + /* below are pseudo tags not defined in the docs but used in + * examples */ { "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, }, @@ -467,6 +495,38 @@ mpd_pars_quoted(char **input) return arg; } +/** + * Helper for writing binary responses. + * https://mpd.readthedocs.io/en/latest/protocol.html#binary + * This helper writes the size line, and binary blocks respecting the + * binarylimit. + */ +static bool +mpd_write_binary_response(struct mpd_client_ctx *ctx, + struct evbuffer *output, + struct evbuffer *data, + size_t offset) +{ + unsigned char *p; + size_t len = evbuffer_get_length(data); + + if (len == 0 || len < offset) + return false; + + /* write header for total size */ + evbuffer_add_printf(output, "size: %zu\n", len); + + len = MIN(len - offset, ctx->binarylimit); + evbuffer_drain(data, offset); + p = evbuffer_pullup(data, len); + evbuffer_add_printf(output, "binary: %zu\n", len); + evbuffer_add(output, p, len); + evbuffer_add(output, "\n", 1); + evbuffer_drain(data, len); + + return true; +} + /* * Parses the argument string into an array of strings. * Arguments are seperated by a whitespace character and may be wrapped in double quotes. @@ -680,171 +740,879 @@ append_string(char **a, const char *b, const char *separator) } /* - * Sets the filter (where clause) and the window (limit clause) in the given query_params - * based on the given arguments + * Computes the absolute position of a relative position. This is a + * feature introduced since MPD 0.23 where + or - for position can be + * used to indicate relative to the currently selected (playing/paused) + * song. + * This feature does the necessary lookups to resolve the current song + * and calculate the absolute position. When ptype is POSITION_ABSOLUTE + * this function acts as a noop and simply returns position. + */ +static int +mpd_get_relative_queue_pos(enum position_type ptype, int position) +{ + struct player_status status; + struct db_queue_item *queue_item; + uint32_t curpos; + + /* shortcut absolute case */ + if (ptype == POSITION_ABSOLUTE) + return position; + + player_get_status(&status); + + curpos = 0; + if (status.status != PLAY_STOPPED) + { + queue_item = db_queue_fetch_byitemid(status.item_id); + if (queue_item != NULL) + { + if (queue_item->id > 0) + curpos = queue_item->pos; + + free_queue_item(queue_item, 0); + } + } + + /* +0 inserts right after the current song */ + if (ptype == POSITION_RELATIVE_AFTER) + position = curpos + position + 1; + else if (ptype == POSITION_RELATIVE_BEFORE) + position = curpos - position; + + DPRINTF(E_DBG, L_MPD, + "current song: %d->%d, relative new position: %d\n", + status.item_id, curpos, position); + + return position; +} + +struct mpd_cmd_params { + int params_allow; + int params_set; + struct query_params qp; + struct mpd_tagtype **groups; + int groupssize; + int groupslen; + bool addgroupfilter; + bool exactmatch; + int pos; +}; + +enum mpd_param_cmd { + CMD_UNSET = 0 << 0, + CMD_WINDOW = 1 << 0, + CMD_GROUP = 1 << 1, + CMD_POSITION = 1 << 2, + CMD_SORT = 1 << 3, + CMD_FILTER = 1 << 4 +}; + +/** + * {START:END} * - * @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 + * parse START and END as integer numbers and store in query_params as + * limit and offset */ static int -parse_filter_window_params(int argc, char **argv, bool exact_match, struct query_params *qp) +mpd_parse_cmd_window(char *arg, struct mpd_cmd_params *param) { - struct mpd_tagtype *tagtype; - char *c1; + struct query_params *qp = ¶m->qp; int start_pos; int end_pos; - int i; - uint32_t num; int ret; - c1 = NULL; + ret = mpd_pars_range_arg(arg, &start_pos, &end_pos); + if (ret == 0 && qp != NULL) + { + qp->idx_type = I_SUB; + qp->limit = end_pos - start_pos; + qp->offset = start_pos; - for (i = 0; i < argc; i += 2) + param->params_set |= CMD_WINDOW; + } + else { - // 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; + DPRINTF(E_LOG, L_MPD, + "Window argument doesn't convert " + "to integer or range: '%s'\n", arg); + return 1; + } + + return 0; +} - // Process filter key/value pair - if ((i + 1) < argc) - { - tagtype = find_tagtype(argv[i]); +/** + * {GROUPTYPE} + * + * parse GROUPTYPE as tagtype (album, artist, etc) and store in groups + * and increment groupslen. It is the callers responsibility to ensure + * groups is allocated and has sufficient space, else results are + * silently dropped. If addgroupfilter is requested, the group argument + * will be appended to (with comma-space separation) for e.g. ORDER BY use. + */ +static int +mpd_parse_cmd_group(char *arg, struct mpd_cmd_params *param) +{ + struct query_params *qp = ¶m->qp; + struct mpd_tagtype *tagtype = find_tagtype(arg); + + if (tagtype != NULL && tagtype->type != MPD_TYPE_SPECIAL) + { + if (param->addgroupfilter) + append_string(&qp->group, tagtype->group_field, ", "); + + /* caller should ensure sufficient memory was allocated */ + if (param->groupslen < param->groupssize) + { + param->groups[param->groupslen] = tagtype; + param->groupslen++; + } + + param->params_set |= CMD_GROUP; + } + + return 0; +} + +/** + * {POSITION} + * + * parse POSITION as an integer number and store the result in pos from + * mpd_cmd_params. If POSITION starts with '+' or '-', the number + * following the sign is considered relative to the current song. As + * such, its value is resolved and stored in pos instead. + */ +static int +mpd_parse_cmd_position(char *arg, struct mpd_cmd_params *param) +{ + enum position_type ptype = POSITION_ABSOLUTE; + int to_pos; + int ret; + + if (*arg == '-') + { + ptype = POSITION_RELATIVE_BEFORE; + arg++; + } + else if (*arg == '+') + { + ptype = POSITION_RELATIVE_AFTER; + arg++; + } + + ret = safe_atoi32(arg, &to_pos); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, + "Argument doesn't convert to integer: '%s'\n", arg); + return 1; + } + else + { + param->pos = mpd_get_relative_queue_pos(ptype, to_pos); + param->params_set |= CMD_POSITION; + } + + return 0; +} + +/** + * {(TAG [OP] VALUE)} + * + * parse filter expression on THING being VALUE in relation to EXPR. + * The possible expressions can be found at: + * https://mpd.readthedocs.io/en/latest/protocol.html#filter-syntax + * The result is stored in filter member from query_params, and appended + * to create one compound SQL WHERE-condition. + * + * NOTE: this command differs from the others in that it isn't prefixed + * by some tag to indicate what the type is and that there are + * single-argument filters (as opposed to key/value), thus the filter + * command is run for as long as no other known tag is found. + * + * The parsed input here comes from e.g. the find command: + * find "((album == \"Flash Gordon\"))" (post v0.21) + * find album "Flash Gordon" (<= v0.21) + * and we deal with + * argv[1]: ((album == "Flash Gordon")) (post v0.21) + * argv[1]: album argv[2]: Flash Gordon (<= v0.21) + * here. + */ +static int +mpd_parse_cmd_filter(char *arg, char *narg, struct mpd_cmd_params *param) +{ + char *condition = NULL; + bool exact_match = param->exactmatch; + size_t len = 0; + + /* determine if we're using v0.21 syntax */ + if (arg[0] == '(' && (len = strlen(arg)) > 2 && arg[len - 1] == ')') + { + bool negate = false; + char *p; + char *q; + char *val = NULL; + char *argend = &arg[len - 1]; + struct mpd_tagtype *tagtype = NULL; + enum parsestate { + STATE_INIT, + STATE_EXPR, + STATE_FINI, + STATE_OP, + STATE_VAL + } state = STATE_INIT; + enum operator { /* CI: case-insensitive, CS, case-sensitive */ + OP_NONE, + OP_EQUALS, /* order below matters for promotion to CI/CS */ + OP_EQUALS_CI, + OP_EQUALS_CS, + OP_NEQUALS, + OP_NEQUALS_CI, + OP_NEQUALS_CS, + OP_CONTAINS, + OP_CONTAINS_CI, + OP_CONTAINS_CS, + OP_NCONTAINS, + OP_NCONTAINS_CI, + OP_NCONTAINS_CS, + OP_STARTSWITH, + OP_STARTSWITH_CI, + OP_STARTSWITH_CS, + OP_NSTARTSWITH, + OP_NSTARTSWITH_CI, + OP_NSTARTSWITH_CS, + OP_REGEX, + OP_NREGEX, + OP_GREQ + } op = OP_NONE; + + /* ((TAG [OP] VALUE)) */ + /* the double parenthesis are used in just two cases: + * - negation (!(artist == "VAL")) + * - conjunction ((artist == "FOO") AND (album == "BAR")) + * this means we need to proper-parse the values, since we need to + * know the closing parenthesis is real, and not inside the value + * to possible parse another expression (via AND) */ + for (p = &arg[1]; p < argend; p++) + { + DPRINTF(E_DBG, L_MPD, "state: %u, tagtype=%s, op=%u, val=%s\n", + state, tagtype ? tagtype->tag : "?", op, val ? val : "?"); + switch (state) + { + case STATE_INIT: + tagtype = NULL; + op = OP_NONE; + switch (*p) + { + case '!': + negate = true; + break; + case '(': + state = STATE_EXPR; + break; + default: + /* silently eat away garbage we don't grok */ + negate = false; + break; + } + break; + case STATE_EXPR: + /* TAG -- hunt for the space, lookup tag */ + for (q = p; *q != ' ' && q < argend; q++) + ; + if (q == argend) + { + state = STATE_INIT; + } + else + { + *q = '\0'; + tagtype = find_tagtype(p); + if (tagtype == NULL) + { + DPRINTF(E_WARN, L_MPD, + "Tag '%s' is not supported, condition ignored\n", + p); + state = STATE_INIT; + } + else + { + if (strcmp(tagtype->tag, "base") == 0 || + strcmp(tagtype->tag, "modified-since") == 0 || + /* added-since: not supported (yet) */ false) + { + /* these expressions somehow lack an operator, + * the meaning is special per tag */ + op = OP_NONE; + state = STATE_VAL; + } + else + { + state = STATE_OP; + } + } + p = q; + } + break; + case STATE_OP: + /* OP -- hunt for the space */ + for (q = p; *q != ' ' && q < argend; q++) + ; + if (q == argend) + { + state = STATE_INIT; + } + else + { + *q = '\0'; + if (strcmp(p, ">=") == 0) + op = OP_GREQ; + else if (strcmp(p, "==") == 0) + op = OP_EQUALS; + else if (strcmp(p, "!=") == 0) + op = OP_NEQUALS; + else if (strcmp(p, "eq_cs") == 0) + op = OP_EQUALS_CS; + else if (strcmp(p, "!eq_cs") == 0) + op = OP_NEQUALS_CS; + else if (strcmp(p, "eq_ci") == 0) + op = OP_EQUALS_CI; + else if (strcmp(p, "!eq_ci") == 0) + op = OP_NEQUALS_CI; + else if (strcmp(p, "=~") == 0) + op = OP_REGEX; + else if (strcmp(p, "!~") == 0) + op = OP_NREGEX; + else if (strcmp(p, "contains") == 0) + op = OP_CONTAINS; + else if (strcmp(p, "!contains") == 0) + op = OP_NCONTAINS; + else if (strcmp(p, "contains_cs") == 0) + op = OP_CONTAINS_CS; + else if (strcmp(p, "!contains_cs") == 0) + op = OP_NCONTAINS_CS; + else if (strcmp(p, "contains_ci") == 0) + op = OP_CONTAINS_CI; + else if (strcmp(p, "!contains_ci") == 0) + op = OP_NCONTAINS_CI; + else if (strcmp(p, "startswith") == 0) + op = OP_STARTSWITH; + else if (strcmp(p, "!startswith") == 0) + op = OP_NSTARTSWITH; + else if (strcmp(p, "startswith_cs") == 0) + op = OP_STARTSWITH_CS; + else if (strcmp(p, "!startswith_cs") == 0) + op = OP_NSTARTSWITH_CS; + else if (strcmp(p, "startswith_ci") == 0) + op = OP_STARTSWITH_CI; + else if (strcmp(p, "!startswith_ci") == 0) + op = OP_NSTARTSWITH_CI; + else + { + DPRINTF(E_WARN, L_MPD, + "Operator '%s' is not supported, " + "condition ignored\n", + p); + state = STATE_INIT; + break; + } + + /* exactmatch is actually "find" commands, which are + * case-sensitive, the rest ignore case, promote the + * non-explicit ones (v0.24) + * further, historically search used strstr behaviour, + * find strcmp, so promote equals to contains when + * used with search */ + switch (op) + { + case OP_EQUALS: + case OP_NEQUALS: + /* don't promote equals when used on numbers */ + if (tagtype->type == MPD_TYPE_INT) + break; + if (!exact_match) + op += 6; + case OP_CONTAINS: + case OP_NCONTAINS: + case OP_STARTSWITH: + case OP_NSTARTSWITH: + op += exact_match ? 2 : 1; + break; + default: + /* nothing to do */ + break; + } + + /* simplify handling in FINI */ + if (negate) + { + switch (op) + { + case OP_EQUALS: + case OP_EQUALS_CI: + case OP_EQUALS_CS: + case OP_CONTAINS_CI: + case OP_CONTAINS_CS: + case OP_STARTSWITH_CI: + case OP_STARTSWITH_CS: + op += 3; /* become NOT */ + break; + case OP_NEQUALS: + case OP_NEQUALS_CI: + case OP_NEQUALS_CS: + case OP_NCONTAINS: + case OP_NCONTAINS_CI: + case OP_NCONTAINS_CS: + case OP_NSTARTSWITH: + case OP_NSTARTSWITH_CI: + case OP_NSTARTSWITH_CS: + op -= 3; /* remove NOT */ + break; + default: + /* nothing to do */ + break; + } + } + + p = q; + state = STATE_VAL; + } + break; + case STATE_VAL: + switch (*p) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + /* VAL) -- hunt for the closing parenthesis */ + for (q = p; *q != ')' && q < argend; q++) + ; + if (q == argend) + { + state = STATE_INIT; + } + else + { + *q = '\0'; + val = p; + state = STATE_FINI; + } + break; + case '"': + case '\'': + { + char *quote = p; + for (q = ++p; q < argend; q++) + { + if (*q == *quote) + break; + if (*q == '\\') + *p++ = *++q; + else + *p++ = *q; + } + if (q == argend) + { + state = STATE_INIT; + } + else + { + *p = '\0'; + p = q; + val = quote + 1; + state = STATE_FINI; + } + } + break; + default: + DPRINTF(E_WARN, L_MPD, + "illegal value for expression: '%s'\n", + p); + state = STATE_INIT; + break; + } + break; + case STATE_FINI: + { + char *sqlopstr; + + /* push out expression, take negate into account + * recursing here for reuse would be nice, but there + * are a bunch of subtle differences which make this + * not as straightforward as it ought to be */ + + switch (op) + { + case OP_GREQ: + if (negate) + sqlopstr = "(%s < %u)"; + else + sqlopstr = "(%s >= %u)"; + break; + case OP_EQUALS: + sqlopstr = "(%s = %u)"; + break; + case OP_NEQUALS: + sqlopstr = "(%s != %u)"; + break; + case OP_EQUALS_CI: + sqlopstr = "(%s LIKE '%q')"; + break; + case OP_NEQUALS_CI: + sqlopstr = "(%s NOT LIKE '%q')"; + break; + case OP_EQUALS_CS: + sqlopstr = "(%s = '%q')"; + break; + case OP_NEQUALS_CS: + sqlopstr = "(%s != '%q')"; + break; + case OP_CONTAINS_CI: + sqlopstr = "(%s LIKE '%%%q%%')"; + break; + case OP_NCONTAINS_CI: + sqlopstr = "(%s NOT LIKE '%%%q%%')"; + break; + case OP_CONTAINS_CS: + sqlopstr = "(%s GLOB '*%q*')"; + break; + case OP_NCONTAINS_CS: + sqlopstr = "(%s NOT GLOB '*%q*')"; + break; + case OP_STARTSWITH_CI: + sqlopstr = "(%s LIKE '%q%%')"; + break; + case OP_NSTARTSWITH_CI: + sqlopstr = "(%s NOT LIKE '%q%%')"; + break; + case OP_STARTSWITH_CS: + sqlopstr = "(%s GLOB '%q*')"; + break; + case OP_NSTARTSWITH_CS: + sqlopstr = "(%s NOT GLOB '%q*')"; + break; + case OP_REGEX: + sqlopstr = "(%s REGEX '%q')"; + break; + case OP_NREGEX: + sqlopstr = "(NOT %s REGEX '%q')"; + break; + default: + sqlopstr = NULL; /* invalid, cause crash */ + break; + } + + if (tagtype->type == MPD_TYPE_STRING) + { + condition = db_mprintf(sqlopstr, + tagtype->field, + val); + } + else if (tagtype->type == MPD_TYPE_INT) + { + uint32_t num; + int ret = safe_atou32(val, &num); + if (ret < 0) + DPRINTF(E_WARN, L_MPD, + "%s parameter '%s' is not an integer and " + "will be ignored\n", tagtype->tag, val); + else + condition = db_mprintf(sqlopstr, + tagtype->field, + num); + } + else if (tagtype->type == MPD_TYPE_SPECIAL) + { + if (strcmp(tagtype->tag, "any") == 0) + { + char *tmp; + /* this really is a hack, the documentation + * says it should check *all* tag types, not + * just these three */ + condition = db_mprintf("("); + tmp = db_mprintf(sqlopstr, "f.artist", val); + append_string(&condition, tmp, " OR "); + free(tmp); + tmp = db_mprintf(sqlopstr, "f.album", val); + append_string(&condition, tmp, " OR "); + free(tmp); + tmp = db_mprintf(sqlopstr, "f.title", val); + append_string(&condition, tmp, " OR "); + free(tmp); + append_string(&condition, ")", NULL); + } + else if (strcmp(tagtype->tag, "file") == 0 || + strcmp(tagtype->tag, "base") == 0) + { + condition = db_mprintf(sqlopstr, + tagtype->field, + val); + } + else if (strcmp(tagtype->tag, "modified-since") == 0) + { + char *datefmt; + + /* according to the mpd protocol specification + * the value can be a unix timestamp or ISO8601 */ + if (strchr(narg, '-') == NULL) + datefmt = "unixepoch"; + else + datefmt = "utc"; + + condition = + db_mprintf("(f.time_modified > strftime('%%s', " + "datetime('%q', '%s')))", val, datefmt); + } + else + { + DPRINTF(E_WARN, L_MPD, + "Unknown special parameter '%s' " + "will be ignored\n", + tagtype->tag); + } + } + + if (condition != NULL) + { + struct query_params *qp = ¶m->qp; + + append_string(&qp->filter, condition, " AND "); + + free(condition); + condition = NULL; + + param->params_set |= CMD_FILTER; + } + + if (*p == ')') + p++; + while (*p == ' ') + p++; + if (strcasecmp(p, "AND") == 0) + p += 3; + + negate = false; + state = STATE_INIT; + break; + } + } + } + + return 0; + } + else if (narg != NULL) + { + struct mpd_tagtype *tagtype = find_tagtype(arg); + + /* arg: TYPE, narg: VALUE */ + + if (!tagtype) + { + DPRINTF(E_WARN, L_MPD, + "Parameter '%s' is not supported and will be ignored\n", + arg); + return 1; + } - if (!tagtype) + if (tagtype->type == MPD_TYPE_STRING) + { + if (exact_match) + condition = db_mprintf("(%s = '%q')", tagtype->field, narg); + else + condition = db_mprintf("(%s LIKE '%%%q%%')", tagtype->field, narg); + } + else if (tagtype->type == MPD_TYPE_INT) + { + uint32_t num; + int ret = safe_atou32(narg, &num); + if (ret < 0) + DPRINTF(E_WARN, L_MPD, + "%s parameter '%s' is not an integer and " + "will be ignored\n", tagtype->tag, narg); + else + condition = db_mprintf("(%s = %u)", tagtype->field, num); + } + else if (tagtype->type == MPD_TYPE_SPECIAL) + { + if (strcasecmp(tagtype->tag, "any") == 0) { - DPRINTF(E_WARN, L_MPD, "Parameter '%s' is not supported and will be ignored\n", argv[i]); - continue; + condition = db_mprintf("(f.artist LIKE '%%%q%%' OR " + " f.album LIKE '%%%q%%' OR " + " f.title LIKE '%%%q%%')", + narg, narg, narg); } - - if (tagtype->type == MPD_TYPE_STRING) + else if (strcasecmp(tagtype->tag, "file") == 0) { if (exact_match) - c1 = db_mprintf("(%s = '%q')", tagtype->field, argv[i + 1]); + condition = db_mprintf("(f.virtual_path = '/%q')", narg); else - c1 = db_mprintf("(%s LIKE '%%%q%%')", tagtype->field, argv[i + 1]); + condition = db_mprintf("(f.virtual_path LIKE '%%%q%%')", narg); } - else if (tagtype->type == MPD_TYPE_INT) + else if (strcasecmp(tagtype->tag, "base") == 0) { - 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); + condition = db_mprintf("(f.virtual_path LIKE '/%q%%')", narg); } - else if (tagtype->type == MPD_TYPE_SPECIAL) + else if (strcasecmp(tagtype->tag, "modified-since") == 0) { - 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 if (0 == strcasecmp(tagtype->tag, "modified-since")) - { - // according to the mpd protocol specification the value can be a unix timestamp or ISO 8601 - if (strchr(argv[i + 1], '-') == NULL) - c1 = db_mprintf("(f.time_modified > strftime('%%s', datetime('%q', 'unixepoch')))", argv[i + 1]); - else - c1 = db_mprintf("(f.time_modified > strftime('%%s', datetime('%q', 'utc')))", argv[i + 1]); - } - else - { - DPRINTF(E_WARN, L_MPD, "Unknown special parameter '%s' will be ignored\n", tagtype->tag); - } - } - } - 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, "Missing value for parameter '%s', ignoring '%s'\n", argv[i], argv[i]); - } + char *datefmt; - if (c1) - { - append_string(&qp->filter, c1, " AND "); + /* according to the mpd protocol specification the value + * can be a unix timestamp or ISO 8601 */ + if (strchr(narg, '-') == NULL) + datefmt = "unixepoch"; + else + datefmt = "utc"; - free(c1); - c1 = NULL; + condition = + db_mprintf("(f.time_modified > strftime('%%s', " + "datetime('%q', '%s')))", narg, datefmt); + } + else + { + DPRINTF(E_WARN, L_MPD, + "Unknown special parameter '%s' will be ignored\n", + tagtype->tag); + return 1; + } } } + else + { + /* Special case: a single token is allowed if listing albums for + * an artist */ + condition = db_mprintf("(f.album_artist = '%q')", narg); + } - if ((i + 1) < argc && 0 == strcasecmp(argv[i], "window")) + if (condition != NULL) { - 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]); - } + struct query_params *qp = ¶m->qp; + + append_string(&qp->filter, condition, " AND "); + + free(condition); + + param->params_set |= CMD_FILTER; } return 0; } +/** + * Parse command arguments as instructed via param. Populates param + * with the found arguments. The caller is expected to setup + * param->params_allow to indicate what it expects to be parsed. Any + * parameter not matching are ignored. + * NOTE: param is an in/out structure, config is read, parsed results + * are stored in it. + * + * Examples of the commands that are processed are: + * - playlistfind {FILTER} [sort {TYPE}] [window {START:END}] + * - searchadd {FILTER} [sort {TYPE}] [window {START:END}] [position POS] + * - searchcount {FILTER} [group {GROUPTYPE}] + * In each of these, a call is made using argv positioned at FILTER to + * this function, which then tries to handle any FILTER commands as long + * as it doesn't find a tag like sort, window, position or group. + * For instance, the searchadd call will have to be setup that + * param->params_allow contains (CMD_FILTER | CMD_SORT | CMD_WINDOW | + * CMD_POSITION). Since sort, window and position are optional, after + * the call param->params_set can be queried to check what commands were + * found in the input in order to handle the arguments. + */ static int -parse_group_params(int argc, char **argv, bool group_in_listcommand, struct query_params *qp, struct mpd_tagtype ***group, int *groupsize) +mpd_parse_cmd_params(int argc, char **argv, struct mpd_cmd_params *param) { - int first_group; + bool dofilters; + enum mpd_param_cmd cmd; + int ret = 0; int i; - int j; - struct mpd_tagtype *tagtype; - *groupsize = 0; - *group = NULL; + if (param == NULL) + return 1; - // Iterate through arguments to the first "group" argument - for (first_group = 0; first_group < argc; first_group++) - { - if (0 == strcasecmp(argv[first_group], "group")) - break; - } + /* only do filter processing if requested */ + dofilters = param->params_allow & CMD_FILTER; - // 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; - - CHECK_NULL(L_MPD, *group = calloc(*groupsize, sizeof(struct mpd_tagtype *))); - - // Now process all group/field arguments - for (j = 0; j < (*groupsize); j++) + /* loop over arguments, detecting parameters and process them + * accordingly -- arguments prior known parameters are assumed to be + * filter arguments */ + for (i = 0; i < argc; i += 2) { - 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) - { - if (group_in_listcommand) - append_string(&qp->group, tagtype->group_field, ", "); - (*group)[j] = tagtype; - } - } + cmd = dofilters ? CMD_FILTER : CMD_UNSET; + if (strcasecmp(argv[i], "window") == 0) + cmd = CMD_WINDOW; + else if (strcasecmp(argv[i], "group") == 0) + cmd = CMD_GROUP; + else if (strcasecmp(argv[i], "position") == 0) + cmd = CMD_POSITION; + else if (strcasecmp(argv[i], "sort") == 0) + cmd = CMD_SORT; + + /* filters stop after the first command is seen */ + if (cmd != CMD_FILTER) + dofilters = false; + + /* ignore this command if not requested */ + if ((param->params_allow & cmd) == CMD_UNSET) + continue; + + /* currently all commands need a single argument */ + if (cmd != CMD_FILTER && i + 1 >= argc) + { + DPRINTF(E_WARN, L_MPD, + "Missing mandatory argument to Parameter '%s'\n", + argv[i]); + /* be lenient, historically thus functionality ignored + * problems, possibly on purpose for forwards compatibility */ + ret = 1; + break; + } + + switch (cmd) + { + case CMD_WINDOW: + ret |= mpd_parse_cmd_window(argv[i + 1], param); + break; + case CMD_GROUP: + /* need to allocate space if we haven't, group command can be + * repeated, so take worst case and assume all remaining + * commands are repetitions */ + if (param->groups == NULL) + { + param->groupssize = (argc - i) / 2; + CHECK_NULL(L_MPD, + param->groups = calloc(param->groupssize, + sizeof(param->groups[0]))); + } + ret |= mpd_parse_cmd_group(argv[i + 1], param); + break; + case CMD_POSITION: + ret |= mpd_parse_cmd_position(argv[i + 1], param); + break; + case CMD_SORT: + /* currently unhandled, ignore */ + break; + case CMD_FILTER: + { + char *nextarg = NULL; + if (i + 1 < argc) + nextarg = argv[i + 1]; + ret |= mpd_parse_cmd_filter(argv[i], nextarg, param); + break; + } + case CMD_UNSET: + break; + } } - return 0; + return ret; } /* @@ -1340,36 +2108,35 @@ mpd_command_next(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s static int mpd_command_pause(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { - int pause; + int pause = -1; struct player_status status; int ret; - pause = 1; if (argc > 1) { ret = safe_atoi32(argv[1], &pause); - if (ret < 0) + if (ret < 0 || pause > 1 || pause < 0) { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); + *errmsg = safe_asprintf("Argument doesn't convert " + "to integer or has unsupported value: '%s'", + argv[1]); return ACK_ERROR_ARG; } } - else - { - player_get_status(&status); - if (status.status != PLAY_PLAYING) - pause = 0; - } - - if (pause == 1) + /* ignore pause when in stopped state or when explicit request matches + * current state, like MPD */ + player_get_status(&status); + if (status.status == PLAY_PAUSED && pause <= 0) + ret = player_playback_start(); + else if (status.status == PLAY_PLAYING && (pause < 0 || pause == 1)) ret = player_playback_pause(); else - ret = player_playback_start(); + ret = 0; if (ret < 0) { - *errmsg = safe_asprintf("Failed to pause playback"); + *errmsg = safe_asprintf("Failed to pause/resume playback"); return ACK_ERROR_UNKNOWN; } @@ -1723,8 +2490,31 @@ mpd_command_add(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, st { struct player_status status; int ret; + int pos = -1; - ret = mpd_queue_add(argv[1], false, -1); + if (argc < 2) + { + *errmsg = safe_asprintf("Missing arguments to command add"); + return ACK_ERROR_ARG; + } + + /* 0.23.3: POSITION argument */ + if (argc >= 3) + { + struct mpd_cmd_params param; + + memset(¶m, 0, sizeof(param)); + + if (mpd_parse_cmd_position(argv[2], ¶m) != 0) + { + *errmsg = safe_asprintf("Could not parse POSITION '%s'", argv[2]); + return ACK_ERROR_ARG; + } + + pos = param.pos; + } + + ret = mpd_queue_add(argv[1], false, pos); if (ret < 0) { @@ -1737,7 +2527,7 @@ mpd_command_add(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, st player_get_status(&status); // Given path is not in the library, check if it is possible to add as a non-library queue item - ret = library_queue_item_add(argv[1], -1, status.shuffle, status.item_id, NULL, NULL); + ret = library_queue_item_add(argv[1], pos, status.shuffle, status.item_id, NULL, NULL); if (ret != LIBRARY_OK) { *errmsg = safe_asprintf("Failed to add song '%s' to playlist (unkown path)", argv[1]); @@ -1752,7 +2542,10 @@ mpd_command_add(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, st * Command handler function for 'addid' * Adds the song under the given path to the end or to the given position of the playqueue. * Expects argument argv[1] to be a path to a single file. argv[2] is optional, if present - * it must be an integer representing the position in the playqueue. + * it must be an integer representing the position in the playqueue. If + * the parameter starts with + or -, it is relative to the current song, + * with +0 being right after the current song, and -0 before the current + * song. */ static int mpd_command_addid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) @@ -1763,12 +2556,17 @@ mpd_command_addid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, if (argc > 2) { - ret = safe_atoi32(argv[2], &to_pos); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); - return ACK_ERROR_ARG; - } + struct mpd_cmd_params param; + + memset(¶m, 0, sizeof(param)); + + if (mpd_parse_cmd_position(argv[2], ¶m) != 0) + { + *errmsg = safe_asprintf("Could not parse POSITION '%s'", argv[2]); + return ACK_ERROR_ARG; + } + + to_pos = param.pos; } ret = mpd_queue_add(argv[1], true, to_pos); @@ -1781,7 +2579,7 @@ mpd_command_addid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, ret = library_queue_item_add(argv[1], to_pos, status.shuffle, status.item_id, NULL, NULL); if (ret != LIBRARY_OK) { - *errmsg = safe_asprintf("Failed to add song '%s' to playlist (unkown path)", argv[1]); + *errmsg = safe_asprintf("Failed to add song '%s' to playlist (unknown path)", argv[1]); return ACK_ERROR_UNKNOWN; } } @@ -1894,8 +2692,10 @@ mpd_command_move(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s int start_pos; int end_pos; int count; - uint32_t to_pos; int ret; + struct mpd_cmd_params param; + + memset(¶m, 0, sizeof(param)); ret = mpd_pars_range_arg(argv[1], &start_pos, &end_pos); if (ret < 0) @@ -1905,21 +2705,30 @@ mpd_command_move(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s } count = end_pos - start_pos; - if (count > 1) - DPRINTF(E_WARN, L_MPD, "Moving ranges is not supported, only the first item will be moved\n"); - ret = safe_atou32(argv[2], &to_pos); - if (ret < 0) + if (mpd_parse_cmd_position(argv[2], ¶m) != 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); return ACK_ERROR_ARG; } - ret = db_queue_move_bypos(start_pos, to_pos); - if (ret < 0) + if (start_pos <= param.pos && end_pos >= param.pos) { - *errmsg = safe_asprintf("Failed to move song at position %d to %d", start_pos, to_pos); - return ACK_ERROR_UNKNOWN; + *errmsg = safe_asprintf("Range overlaps with destination: %d-%d -> %d", + start_pos, end_pos, param.pos); + return ACK_ERROR_ARG; + } + + while (count-- >= 0) + { + DPRINTF(E_WARN, L_MPD, "moving %d -> %d\n", start_pos, param.pos); + ret = db_queue_move_bypos(start_pos, param.pos); + if (ret < 0) + { + *errmsg = safe_asprintf("Failed to move song at position " + "%d to %d", start_pos, param.pos); + return ACK_ERROR_UNKNOWN; + } } return 0; @@ -1929,8 +2738,10 @@ static int mpd_command_moveid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { uint32_t songid; - uint32_t to_pos; int ret; + struct mpd_cmd_params param; + + memset(¶m, 0, sizeof(param)); ret = safe_atou32(argv[1], &songid); if (ret < 0) @@ -1939,14 +2750,13 @@ mpd_command_moveid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, return ACK_ERROR_ARG; } - ret = safe_atou32(argv[2], &to_pos); - if (ret < 0) + if (mpd_parse_cmd_position(argv[2], ¶m) != 0) { *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); return ACK_ERROR_ARG; } - ret = db_queue_move_byitemid(songid, to_pos, 0); + ret = db_queue_move_byitemid(songid, param.pos, 0); if (ret < 0) { *errmsg = safe_asprintf("Failed to move song with id '%s' to index '%s'", argv[1], argv[2]); @@ -2078,90 +2888,99 @@ mpd_command_playlistinfo(struct evbuffer *evbuf, int argc, char **argv, char **e return 0; } +/* https://mpd.readthedocs.io/en/latest/protocol.html#command-playlistfind */ static int mpd_command_playlistfind(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { - struct query_params query_params; + struct mpd_cmd_params params; + struct query_params *query_params; struct db_queue_item queue_item; int ret; - memset(&query_params, 0, sizeof(struct query_params)); - if (argc < 3 || ((argc - 1) % 2) != 0) { *errmsg = safe_asprintf("Missing argument(s) for command 'playlistfind'"); return ACK_ERROR_ARG; } - parse_filter_window_params(argc - 1, argv + 1, true, &query_params); + memset(¶ms, 0, sizeof(params)); + params.exactmatch = true; + query_params = ¶ms.qp; - ret = db_queue_enum_start(&query_params); + params.params_allow = CMD_FILTER | CMD_SORT | CMD_WINDOW; + mpd_parse_cmd_params(argc - 1, argv + 1, ¶ms); + + ret = db_queue_enum_start(query_params); if (ret < 0) { - free(query_params.filter); + free(query_params->filter); *errmsg = safe_asprintf("Failed to start queue enum for command playlistinfo: '%s'", argv[1]); return ACK_ERROR_ARG; } - while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) + while ((ret = db_queue_enum_fetch(query_params, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(evbuf, &queue_item); if (ret < 0) { *errmsg = safe_asprintf("Error adding media info for file with id: %d", queue_item.file_id); - db_queue_enum_end(&query_params); - free(query_params.filter); + db_queue_enum_end(query_params); + free(query_params->filter); return ACK_ERROR_UNKNOWN; } } - db_queue_enum_end(&query_params); - free(query_params.filter); + db_queue_enum_end(query_params); + free(query_params->filter); return 0; } +/* https://mpd.readthedocs.io/en/latest/protocol.html#command-playlistsearch */ static int mpd_command_playlistsearch(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { - struct query_params query_params; + struct mpd_cmd_params params; + struct query_params *query_params; struct db_queue_item queue_item; int ret; - memset(&query_params, 0, sizeof(struct query_params)); - if (argc < 3 || ((argc - 1) % 2) != 0) { *errmsg = safe_asprintf("Missing argument(s) for command 'playlistfind'"); return ACK_ERROR_ARG; } - parse_filter_window_params(argc - 1, argv + 1, false, &query_params); + memset(¶ms, 0, sizeof(params)); + query_params = ¶ms.qp; - ret = db_queue_enum_start(&query_params); + params.params_allow = CMD_FILTER | CMD_SORT | CMD_WINDOW; + mpd_parse_cmd_params(argc - 1, argv + 1, ¶ms); + + ret = db_queue_enum_start(query_params); if (ret < 0) { - free(query_params.filter); + free(query_params->filter); *errmsg = safe_asprintf("Failed to start queue enum for command playlistinfo: '%s'", argv[1]); return ACK_ERROR_ARG; } - while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) + while ((ret = db_queue_enum_fetch(query_params, &queue_item)) == 0 && queue_item.id > 0) { ret = mpd_add_db_queue_item(evbuf, &queue_item); if (ret < 0) { *errmsg = safe_asprintf("Error adding media info for file with id: %d", queue_item.file_id); - db_queue_enum_end(&query_params); - free(query_params.filter); + db_queue_enum_end(query_params); + free(query_params->filter); return ACK_ERROR_UNKNOWN; } } - db_queue_enum_end(&query_params); - free(query_params.filter); + db_queue_enum_end(query_params); + free(query_params->filter); return 0; } @@ -2296,10 +3115,16 @@ mpd_command_listplaylist(struct evbuffer *evbuf, int argc, char **argv, char **e { char *path; struct playlist_info *pli; - struct query_params qp; struct db_media_file_info dbmfi; + struct mpd_cmd_params param; int ret; + if (argc < 2) + { + *errmsg = safe_asprintf("Missing argument for listplaylist"); + return ACK_ERROR_ARG; + } + if (!default_pl_dir || strstr(argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' @@ -2319,16 +3144,19 @@ mpd_command_listplaylist(struct evbuffer *evbuf, int argc, char **argv, char **e return ACK_ERROR_ARG; } - memset(&qp, 0, sizeof(struct query_params)); + memset(¶m, 0, sizeof(param)); - qp.type = Q_PLITEMS; - qp.idx_type = I_NONE; - qp.id = pli->id; + param.qp.type = Q_PLITEMS; + param.qp.idx_type = I_NONE; + param.qp.id = pli->id; - ret = db_query_start(&qp); + if (argc >= 3) + mpd_parse_cmd_window(argv[2], ¶m); + + ret = db_query_start(¶m.qp); if (ret < 0) { - db_query_end(&qp); + db_query_end(¶m.qp); free_pli(pli, 0); @@ -2336,14 +3164,14 @@ mpd_command_listplaylist(struct evbuffer *evbuf, int argc, char **argv, char **e return ACK_ERROR_UNKNOWN; } - while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) + while ((ret = db_query_fetch_file(&dbmfi, ¶m.qp)) == 0) { evbuffer_add_printf(evbuf, "file: %s\n", (dbmfi.virtual_path + 1)); } - db_query_end(&qp); + db_query_end(¶m.qp); free_pli(pli, 0); @@ -2359,10 +3187,16 @@ mpd_command_listplaylistinfo(struct evbuffer *evbuf, int argc, char **argv, char { char *path; struct playlist_info *pli; - struct query_params qp; + struct mpd_cmd_params param; struct db_media_file_info dbmfi; int ret; + if (argc < 2) + { + *errmsg = safe_asprintf("Missing argument for listplaylistinfo"); + return ACK_ERROR_ARG; + } + if (!default_pl_dir || strstr(argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' @@ -2382,16 +3216,19 @@ mpd_command_listplaylistinfo(struct evbuffer *evbuf, int argc, char **argv, char return ACK_ERROR_NO_EXIST; } - memset(&qp, 0, sizeof(struct query_params)); + memset(¶m, 0, sizeof(param)); - qp.type = Q_PLITEMS; - qp.idx_type = I_NONE; - qp.id = pli->id; + param.qp.type = Q_PLITEMS; + param.qp.idx_type = I_NONE; + param.qp.id = pli->id; - ret = db_query_start(&qp); + if (argc >= 3) + mpd_parse_cmd_window(argv[2], ¶m); + + ret = db_query_start(¶m.qp); if (ret < 0) { - db_query_end(&qp); + db_query_end(¶m.qp); free_pli(pli, 0); @@ -2399,7 +3236,7 @@ mpd_command_listplaylistinfo(struct evbuffer *evbuf, int argc, char **argv, char return ACK_ERROR_UNKNOWN; } - while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) + while ((ret = db_query_fetch_file(&dbmfi, ¶m.qp)) == 0) { ret = mpd_add_db_media_file_info(evbuf, &dbmfi); if (ret < 0) @@ -2408,7 +3245,7 @@ mpd_command_listplaylistinfo(struct evbuffer *evbuf, int argc, char **argv, char } } - db_query_end(&qp); + db_query_end(¶m.qp); free_pli(pli, 0); @@ -2459,7 +3296,8 @@ mpd_command_listplaylists(struct evbuffer *evbuf, int argc, char **argv, char ** evbuffer_add_printf(evbuf, "playlist: %s\n" - "Last-Modified: %s\n", + "Last-Modified: %s\n" + "added: -1\n", /* MPD v0.24 */ (dbpli.virtual_path + 1), modified); } @@ -2482,6 +3320,13 @@ mpd_command_load(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s struct player_status status; struct query_params qp = { .type = Q_PLITEMS }; int ret; + int pos = -1; + + if (argc < 2) + { + *errmsg = safe_asprintf("Missing arguments to command load"); + return ACK_ERROR_ARG; + } if (!default_pl_dir || strstr(argv[1], ":/")) { @@ -2503,13 +3348,29 @@ mpd_command_load(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s } //TODO If a second parameter is given only add the specified range of songs to the playqueue + + /* 0.23.1: POSITION specifies where to insert in the queue */ + if (argc >= 4) + { + struct mpd_cmd_params param; + + memset(¶m, 0, sizeof(param)); + + if (mpd_parse_cmd_position(argv[3], ¶m) != 0) + { + *errmsg = safe_asprintf("Could not parse POSITION '%s'", argv[3]); + return ACK_ERROR_ARG; + } + + pos = param.pos; + } qp.id = pli->id; free_pli(pli, 0); player_get_status(&status); - ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, -1, NULL, NULL); + ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, pos, NULL, NULL); if (ret < 0) { *errmsg = safe_asprintf("Failed to add song '%s' to playlist", argv[1]); @@ -2526,12 +3387,26 @@ mpd_command_playlistadd(struct evbuffer *evbuf, int argc, char **argv, char **er char *vp_item; int ret; + if (argc < 3) + { + *errmsg = safe_asprintf("Missing arguments to command playlistadd"); + return ACK_ERROR_ARG; + } + if (!allow_modifying_stored_playlists) { *errmsg = safe_asprintf("Modifying stored playlists is not enabled"); return ACK_ERROR_PERMISSION; } + /* 0.23.1: POSITION specifies where to insert, not supported by + * library currently */ + if (argc >= 4) + { + *errmsg = safe_asprintf("Positional updates to playlists not supported"); + return ACK_ERROR_SYSTEM; + } + if (!default_pl_dir || strstr(argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' @@ -2595,7 +3470,19 @@ static int mpd_command_save(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { char *virtual_path; + struct playlist_info *pli; int ret; + enum { + SAVEMODE_CREATE, + SAVEMODE_APPEND, + SAVEMODE_REPLACE + } save_mode = SAVEMODE_CREATE; /* default */ + + if (argc < 2) + { + *errmsg = safe_asprintf("Missing arguments to command save"); + return ACK_ERROR_ARG; + } if (!allow_modifying_stored_playlists) { @@ -2603,6 +3490,16 @@ mpd_command_save(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s return ACK_ERROR_PERMISSION; } + if (argc >= 3) + { + if (strcasecmp(argv[2], "create") == 0) + save_mode = SAVEMODE_CREATE; + else if (strcasecmp(argv[2], "append") == 0) + save_mode = SAVEMODE_APPEND; + else if (strcasecmp(argv[2], "replace") == 0) + save_mode = SAVEMODE_REPLACE; + } + if (!default_pl_dir || strstr(argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' @@ -2614,7 +3511,65 @@ mpd_command_save(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s virtual_path = safe_asprintf("%s/%s", default_pl_dir, argv[1]); } - ret = library_queue_save(virtual_path); + /* lookup the playlist to see if it exists */ + pli = db_pl_fetch_byvirtualpath(virtual_path); + + if (pli) + free_pli(pli, 0); + + if (pli && save_mode == SAVEMODE_CREATE) + { + *errmsg = safe_asprintf("Playlist already exists by that name: %s", + virtual_path); + free(virtual_path); + return ACK_ERROR_ARG; + } + else if (!pli && save_mode != SAVEMODE_CREATE) + { + *errmsg = safe_asprintf("No such playlist by that name: %s", + virtual_path); + free(virtual_path); + return ACK_ERROR_ARG; + } + + if (save_mode == SAVEMODE_REPLACE) + { + library_playlist_remove(virtual_path); + } + + if (save_mode == SAVEMODE_APPEND) + { + struct query_params query_params; + struct db_queue_item queue_item; + + /* walk through queue, append one by one */ + memset(&query_params, 0, sizeof(query_params)); + + ret = db_queue_enum_start(&query_params); + if (ret < 0) + { + *errmsg = safe_asprintf("Failed to start queue enum " + "for command save append"); + free(virtual_path); + return ACK_ERROR_ARG; + } + + while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && + queue_item.id > 0) + { + ret = library_playlist_item_add(virtual_path, + queue_item.virtual_path); + if (ret < 0) + break; + } + + db_queue_enum_end(&query_params); + } + else /* SAVEMODE_CREATE/REPLACE */ + { + ret = library_queue_save(virtual_path); + } + free(virtual_path); if (ret < 0) { @@ -2625,27 +3580,110 @@ mpd_command_save(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s return 0; } +/* https://mpd.readthedocs.io/en/latest/protocol.html#command-albumart */ +static int +mpd_command_albumart(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +{ + struct evbuffer *evbuffer; + const char *type = NULL; + size_t size; + int itemid; + int format; + uint32_t off; + + if (argc < 2) + { + *errmsg = safe_asprintf("Missing argument(s) for command 'albumart'"); + return ACK_ERROR_ARG; + } + + itemid = db_file_id_byvirtualpath_match(argv[1]); + if (!itemid) + { + DPRINTF(E_WARN, L_MPD, "No item found for path '%s'\n", argv[1]); + *errmsg = safe_asprintf("Item not found"); + return ACK_ERROR_ARG; + } + + if (safe_atou32(argv[2], &off) != 0) + { + DPRINTF(E_WARN, L_MPD, "Argument not a number: '%s'\n", argv[2]); + *errmsg = safe_asprintf("Illegal offset argument"); + return ACK_ERROR_ARG; + } + + evbuffer = evbuffer_new(); + if (!evbuffer) + { + DPRINTF(E_LOG, L_MPD, + "Could not allocate an evbuffer for artwork request\n"); + *errmsg = safe_asprintf("Item not found"); + return ACK_ERROR_ARG; + } + + format = artwork_get_item(evbuffer, itemid, + ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT, 0); + if (format < 0) + { + *errmsg = safe_asprintf("Item was not found"); + evbuffer_free(evbuffer); + return ACK_ERROR_ARG; + } + + switch (format) + { + case ART_FMT_PNG: + type = "image/png"; + break; + + default: + type = "image/jpeg"; + break; + } + + size = evbuffer_get_length(evbuffer); + if (size == 0) + { + *errmsg = safe_asprintf("Item contains no data"); + evbuffer_free(evbuffer); + return ACK_ERROR_ARG; + } + + evbuffer_add_printf(evbuf, "type: %s\n", type); + + mpd_write_binary_response(ctx, evbuf, evbuffer, (size_t)off); + evbuffer_free(evbuffer); + + return 0; +} + +/* https://mpd.readthedocs.io/en/latest/protocol.html#command-count */ static int mpd_command_count(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { - struct query_params qp; + struct mpd_cmd_params params; + struct query_params *qp; struct filecount_info fci; int ret; - if (argc < 3 || ((argc - 1) % 2) != 0) + if (argc < 2) { - *errmsg = safe_asprintf("Missing argument(s) for command 'find'"); + *errmsg = safe_asprintf("Missing argument(s) for command 'count'"); return ACK_ERROR_ARG; } - memset(&qp, 0, sizeof(struct query_params)); - qp.type = Q_COUNT_ITEMS; - parse_filter_window_params(argc - 1, argv + 1, true, &qp); + memset(¶ms, 0, sizeof(params)); + params.exactmatch = true; + qp = ¶ms.qp; + qp->type = Q_COUNT_ITEMS; - ret = db_filecount_get(&fci, &qp); + params.params_allow = CMD_FILTER | CMD_GROUP; + mpd_parse_cmd_params(argc - 1, argv + 1, ¶ms); + + ret = db_filecount_get(&fci, qp); if (ret < 0) { - free(qp.filter); + free(qp->filter); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; @@ -2657,44 +3695,49 @@ mpd_command_count(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, fci.count, (fci.length / 1000)); - db_query_end(&qp); - free(qp.filter); + db_query_end(qp); + free(qp->filter); return 0; } +/* https://mpd.readthedocs.io/en/latest/protocol.html#command-find */ static int mpd_command_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { - struct query_params qp; + struct mpd_cmd_params params; + struct query_params *qp; struct db_media_file_info dbmfi; int ret; - if (argc < 3 || ((argc - 1) % 2) != 0) + if (argc < 2) { *errmsg = safe_asprintf("Missing argument(s) for command 'find'"); return ACK_ERROR_ARG; } - memset(&qp, 0, sizeof(struct query_params)); + memset(¶ms, 0, sizeof(params)); + params.exactmatch = true; + qp = ¶ms.qp; - qp.type = Q_ITEMS; - qp.sort = S_NAME; - qp.idx_type = I_NONE; + qp->type = Q_ITEMS; + qp->sort = S_NAME; + qp->idx_type = I_NONE; - parse_filter_window_params(argc - 1, argv + 1, true, &qp); + params.params_allow = CMD_FILTER | CMD_SORT | CMD_WINDOW; + mpd_parse_cmd_params(argc - 1, argv + 1, ¶ms); - ret = db_query_start(&qp); + ret = db_query_start(qp); if (ret < 0) { - db_query_end(&qp); - free(qp.filter); + db_query_end(qp); + free(qp->filter); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } - while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) + while ((ret = db_query_fetch_file(&dbmfi, qp)) == 0) { ret = mpd_add_db_media_file_info(evbuf, &dbmfi); if (ret < 0) @@ -2703,18 +3746,21 @@ mpd_command_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s } } - db_query_end(&qp); - free(qp.filter); + db_query_end(qp); + free(qp->filter); return 0; } +/* https://mpd.readthedocs.io/en/latest/protocol.html#command-findadd */ static int mpd_command_findadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { - struct query_params qp; + struct mpd_cmd_params params; + struct query_params *qp; struct player_status status; int ret; + int pos = -1; if (argc < 3 || ((argc - 1) % 2) != 0) { @@ -2722,18 +3768,21 @@ mpd_command_findadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg return ACK_ERROR_ARG; } - memset(&qp, 0, sizeof(struct query_params)); + memset(¶ms, 0, sizeof(params)); + params.exactmatch = true; + qp = ¶ms.qp; - qp.type = Q_ITEMS; - qp.sort = S_ARTIST; - qp.idx_type = I_NONE; + qp->type = Q_ITEMS; + qp->sort = S_ARTIST; + qp->idx_type = I_NONE; - parse_filter_window_params(argc - 1, argv + 1, true, &qp); + params.params_allow = CMD_FILTER | CMD_SORT | CMD_WINDOW | CMD_POSITION; + mpd_parse_cmd_params(argc - 1, argv + 1, ¶ms); player_get_status(&status); - ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, -1, NULL, NULL); - free(qp.filter); + ret = db_queue_add_by_query(qp, status.shuffle, status.item_id, pos, NULL, NULL); + free(qp->filter); if (ret < 0) { *errmsg = safe_asprintf("Failed to add songs to playlist"); @@ -2764,13 +3813,13 @@ sanitize_value(char **strval) } } +/* https://mpd.readthedocs.io/en/latest/protocol.html#command-list */ 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 mpd_tagtype **group; - int groupsize; + struct mpd_cmd_params params; + struct query_params *qp; struct db_media_file_info dbmfi; char **strval; int i; @@ -2793,34 +3842,30 @@ mpd_command_list(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s 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) - { - parse_filter_window_params(argc - 2, argv + 2, true, &qp); - } + memset(¶ms, 0, sizeof(params)); + qp = ¶ms.qp; + qp->type = Q_ITEMS; + qp->idx_type = I_NONE; + qp->order = tagtype->sort_field; + qp->group = strdup(tagtype->group_field); + params.addgroupfilter = tagtype->group_in_listcommand; - group = NULL; - groupsize = 0; - parse_group_params(argc - 2, argv + 2, tagtype->group_in_listcommand, &qp, &group, &groupsize); + params.params_allow = CMD_FILTER | CMD_GROUP; + mpd_parse_cmd_params(argc - 2, argv + 2, ¶ms); - ret = db_query_start(&qp); + ret = db_query_start(qp); if (ret < 0) { - db_query_end(&qp); - free(qp.filter); - free(qp.group); - free(group); + db_query_end(qp); + free(qp->filter); + free(qp->group); + free(params.groups); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } - while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) + while ((ret = db_query_fetch_file(&dbmfi, qp)) == 0) { strval = (char **) ((char *)&dbmfi + tagtype->mfi_offset); @@ -2833,30 +3878,30 @@ mpd_command_list(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s tagtype->tag, *strval); - if (group && groupsize > 0) + if (params.groups && params.groupslen > 0) { - for (i = 0; i < groupsize; i++) + for (i = 0; i < params.groupslen; i++) { - if (!group[i]) + if (!params.groups[i]) continue; - strval = (char **) ((char *)&dbmfi + group[i]->mfi_offset); + strval = (char **)((char *)&dbmfi + params.groups[i]->mfi_offset); if (!(*strval) || (**strval == '\0')) continue; evbuffer_add_printf(evbuf, "%s: %s\n", - group[i]->tag, + params.groups[i]->tag, *strval); } } } - db_query_end(&qp); - free(qp.filter); - free(qp.group); - free(group); + db_query_end(qp); + free(qp->filter); + free(qp->group); + free(params.groups); return 0; } @@ -3139,7 +4184,7 @@ mpd_command_listfiles(struct evbuffer *evbuf, int argc, char **argv, char **errm return mpd_command_lsinfo(evbuf, argc, argv, errmsg, ctx); } -/* +/* https://mpd.readthedocs.io/en/latest/protocol.html#command-search * Command handler function for 'search' * Lists any song that matches the given list of arguments. Arguments are pairs of TYPE and WHAT, where * TYPE is the tag that contains WHAT (case insensitiv). @@ -3156,35 +4201,38 @@ mpd_command_listfiles(struct evbuffer *evbuf, int argc, char **argv, char **errm static int mpd_command_search(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { - struct query_params qp; + struct mpd_cmd_params params; + struct query_params *qp; struct db_media_file_info dbmfi; int ret; - if (argc < 3 || ((argc - 1) % 2) != 0) + if (argc < 2) { *errmsg = safe_asprintf("Missing argument(s) for command 'search'"); return ACK_ERROR_ARG; } - memset(&qp, 0, sizeof(struct query_params)); + memset(¶ms, 0, sizeof(params)); + qp = ¶ms.qp; - qp.type = Q_ITEMS; - qp.sort = S_NAME; - qp.idx_type = I_NONE; + qp->type = Q_ITEMS; + qp->sort = S_NAME; + qp->idx_type = I_NONE; - parse_filter_window_params(argc - 1, argv + 1, false, &qp); + params.params_allow = CMD_FILTER | CMD_SORT | CMD_WINDOW; + mpd_parse_cmd_params(argc - 1, argv + 1, ¶ms); - ret = db_query_start(&qp); + ret = db_query_start(qp); if (ret < 0) { - db_query_end(&qp); - free(qp.filter); + db_query_end(qp); + free(qp->filter); *errmsg = safe_asprintf("Could not start query"); return ACK_ERROR_UNKNOWN; } - while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) + while ((ret = db_query_fetch_file(&dbmfi, qp)) == 0) { ret = mpd_add_db_media_file_info(evbuf, &dbmfi); if (ret < 0) @@ -3193,37 +4241,42 @@ mpd_command_search(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, } } - db_query_end(&qp); - free(qp.filter); + db_query_end(qp); + free(qp->filter); return 0; } +/* https://mpd.readthedocs.io/en/latest/protocol.html#command-searchadd */ static int mpd_command_searchadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) { - struct query_params qp; + struct mpd_cmd_params params; + struct query_params *qp; struct player_status status; int ret; + int pos = -1; - if (argc < 3 || ((argc - 1) % 2) != 0) + if (argc < 2) { *errmsg = safe_asprintf("Missing argument(s) for command 'search'"); return ACK_ERROR_ARG; } - memset(&qp, 0, sizeof(struct query_params)); + memset(¶ms, 0, sizeof(params)); + qp = ¶ms.qp; - qp.type = Q_ITEMS; - qp.sort = S_ARTIST; - qp.idx_type = I_NONE; + qp->type = Q_ITEMS; + qp->sort = S_ARTIST; + qp->idx_type = I_NONE; - parse_filter_window_params(argc - 1, argv + 1, false, &qp); + params.params_allow = CMD_FILTER | CMD_SORT | CMD_WINDOW | CMD_POSITION; + mpd_parse_cmd_params(argc - 1, argv + 1, ¶ms); player_get_status(&status); - ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, -1, NULL, NULL); - free(qp.filter); + ret = db_queue_add_by_query(qp, status.shuffle, status.item_id, pos, NULL, NULL); + free(qp->filter); if (ret < 0) { *errmsg = safe_asprintf("Failed to add songs to playlist"); @@ -4190,7 +5243,7 @@ static struct mpd_command mpd_handlers[] = { "stop", mpd_command_stop, -1 }, // The current playlist - { "add", mpd_command_add, 2 }, + { "add", mpd_command_add, -1 }, { "addid", mpd_command_addid, 2 }, { "clear", mpd_command_clear, -1 }, { "delete", mpd_command_delete, -1 }, @@ -4214,19 +5267,20 @@ static struct mpd_command mpd_handlers[] = // { "cleartagid", mpd_command_cleartagid, -1 }, // Stored playlists - { "listplaylist", mpd_command_listplaylist, 2 }, - { "listplaylistinfo", mpd_command_listplaylistinfo, 2 }, + { "listplaylist", mpd_command_listplaylist, -1 }, + { "listplaylistinfo", mpd_command_listplaylistinfo, -1 }, { "listplaylists", mpd_command_listplaylists, -1 }, - { "load", mpd_command_load, 2 }, - { "playlistadd", mpd_command_playlistadd, 3 }, + { "load", mpd_command_load, -1 }, + { "playlistadd", mpd_command_playlistadd, -1 }, // { "playlistclear", mpd_command_playlistclear, -1 }, // { "playlistdelete", mpd_command_playlistdelete, -1 }, // { "playlistmove", mpd_command_playlistmove, -1 }, // { "rename", mpd_command_rename, -1 }, { "rm", mpd_command_rm, 2 }, - { "save", mpd_command_save, 2 }, + { "save", mpd_command_save, -1 }, // The music database + { "albumart", mpd_command_albumart, 2 }, { "count", mpd_command_count, -1 }, { "find", mpd_command_find, -1 }, { "findadd", mpd_command_findadd, -1 }, @@ -4236,6 +5290,7 @@ static struct mpd_command mpd_handlers[] = { "listfiles", mpd_command_listfiles, -1 }, { "lsinfo", mpd_command_lsinfo, -1 }, // { "readcomments", mpd_command_readcomments, -1 }, + { "readpicture", mpd_command_albumart, 2 }, { "search", mpd_command_search, -1 }, { "searchadd", mpd_command_searchadd, -1 }, // { "searchaddpl", mpd_command_searchaddpl, -1 }, @@ -4615,7 +5670,7 @@ mpd_accept_conn_cb(struct evconnlistener *listener, * According to the mpd protocol send "OK MPD \n" to the client, where version is the version * of the supported mpd protocol and not the server version. */ - evbuffer_add(bufferevent_get_output(bev), "OK MPD 0.22.4\n", 14); + evbuffer_add(bufferevent_get_output(bev), "OK MPD 0.24.0\n", 14); client_ctx->evbuffer = bufferevent_get_output(bev); DPRINTF(E_INFO, L_MPD, "New mpd client connection accepted\n");