diff --git a/src/Makefile.am b/src/Makefile.am index f0d4ccbf..0a277642 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -53,8 +53,8 @@ GPERF_FILES = \ GPERF_SRC = $(GPERF_FILES:.gperf=_hash.h) -LEXER_SRC = parsers/daap_lexer.l parsers/smartpl_lexer.l parsers/rsp_lexer.l -PARSER_SRC = parsers/daap_parser.y parsers/smartpl_parser.y parsers/rsp_parser.y +LEXER_SRC = parsers/daap_lexer.l parsers/smartpl_lexer.l parsers/rsp_lexer.l parsers/mpd_lexer.l +PARSER_SRC = parsers/daap_parser.y parsers/smartpl_parser.y parsers/rsp_parser.y parsers/mpd_parser.y # This flag is given to Bison and tells it to produce headers. Note that # automake recognizes this flag too, and has special logic around it, so don't diff --git a/src/mpd.c b/src/mpd.c index 1beb5a47..c5584d6b 100644 --- a/src/mpd.c +++ b/src/mpd.c @@ -16,6 +16,13 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +/* + * To test with raw command input use: + * echo "command" | nc -q0 localhost 6600 + * Or interactive with: + * socat - TCP:localhost:6600 + */ + #ifdef HAVE_CONFIG_H # include #endif @@ -33,6 +40,7 @@ #include #include #include +#include // isdigit #include #include @@ -50,6 +58,17 @@ #include "misc.h" #include "player.h" #include "remote_pairing.h" +#include "parsers/mpd_parser.h" + +// TODO +// optimize queries (map albumartist/album groupings to songalbumid/artistid in db.c) +// support for group in count, e.g. count group artist +// empty group value is a negation + +// Command handlers should use this for returning errors to make sure both +// ack_error and errmsg are correctly set +#define RETURN_ERROR(r, ...) \ + do { out->ack_error = (r); free(out->errmsg); out->errmsg = safe_asprintf(__VA_ARGS__); return -1; } while(0) // 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 @@ -100,12 +119,6 @@ enum mpd_ack_error ACK_ERROR_EXIST = 56, }; -enum mpd_type { - MPD_TYPE_INT, - MPD_TYPE_STRING, - MPD_TYPE_SPECIAL, -}; - enum command_list_type { COMMAND_LIST_NONE = 0, @@ -115,26 +128,6 @@ enum command_list_type COMMAND_LIST_OK_END, }; -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; -}; - struct mpd_client_ctx { // True if the connection is already authenticated or does not need authentication @@ -172,22 +165,47 @@ struct mpd_client_ctx struct mpd_client_ctx *next; }; +#define MPD_WANTS_NUM_ARGV_MIN 1 +#define MPD_WANTS_NUM_ARGV_MAX 3 +enum command_wants_num +{ + MPD_WANTS_NUM_NONE = 0, + MPD_WANTS_NUM_ARG1_IVAL = (1 << (0 + MPD_WANTS_NUM_ARGV_MIN)), + MPD_WANTS_NUM_ARG2_IVAL = (1 << (1 + MPD_WANTS_NUM_ARGV_MIN)), + MPD_WANTS_NUM_ARG3_IVAL = (1 << (2 + MPD_WANTS_NUM_ARGV_MIN)), + MPD_WANTS_NUM_ARG1_UVAL = (1 << (0 + MPD_WANTS_NUM_ARGV_MIN + MPD_WANTS_NUM_ARGV_MAX)), + MPD_WANTS_NUM_ARG2_UVAL = (1 << (1 + MPD_WANTS_NUM_ARGV_MIN + MPD_WANTS_NUM_ARGV_MAX)), + MPD_WANTS_NUM_ARG3_UVAL = (1 << (2 + MPD_WANTS_NUM_ARGV_MIN + MPD_WANTS_NUM_ARGV_MAX)), +}; + +struct mpd_command_input +{ + // Raw argument line + const char *args_raw; + + // Argument line unescaped and split + char *args_split; + char *argv[MPD_COMMAND_ARGV_MAX]; + int argc; + + int has_num; + int32_t argv_i32val[MPD_COMMAND_ARGV_MAX]; + uint32_t argv_u32val[MPD_COMMAND_ARGV_MAX]; +}; + +struct mpd_command_output +{ + struct evbuffer *evbuf; + char *errmsg; + enum mpd_ack_error ack_error; +}; + struct mpd_command { - /* The command name */ - const char *mpdcommand; - - /* - * The function to execute the command - * - * @param evbuf the response event buffer - * @param argc number of arguments in argv - * @param argv argument array, first entry is the commandname - * @param errmsg error message set by this function if an error occured - * @return MPD ack error (ACK_ERROR_NONE on succes) - */ - enum mpd_ack_error (*handler)(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx); + const char *name; + int (*handler)(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx); int min_argc; + int wants_num; }; struct output @@ -274,47 +292,31 @@ static const char * const ffmpeg_mime_types[] = { "application/flv", "applicatio NULL }; -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, }, - { "modified-since", NULL, NULL, NULL, MPD_TYPE_SPECIAL, -1, true, }, - - }; - /* -------------------------------- Helpers --------------------------------- */ -static struct mpd_tagtype * -find_tagtype(const char *tag) +/* + * Some MPD clients crash if the tag value includes the newline character. + * While they should normally not be included in most ID3 tags, they sometimes + * are, so we just change them to space. See #1613 for more details. + */ +static char * +sanitize(char *strval) { - int i; + char *ptr = strval; - if (!tag) - return 0; + if (!strval) + return ""; - for (i = 0; i < ARRAY_SIZE(tagtypes); i++) + while (*ptr != '\0') { - if (strcasecmp(tag, tagtypes[i].tag) == 0) - return &tagtypes[i]; + if (*ptr == '\n') + *ptr = ' '; + + ptr++; } - return NULL; + return strval; } static void @@ -422,16 +424,16 @@ mpd_time(char *buffer, size_t bufferlen, time_t t) } /* - * Parses a range argument of the form START:END (the END item is not included in the range) + * Splits a range argument of the form START:END (the END item is not included in the range) * into its start and end position. * - * @param range the range argument * @param start_pos set by this method to the start position * @param end_pos set by this method to the end postion + * @param range the range argument * @return 0 on success, -1 on failure */ static int -mpd_pars_range_arg(const char *range, int *start_pos, int *end_pos) +range_pos_from_arg(int *start_pos, int *end_pos, const char *range) { int ret; @@ -440,7 +442,7 @@ mpd_pars_range_arg(const char *range, int *start_pos, int *end_pos) ret = sscanf(range, "%d:%d", start_pos, end_pos); if (ret < 0) { - DPRINTF(E_LOG, L_MPD, "Error parsing range argument '%s' (return code = %d)\n", range, ret); + DPRINTF(E_LOG, L_MPD, "Error splitting range argument '%s' (return code = %d)\n", range, ret); return -1; } } @@ -449,7 +451,7 @@ mpd_pars_range_arg(const char *range, int *start_pos, int *end_pos) ret = safe_atoi32(range, start_pos); if (ret < 0) { - DPRINTF(E_LOG, L_MPD, "Error parsing integer argument '%s' (return code = %d)\n", range, ret); + DPRINTF(E_LOG, L_MPD, "Error spitting integer argument '%s' (return code = %d)\n", range, ret); return -1; } @@ -459,11 +461,51 @@ mpd_pars_range_arg(const char *range, int *start_pos, int *end_pos) return 0; } +/* + * Converts a TO arg, which is either an absolute position "X", or a position + * relative to currently playing song "+X"/"-X", into absolute queue pos. + * + * @param to_pos set by this method to absolute queue pos + * @param to_arg string with an integer (abs) or prefixed with +/- (rel) + * @return 0 on success, -1 on failure + */ +static int +to_pos_from_arg(int *to_pos, const char *to_arg) +{ + struct player_status status; + struct db_queue_item *queue_item; + int ret; + + *to_pos = -1; + + if (!to_arg) + return 0; // If no to_arg, assume end of queue (to_pos = -1) + + ret = safe_atoi32(to_arg, to_pos); // +12 will become 12, -12 becomes -12 + if (ret < 0) + return -1; + + if (to_arg[0] != '+' && to_arg[0] != '-') + return 0; + + ret = player_get_status(&status); + if (ret < 0) + return -1; + + queue_item = (status.status == PLAY_STOPPED) ? db_queue_fetch_bypos(0, status.shuffle) : db_queue_fetch_byitemid(status.item_id); + if (!queue_item) + return -1; + + *to_pos += queue_item->pos; + free_queue_item(queue_item, 0); + return 0; +} + /* * Returns the next unquoted string argument from the input string */ static char* -mpd_pars_unquoted(char **input) +mpd_next_unquoted(char **input) { char *arg; @@ -489,7 +531,7 @@ mpd_pars_unquoted(char **input) * with the quotes removed */ static char* -mpd_pars_quoted(char **input) +mpd_next_quoted(char **input) { char *arg; char *src; @@ -526,6 +568,52 @@ mpd_pars_quoted(char **input) return arg; } +// Splits the argument string into an array of strings. Arguments are separated +// by a whitespace character and may be wrapped in double quotes. +static int +mpd_split_args(char **argv, int argv_size, int *argc, char **split, const char *line) +{ + char *ptr; + int arg_count = 0; + + *split = safe_strdup(line); + ptr = *split; + + while (*ptr != 0 && arg_count < argv_size) + { + // Ignore whitespace characters + if (*ptr == ' ') + { + ptr++; + continue; + } + + // Check if the parameter is wrapped in double quotes + if (*ptr == '"') + argv[arg_count] = mpd_next_quoted(&ptr); + else + argv[arg_count] = mpd_next_unquoted(&ptr); + + if (!argv[arg_count]) + goto error; + + arg_count++; + } + + *argc = arg_count; + + // No args or too many args + if (arg_count == 0 || (*ptr != 0 && arg_count == argv_size)) + goto error; + + return 0; + + error: + free(*split); + *split = NULL; + return -1; +} + /* * Adds the information (path, id, tags, etc.) for the given song to the given buffer * with additional information for the position of this song in the player queue. @@ -582,15 +670,15 @@ mpd_add_db_queue_item(struct evbuffer *evbuf, struct db_queue_item *queue_item) (queue_item->virtual_path + 1), modified, (queue_item->song_length / 1000), - queue_item->artist, - queue_item->album_artist, - queue_item->artist_sort, - queue_item->album_artist_sort, - queue_item->album, - queue_item->title, + sanitize(queue_item->artist), + sanitize(queue_item->album_artist), + sanitize(queue_item->artist_sort), + sanitize(queue_item->album_artist_sort), + sanitize(queue_item->album), + sanitize(queue_item->title), queue_item->track, queue_item->year, - queue_item->genre, + sanitize(queue_item->genre), queue_item->disc, queue_item->pos, queue_item->id); @@ -665,204 +753,132 @@ mpd_add_db_media_file_info(struct evbuffer *evbuf, struct db_media_file_info *db modified, (songlength / 1000), ((float) songlength / 1000), - dbmfi->artist, - dbmfi->album_artist, - dbmfi->artist_sort, - dbmfi->album_artist_sort, - dbmfi->album, - dbmfi->title, + sanitize(dbmfi->artist), + sanitize(dbmfi->album_artist), + sanitize(dbmfi->artist_sort), + sanitize(dbmfi->album_artist_sort), + sanitize(dbmfi->album), + sanitize(dbmfi->title), dbmfi->track, dbmfi->year, - dbmfi->genre, + sanitize(dbmfi->genre), dbmfi->disc); return ret; } -static void -append_string(char **a, const char *b, const char *separator) +static bool +is_filter_end(const char *arg) { - char *temp; + return (strcasecmp(arg, "sort") == 0 || strcasecmp(arg, "group") == 0 || strcasecmp(arg, "window") == 0); +} - if (*a) - temp = db_mprintf("%s%s%s", *a, (separator ? separator : ""), b); - else - temp = db_mprintf("%s", b); +// The bison/flex parser only works with the filter format used after version +// 0.20 e.g. "(TAG == 'value') instead of TAG VALUE. +static int +args_reassemble(char *args, size_t args_size, int argc, char **argv) +{ + const char *op; + int filter_start; + int filter_end; + int i; - free(*a); - *a = temp; + // "list" has the filter as second arg, the rest have it as first + filter_start = (strcmp(argv[0], "list") == 0) ? 2 : 1; + op = strstr(argv[0], "search") ? " contains " : " == "; + + for (i = filter_start; i < argc && !is_filter_end(argv[i]); i++) + ; + + filter_end = i; + + snprintf(args, args_size, "%s", argv[0]); + + for (i = 1; i < filter_start; i++) + safe_snprintf_cat(args, args_size, " %s", argv[i]); + + for (i = filter_start; i < filter_end; i++) + { + if (*argv[i] == '(') + { + safe_snprintf_cat(args, args_size, " %s", argv[i]); + } + else if (i + 1 < filter_end) // Legacy filter format (0.20 and before), we will convert + { + safe_snprintf_cat(args, args_size, " %s%s%s\"%s\"%s", i == filter_start ? "((" : "AND (", argv[i], op, argv[i + 1], i + 2 == filter_end ? "))" : ")"); + i++; + } + else if (filter_end == filter_start + 1) // Special case: a legacy single token is allowed if listing albums for an artist + { + safe_snprintf_cat(args, args_size, " (AlbumArtist%s\"%s\")", op, argv[i]); + } + } + + for (i = filter_end; i < argc; i++) + safe_snprintf_cat(args, args_size, " %s", argv[i]); + + // Return an error if the buffer was filled and thus probably truncated + return (strlen(args) + 1 < args_size) ? 0 : -1; } /* - * Sets the filter (where clause) and the window (limit clause) in the given query_params - * based on the given arguments + * Invokes a lexer/parser to read a supported command * - * @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 + * @param qp Must be initialized by caller (zeroed or set to default values) + * @param pos Allocated string with TO position parameter, e.g. "+5" (or NULL) + * @param type Allocated string with TYPE parameter, e.g. "albums" (or NULL) + * @param in The command input */ static int -parse_filter_window_params(int argc, char **argv, bool exact_match, struct query_params *qp) +parse_command(struct query_params *qp, char **pos, char **tagtype, struct mpd_command_input *in) { - struct mpd_tagtype *tagtype; - char *c1; - int start_pos; - int end_pos; - int i; - uint32_t num; + struct mpd_result result; + char args_reassembled[8192]; int ret; - c1 = NULL; + if (pos) + *pos = NULL; + if (tagtype) + *tagtype = NULL; - for (i = 0; i < argc; i += 2) + if (in->argc < 2) + return 0; // Nothing to parse + + ret = args_reassemble(args_reassembled, sizeof(args_reassembled), in->argc, in->argv); + 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) - { - DPRINTF(E_WARN, L_MPD, "Parameter '%s' is not supported and will be ignored\n", argv[i]); - continue; - } - - if (tagtype->type == MPD_TYPE_STRING) - { - 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 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]); - } - - if (c1) - { - append_string(&qp->filter, c1, " AND "); - - free(c1); - c1 = NULL; - } + DPRINTF(E_LOG, L_MPD, "Could not prepare '%s' for parsing\n", in->args_raw); + return -1; } - if ((i + 1) < argc && 0 == strcasecmp(argv[i], "window")) + DPRINTF(E_DBG, L_MPD, "Parse mpd query input '%s'\n", args_reassembled); + + ret = mpd_lex_parse(&result, args_reassembled); + if (ret != 0) { - 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]); - } + DPRINTF(E_LOG, L_MPD, "Could not parse '%s': %s\n", args_reassembled, result.errmsg); + return -1; } + qp->filter = safe_strdup(result.where); + qp->order = safe_strdup(result.order); + qp->group = safe_strdup(result.group); + + qp->limit = result.limit; + qp->offset = result.offset; + qp->idx_type = (qp->limit || qp->offset) ? I_SUB : I_NONE; + + if (pos) + *pos = safe_strdup(result.position); + + if (tagtype) + *tagtype = safe_strdup(result.tagtype); + return 0; } static int -parse_group_params(int argc, char **argv, bool group_in_listcommand, struct query_params *qp, struct mpd_tagtype ***group, int *groupsize) -{ - int first_group; - int i; - int j; - struct mpd_tagtype *tagtype; - - *groupsize = 0; - *group = NULL; - - // 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; - } - - // 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++) - { - 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; - } - } - } - - return 0; -} - -static int -notify_idle_client(struct mpd_client_ctx *client_ctx, short events) +notify_idle_client(struct mpd_client_ctx *client_ctx, short events, bool add_ok) { if (!client_ctx->is_idle) { @@ -895,6 +911,9 @@ notify_idle_client(struct mpd_client_ctx *client_ctx, short events) if (events & LISTENER_RATING) evbuffer_add(client_ctx->evbuffer, "changed: sticker\n", 17); + if (add_ok) + evbuffer_add(client_ctx->evbuffer, "OK\n", 3); + client_ctx->is_idle = false; client_ctx->idle_events = 0; client_ctx->events = 0; @@ -905,10 +924,9 @@ notify_idle_client(struct mpd_client_ctx *client_ctx, short events) /* ----------------------------- Command handlers --------------------------- */ -static enum mpd_ack_error -mpd_command_currentsong(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_currentsong(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct player_status status; struct db_queue_item *queue_item; int ret; @@ -921,59 +939,55 @@ mpd_command_currentsong(struct evbuffer *evbuf, int argc, char **argv, char **er queue_item = db_queue_fetch_byitemid(status.item_id); if (!queue_item) - { - return ACK_ERROR_NONE; - } - - ret = mpd_add_db_queue_item(evbuf, queue_item); + return 0; + ret = mpd_add_db_queue_item(out->evbuf, queue_item); free_queue_item(queue_item, 0); - if (ret < 0) - { - *errmsg = safe_asprintf("Error setting media info for file with id: %d", status.id); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error setting media info for file with id: %d", status.id); - return ACK_ERROR_NONE; + return 0; } /* * Example input: * idle "database" "mixer" "options" "output" "player" "playlist" "sticker" "update" */ -static enum mpd_ack_error -mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_idle(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { + char *key; int i; ctx->idle_events = 0; ctx->is_idle = true; - if (argc > 1) + if (in->argc > 1) { - for (i = 1; i < argc; i++) + for (i = 1; i < in->argc; i++) { - if (0 == strcmp(argv[i], "database")) + key = in->argv[i]; + + if (0 == strcmp(key, "database")) ctx->idle_events |= LISTENER_DATABASE; - else if (0 == strcmp(argv[i], "update")) + else if (0 == strcmp(key, "update")) ctx->idle_events |= LISTENER_UPDATE; - else if (0 == strcmp(argv[i], "player")) + else if (0 == strcmp(key, "player")) ctx->idle_events |= LISTENER_PLAYER; - else if (0 == strcmp(argv[i], "playlist")) + else if (0 == strcmp(key, "playlist")) ctx->idle_events |= LISTENER_QUEUE; - else if (0 == strcmp(argv[i], "mixer")) + else if (0 == strcmp(key, "mixer")) ctx->idle_events |= LISTENER_VOLUME; - else if (0 == strcmp(argv[i], "output")) + else if (0 == strcmp(key, "output")) ctx->idle_events |= LISTENER_SPEAKER; - else if (0 == strcmp(argv[i], "options")) + else if (0 == strcmp(key, "options")) ctx->idle_events |= LISTENER_OPTIONS; - else if (0 == strcmp(argv[i], "stored_playlist")) + else if (0 == strcmp(key, "stored_playlist")) ctx->idle_events |= LISTENER_STORED_PLAYLIST; - else if (0 == strcmp(argv[i], "sticker")) + else if (0 == strcmp(key, "sticker")) ctx->idle_events |= LISTENER_RATING; else - DPRINTF(E_DBG, L_MPD, "Idle command for '%s' not supported\n", argv[i]); + DPRINTF(E_DBG, L_MPD, "Idle command for '%s' not supported\n", key); } } else @@ -982,13 +996,13 @@ mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s // If events the client listens to occurred since the last idle call (or since the client connected, // if it is the first idle call), notify immediately. if (ctx->events & ctx->idle_events) - notify_idle_client(ctx, ctx->events); + notify_idle_client(ctx, ctx->events, false); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_noidle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_noidle(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { /* * The protocol specifies: "The idle command can be canceled by @@ -997,10 +1011,10 @@ mpd_command_noidle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * empty at this time." */ if (ctx->events) - notify_idle_client(ctx, ctx->events); + notify_idle_client(ctx, ctx->events, false); ctx->is_idle = false; - return ACK_ERROR_NONE; + return 0; } /* @@ -1025,8 +1039,8 @@ mpd_command_noidle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * nextsong: 1 * nextsongid: 2 */ -static enum mpd_ack_error -mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_status(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct player_status status; uint32_t queue_length = 0; @@ -1055,7 +1069,7 @@ mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, db_admin_getint(&queue_version, DB_ADMIN_QUEUE_VERSION); db_queue_get_count(&queue_length); - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "volume: %d\n" "repeat: %d\n" "random: %d\n" @@ -1081,7 +1095,7 @@ mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, if (queue_item) { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "song: %d\n" "songid: %d\n", queue_item->pos, @@ -1093,7 +1107,7 @@ mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, if (status.status != PLAY_STOPPED) { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "time: %d:%d\n" "elapsed: %#.3f\n" "bitrate: 128\n" @@ -1104,7 +1118,7 @@ mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, if (library_is_scanning()) { - evbuffer_add(evbuf, "updating_db: 1\n", 15); + evbuffer_add(out->evbuf, "updating_db: 1\n", 15); } if (itemid > 0) @@ -1112,7 +1126,7 @@ mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, queue_item = db_queue_fetch_next(itemid, status.shuffle); if (queue_item) { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "nextsong: %d\n" "nextsongid: %d\n", queue_item->pos, @@ -1122,38 +1136,32 @@ mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, } } - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'stats' */ -static enum mpd_ack_error -mpd_command_stats(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_stats(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params qp; + struct query_params qp = { .type = Q_COUNT_ITEMS }; struct filecount_info fci; double uptime; int64_t db_start = 0; int64_t db_update = 0; int ret; - memset(&qp, 0, sizeof(struct query_params)); - qp.type = Q_COUNT_ITEMS; - ret = db_filecount_get(&fci, &qp); if (ret < 0) - { - *errmsg = safe_asprintf("Could not start query"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); db_admin_getint64(&db_start, DB_ADMIN_START_TIME); uptime = difftime(time(NULL), (time_t) db_start); db_admin_getint64(&db_update, DB_ADMIN_DB_UPDATE); //TODO [mpd] Implement missing stats attributes (playtime) - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "artists: %d\n" "albums: %d\n" "songs: %d\n" @@ -1169,7 +1177,7 @@ mpd_command_stats(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, db_update, 7); - return ACK_ERROR_NONE; + return 0; } /* @@ -1178,21 +1186,11 @@ mpd_command_stats(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * 0 = disable consume * 1 = enable consume */ -static enum mpd_ack_error -mpd_command_consume(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_consume(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - int enable; - int ret; - - ret = safe_atoi32(argv[1], &enable); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - - player_consume_set(enable); - return ACK_ERROR_NONE; + player_consume_set(in->argv_u32val[1]); + return 0; } /* @@ -1201,21 +1199,11 @@ mpd_command_consume(struct evbuffer *evbuf, int argc, char **argv, char **errmsg * 0 = disable shuffle * 1 = enable shuffle */ -static enum mpd_ack_error -mpd_command_random(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_random(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - int enable; - int ret; - - ret = safe_atoi32(argv[1], &enable); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - - player_shuffle_set(enable); - return ACK_ERROR_NONE; + player_shuffle_set(in->argv_u32val[1]); + return 0; } /* @@ -1224,47 +1212,26 @@ mpd_command_random(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * 0 = repeat off * 1 = repeat all */ -static enum mpd_ack_error -mpd_command_repeat(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_repeat(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - int enable; - int ret; - - ret = safe_atoi32(argv[1], &enable); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - - if (enable == 0) + if (in->argv_u32val[1] == 0) player_repeat_set(REPEAT_OFF); else player_repeat_set(REPEAT_ALL); - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'setvol' * Sets the volume, expects argument argv[1] to be an integer 0-100 */ -static enum mpd_ack_error -mpd_command_setvol(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_setvol(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - int volume; - int ret; - - ret = safe_atoi32(argv[1], &volume); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - - player_volume_set(volume); - - return ACK_ERROR_NONE; + player_volume_set(in->argv_u32val[1]); + return 0; } /* @@ -1284,22 +1251,19 @@ mpd_command_setvol(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * 1 = repeat song * Thus "oneshot" is accepted, but ignored under all circumstances. */ -static enum mpd_ack_error -mpd_command_single(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_single(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - int enable; + bool has_enable = (in->has_num & MPD_WANTS_NUM_ARG1_UVAL); + uint32_t enable = in->argv_u32val[1]; struct player_status status; - int ret; - ret = safe_atoi32(argv[1], &enable); - if (ret < 0) - { - /* 0.21 protocol: accept "oneshot" mode */ - if (strcmp(argv[1], "oneshot") == 0) - return 0; - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } + // 0.21 protocol: accept "oneshot" mode + if (strcmp(in->argv[1], "oneshot") == 0) + return 0; + + if (!has_enable) + RETURN_ERROR(ACK_ERROR_ARG, "Command 'single' expects integer or 'oneshot' argument"); player_get_status(&status); @@ -1310,7 +1274,7 @@ mpd_command_single(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, else player_repeat_set(REPEAT_SONG); - return ACK_ERROR_NONE; + return 0; } /* @@ -1318,32 +1282,22 @@ mpd_command_single(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * The server does not support replay gain, therefor this function returns always * "replay_gain_mode: off". */ -static enum mpd_ack_error -mpd_command_replay_gain_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_replay_gain_status(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - evbuffer_add(evbuf, "replay_gain_mode: off\n", 22); - return ACK_ERROR_NONE; + evbuffer_add(out->evbuf, "replay_gain_mode: off\n", 22); + return 0; } /* * Command handler function for 'volume' * Changes the volume by the given amount, expects argument argv[1] to be an integer - * - * According to the mpd protocoll specification this function is deprecated. */ -static enum mpd_ack_error -mpd_command_volume(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_volume(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct player_status status; - int volume; - int ret; - - ret = safe_atoi32(argv[1], &volume); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } + int32_t volume = in->argv_i32val[1]; player_get_status(&status); @@ -1351,34 +1305,27 @@ mpd_command_volume(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, player_volume_set(volume); - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'next' * Skips to the next song in the playqueue */ -static enum mpd_ack_error -mpd_command_next(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_next(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int ret; ret = player_playback_next(); - if (ret < 0) - { - *errmsg = safe_asprintf("Failed to skip to next song"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to skip to next song"); ret = player_playback_start(); if (ret < 0) - { - *errmsg = safe_asprintf("Player returned an error for start after nextitem"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Player returned an error for start after nextitem"); - return ACK_ERROR_NONE; + return 0; } /* @@ -1387,28 +1334,19 @@ mpd_command_next(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s * 0 = play * 1 = pause */ -static enum mpd_ack_error -mpd_command_pause(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_pause(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - int pause; + uint32_t pause; struct player_status status; int ret; player_get_status(&status); - if (argc > 1) - { - ret = safe_atoi32(argv[1], &pause); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - } + if (in->has_num & MPD_WANTS_NUM_ARG1_UVAL) + pause = in->argv_u32val[1]; else - { - pause = status.status == PLAY_PLAYING ? 1 : 0; - } + pause = (status.status == PLAY_PLAYING) ? 1 : 0; // MPD ignores pause in stopped state if (pause == 1 && status.status == PLAY_PLAYING) @@ -1419,12 +1357,9 @@ mpd_command_pause(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, ret = 0; if (ret < 0) - { - *errmsg = safe_asprintf("Failed to %s playback", (pause == 1) ? "pause" : "start"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to %s playback", (pause == 1) ? "pause" : "start"); - return ACK_ERROR_NONE; + return 0; } /* @@ -1432,47 +1367,30 @@ mpd_command_pause(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * Starts playback, the optional argument argv[1] represents the position in the playqueue * where to start playback. */ -static enum mpd_ack_error -mpd_command_play(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_play(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - int songpos; + bool has_songpos = (in->has_num & MPD_WANTS_NUM_ARG1_UVAL); + uint32_t songpos = in->argv_u32val[1]; struct player_status status; struct db_queue_item *queue_item; int ret; - songpos = -1; - if (argc > 1) - { - ret = safe_atoi32(argv[1], &songpos); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - } - player_get_status(&status); - if (status.status == PLAY_PLAYING && songpos < 0) - { - DPRINTF(E_DBG, L_MPD, "Ignoring play command with parameter '%s', player is already playing.\n", argv[1]); - return ACK_ERROR_NONE; - } + if (status.status == PLAY_PLAYING && !has_songpos) + return 0; + // Stop playback, if player is already playing and a valid song position is + // given (it will be restarted for the given song position) if (status.status == PLAY_PLAYING) - { - // Stop playback, if player is already playing and a valid song position is given (it will be restarted for the given song position) - player_playback_stop(); - } + player_playback_stop(); - if (songpos > 0) + if (has_songpos) { queue_item = db_queue_fetch_bypos(songpos, 0); if (!queue_item) - { - *errmsg = safe_asprintf("Failed to start playback"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback, unknown song position"); ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); @@ -1481,12 +1399,9 @@ mpd_command_play(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s ret = player_playback_start(); if (ret < 0) - { - *errmsg = safe_asprintf("Failed to start playback"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback (queue empty?)"); - return ACK_ERROR_NONE; + return 0; } /* @@ -1494,42 +1409,27 @@ mpd_command_play(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s * Starts playback, the optional argument argv[1] represents the songid of the song * where to start playback. */ -static enum mpd_ack_error -mpd_command_playid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_playid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - uint32_t id; + bool has_id = (in->has_num & MPD_WANTS_NUM_ARG1_UVAL); + uint32_t id = in->argv_u32val[1]; struct player_status status; struct db_queue_item *queue_item; int ret; player_get_status(&status); - id = 0; - if (argc > 1) - { - //TODO [mpd] mpd allows passing "-1" as argument and simply ignores it, the server fails to convert "-1" to an unsigned int - ret = safe_atou32(argv[1], &id); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - } - + // Stop playback, if player is already playing and a valid item id is given + // (it will be restarted for the given song) if (status.status == PLAY_PLAYING) - { - // Stop playback, if player is already playing and a valid item id is given (it will be restarted for the given song) - player_playback_stop(); - } + player_playback_stop(); - if (id > 0) + if (has_id) { queue_item = db_queue_fetch_byitemid(id); if (!queue_item) - { - *errmsg = safe_asprintf("Failed to start playback"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback, unknown song id"); ret = player_playback_start_byitem(queue_item); free_queue_item(queue_item, 0); @@ -1538,82 +1438,57 @@ mpd_command_playid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, ret = player_playback_start(); if (ret < 0) - { - *errmsg = safe_asprintf("Failed to start playback"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback (queue empty?)"); - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'previous' * Skips to the previous song in the playqueue */ -static enum mpd_ack_error -mpd_command_previous(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_previous(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int ret; ret = player_playback_prev(); - if (ret < 0) - { - *errmsg = safe_asprintf("Failed to skip to previous song"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to skip to previous song"); ret = player_playback_start(); if (ret < 0) - { - *errmsg = safe_asprintf("Player returned an error for start after previtem"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Player returned an error for start after previtem"); - return ACK_ERROR_NONE; + return 0; } /* - * Command handler function for 'seekid' + * Command handler function for 'seek' * Seeks to song at the given position in argv[1] to the position in seconds given in argument argv[2] * (fractions allowed). */ -static enum mpd_ack_error -mpd_command_seek(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_seek(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - uint32_t songpos; float seek_target_sec; int seek_target_msec; int ret; - ret = safe_atou32(argv[1], &songpos); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - //TODO Allow seeking in songs not currently playing - seek_target_sec = strtof(argv[2], NULL); + seek_target_sec = strtof(in->argv[2], NULL); seek_target_msec = seek_target_sec * 1000; ret = player_playback_seek(seek_target_msec, PLAYER_SEEK_POSITION); - if (ret < 0) - { - *errmsg = safe_asprintf("Failed to seek current song to time %d msec", seek_target_msec); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to seek current song to time %d msec", seek_target_msec); ret = player_playback_start(); - if (ret < 0) - { - *errmsg = safe_asprintf("Player returned an error for start after seekcur"); - return ACK_ERROR_UNKNOWN; - } + if (ret < 0) + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback"); - return ACK_ERROR_NONE; + return 0; } /* @@ -1621,102 +1496,73 @@ mpd_command_seek(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s * Seeks to song with id given in argv[1] to the position in seconds given in argument argv[2] * (fractions allowed). */ -static enum mpd_ack_error -mpd_command_seekid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_seekid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct player_status status; - uint32_t id; float seek_target_sec; int seek_target_msec; int ret; - ret = safe_atou32(argv[1], &id); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - //TODO Allow seeking in songs not currently playing player_get_status(&status); - if (status.item_id != id) - { - *errmsg = safe_asprintf("Given song is not the current playing one, seeking is not supported"); - return ACK_ERROR_UNKNOWN; - } + if (status.item_id != in->argv_u32val[1]) + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Given song is not the current playing one, seeking is not supported"); - seek_target_sec = strtof(argv[2], NULL); + seek_target_sec = strtof(in->argv[2], NULL); seek_target_msec = seek_target_sec * 1000; ret = player_playback_seek(seek_target_msec, PLAYER_SEEK_POSITION); - if (ret < 0) - { - *errmsg = safe_asprintf("Failed to seek current song to time %d msec", seek_target_msec); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to seek current song to time %d msec", seek_target_msec); ret = player_playback_start(); if (ret < 0) - { - *errmsg = safe_asprintf("Player returned an error for start after seekcur"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback"); - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'seekcur' * Seeks the current song to the position in seconds given in argument argv[1] (fractions allowed). */ -static enum mpd_ack_error -mpd_command_seekcur(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_seekcur(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { float seek_target_sec; int seek_target_msec; int ret; - seek_target_sec = strtof(argv[1], NULL); + seek_target_sec = strtof(in->argv[1], NULL); seek_target_msec = seek_target_sec * 1000; // TODO If prefixed by '+' or '-', then the time is relative to the current playing position. ret = player_playback_seek(seek_target_msec, PLAYER_SEEK_POSITION); - if (ret < 0) - { - *errmsg = safe_asprintf("Failed to seek current song to time %d msec", seek_target_msec); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to seek current song to time %d msec", seek_target_msec); ret = player_playback_start(); if (ret < 0) - { - *errmsg = safe_asprintf("Player returned an error for start after seekcur"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start playback"); - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'stop' * Stop playback. */ -static enum mpd_ack_error -mpd_command_stop(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_stop(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int ret; ret = player_playback_stop(); - if (ret != 0) - { - *errmsg = safe_asprintf("Failed to stop playback"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to stop playback"); - return ACK_ERROR_NONE; + return 0; } /* @@ -1729,34 +1575,23 @@ mpd_command_stop(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s static int mpd_queue_add(char *path, bool exact_match, int position) { - struct query_params qp; + struct query_params qp = { .type = Q_ITEMS, .idx_type = I_NONE, .sort = S_ARTIST }; struct player_status status; int new_item_id; int ret; new_item_id = 0; - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_ITEMS; - qp.idx_type = I_NONE; - qp.sort = S_ARTIST; if (exact_match) - qp.filter = db_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q'", path); + CHECK_NULL(L_MPD, qp.filter = db_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q'", path)); else - qp.filter = db_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q%%'", path); - - if (!qp.filter) - { - DPRINTF(E_DBG, L_PLAYER, "Out of memory\n"); - return -1; - } + CHECK_NULL(L_MPD, qp.filter = db_mprintf("f.disabled = 0 AND f.virtual_path LIKE '/%q%%'", path)); player_get_status(&status); ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, position, NULL, &new_item_id); - free(qp.filter); + free_query_params(&qp, 1); if (ret == 0) return new_item_id; @@ -1769,93 +1604,77 @@ mpd_queue_add(char *path, bool exact_match, int position) * Adds the all songs under the given path to the end of the playqueue (directories add recursively). * Expects argument argv[1] to be a path to a single file or directory. */ -static enum mpd_ack_error -mpd_command_add(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_add(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct player_status status; int ret; - ret = mpd_queue_add(argv[1], false, -1); - + ret = mpd_queue_add(in->argv[1], false, -1); if (ret < 0) - { - *errmsg = safe_asprintf("Failed to add song '%s' to playlist", argv[1]); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist", in->argv[1]); if (ret == 0) { 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(in->argv[1], -1, 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]); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist (unknown path)", in->argv[1]); } - return ACK_ERROR_NONE; + return 0; } /* * 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. + * as int is the absolute new position, if present as "+x" og "-x" it is relative. */ -static enum mpd_ack_error -mpd_command_addid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_addid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct player_status status; int to_pos = -1; + struct player_status status; + char *path = in->argv[1]; int ret; - if (argc > 2) + if (in->argc > 2) { - ret = safe_atoi32(argv[2], &to_pos); + ret = to_pos_from_arg(&to_pos, in->argv[2]); if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Invalid TO argument: '%s'", in->argv[2]); } - ret = mpd_queue_add(argv[1], true, to_pos); - + ret = mpd_queue_add(path, true, to_pos); if (ret == 0) { player_get_status(&status); // Given path is not in the library, directly add it as a new queue item - ret = library_queue_item_add(argv[1], to_pos, status.shuffle, status.item_id, NULL, NULL); + ret = library_queue_item_add(path, 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]); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist (unknown path)", path); } if (ret < 0) - { - *errmsg = safe_asprintf("Failed to add song '%s' to playlist", argv[1]); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist", path); - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "Id: %d\n", ret); // mpd_queue_add returns the item_id of the last inserted queue item - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'clear' * Stops playback and removes all songs from the playqueue */ -static enum mpd_ack_error -mpd_command_clear(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_clear(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int ret; @@ -1867,7 +1686,7 @@ mpd_command_clear(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, db_queue_clear(0); - return ACK_ERROR_NONE; + return 0; } /* @@ -1876,8 +1695,8 @@ mpd_command_clear(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * an integer range {START:END} representing the position of the songs in the playlist, that * should be removed. */ -static enum mpd_ack_error -mpd_command_delete(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_delete(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int start_pos; int end_pos; @@ -1885,84 +1704,62 @@ mpd_command_delete(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, int ret; // If argv[1] is omitted clear the whole queue - if (argc < 2) + if (in->argc < 2) { db_queue_clear(0); - return ACK_ERROR_NONE; + return 0; } // If argument argv[1] is present remove only the specified songs - ret = mpd_pars_range_arg(argv[1], &start_pos, &end_pos); + ret = range_pos_from_arg(&start_pos, &end_pos, in->argv[1]); if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer or range: '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Argument doesn't convert to integer or range: '%s'", in->argv[1]); count = end_pos - start_pos; ret = db_queue_delete_bypos(start_pos, count); if (ret < 0) - { - *errmsg = safe_asprintf("Failed to remove %d songs starting at position %d", count, start_pos); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to remove %d songs starting at position %d", count, start_pos); - return ACK_ERROR_NONE; + return 0; } /* Command handler function for 'deleteid' * Removes the song with given id from the playqueue. Expects argument argv[1] to be an integer (song id). */ -static enum mpd_ack_error -mpd_command_deleteid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_deleteid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - uint32_t songid; + uint32_t songid = in->argv_u32val[1]; int ret; - ret = safe_atou32(argv[1], &songid); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - ret = db_queue_delete_byitemid(songid); if (ret < 0) - { - *errmsg = safe_asprintf("Failed to remove song with id '%s'", argv[1]); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to remove song with id '%s'", in->argv[1]); - return ACK_ERROR_NONE; + return 0; } // Moves the song at FROM or range of songs at START:END to TO in the playlist. -static enum mpd_ack_error -mpd_command_move(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_move(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int start_pos; int end_pos; int count; - uint32_t to_pos; + int to_pos; uint32_t queue_length; int ret; - ret = mpd_pars_range_arg(argv[1], &start_pos, &end_pos); + ret = range_pos_from_arg(&start_pos, &end_pos, in->argv[1]); if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer or range: '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Argument doesn't convert to integer or range: '%s'", in->argv[1]); count = end_pos - start_pos; - ret = safe_atou32(argv[2], &to_pos); + ret = to_pos_from_arg(&to_pos, in->argv[2]); if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Invalid argument: '%s'", in->argv[2]); db_queue_get_count(&queue_length); @@ -1973,50 +1770,31 @@ mpd_command_move(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s if (!(start_pos >= 0 && start_pos < queue_length && end_pos > start_pos && end_pos <= queue_length && to_pos >= 0 && to_pos <= queue_length - count)) - { - *errmsg = safe_asprintf("Range too large for target position %d or bad song index (count %d, length %u)", to_pos, count, queue_length); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Range too large for target position %d or bad song index (count %d, length %u)", to_pos, count, queue_length); ret = db_queue_move_bypos_range(start_pos, end_pos, to_pos); if (ret < 0) - { - *errmsg = safe_asprintf("Failed to move song at position %d to %d", start_pos, to_pos); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to move song at position %d to %d", start_pos, to_pos); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_moveid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_moveid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - uint32_t songid; - uint32_t to_pos; + uint32_t songid = in->argv_u32val[1]; + int to_pos; int ret; - ret = safe_atou32(argv[1], &songid); + ret = to_pos_from_arg(&to_pos, in->argv[2]); if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - - ret = safe_atou32(argv[2], &to_pos); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Invalid TO argument: '%s'", in->argv[2]); ret = db_queue_move_byitemid(songid, to_pos, 0); if (ret < 0) - { - *errmsg = safe_asprintf("Failed to move song with id '%s' to index '%s'", argv[1], argv[2]); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to move song with id '%u' to index '%u'", songid, to_pos); - return ACK_ERROR_NONE; + return 0; } /* @@ -2026,59 +1804,42 @@ mpd_command_moveid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * * The order of the songs is always the not shuffled order. */ -static enum mpd_ack_error -mpd_command_playlistid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_playlistid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params query_params; + bool has_songid = (in->has_num & MPD_WANTS_NUM_ARG1_UVAL); + uint32_t songid = in->argv_u32val[1]; + struct query_params qp = { 0 }; struct db_queue_item queue_item; - uint32_t songid; int ret; - songid = 0; + if (has_songid) + qp.filter = db_mprintf("id = %d", songid); - if (argc > 1) - { - ret = safe_atou32(argv[1], &songid); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - } - - memset(&query_params, 0, sizeof(struct query_params)); - - if (songid > 0) - query_params.filter = db_mprintf("id = %d", songid); - - ret = db_queue_enum_start(&query_params); + ret = db_queue_enum_start(&qp); if (ret < 0) { - free(query_params.filter); - *errmsg = safe_asprintf("Failed to start queue enum for command playlistid: '%s'", argv[1]); - return ACK_ERROR_ARG; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_ARG, "Failed to start queue enum for command playlistid"); } - while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) + while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { - ret = mpd_add_db_queue_item(evbuf, &queue_item); + ret = mpd_add_db_queue_item(out->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); - return ACK_ERROR_UNKNOWN; + db_queue_enum_end(&qp); + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info for file"); } } - db_queue_enum_end(&query_params); - free(query_params.filter); + db_queue_enum_end(&qp); + free_query_params(&qp, 1); - return ACK_ERROR_NONE; + return 0; } - /* * Command handler function for 'playlistinfo' * Displays a list of all songs in the queue, or if the optional argument is given, displays information @@ -2086,10 +1847,10 @@ mpd_command_playlistid(struct evbuffer *evbuf, int argc, char **argv, char **err * * The order of the songs is always the not shuffled order. */ -static enum mpd_ack_error -mpd_command_playlistinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_playlistinfo(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params query_params; + struct query_params qp = { 0 }; struct db_queue_item queue_item; int start_pos; int end_pos; @@ -2097,375 +1858,320 @@ mpd_command_playlistinfo(struct evbuffer *evbuf, int argc, char **argv, char **e start_pos = 0; end_pos = 0; - memset(&query_params, 0, sizeof(struct query_params)); - if (argc > 1) + if (in->argc > 1) { - ret = mpd_pars_range_arg(argv[1], &start_pos, &end_pos); + ret = range_pos_from_arg(&start_pos, &end_pos, in->argv[1]); if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer or range: '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Argument doesn't convert to integer or range: '%s'", in->argv[1]); if (start_pos < 0) - DPRINTF(E_DBG, L_MPD, "Command 'playlistinfo' called with pos < 0 (arg = '%s'), ignore arguments and return whole queue\n", argv[1]); + DPRINTF(E_DBG, L_MPD, "Command 'playlistinfo' called with pos < 0 (arg = '%s'), ignore arguments and return whole queue\n", in->argv[1]); else - query_params.filter = db_mprintf("pos >= %d AND pos < %d", start_pos, end_pos); + qp.filter = db_mprintf("pos >= %d AND pos < %d", start_pos, end_pos); } - ret = db_queue_enum_start(&query_params); + ret = db_queue_enum_start(&qp); if (ret < 0) - { - free(query_params.filter); - *errmsg = safe_asprintf("Failed to start queue enum for command playlistinfo: '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Failed to start queue enum for command playlistinfo: '%s'", in->argv[1]); - while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) + while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { - ret = mpd_add_db_queue_item(evbuf, &queue_item); + ret = mpd_add_db_queue_item(out->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); - return ACK_ERROR_UNKNOWN; + db_queue_enum_end(&qp); + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info"); } } - db_queue_enum_end(&query_params); - free(query_params.filter); + db_queue_enum_end(&qp); + free_query_params(&qp, 1); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_playlistfind(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +/* + * playlistfind {FILTER} [sort {TYPE}] [window {START:END}] + * Searches for songs that match in the queue + * TODO add support for window (currently not supported by db_queue_enum_x) + */ +static int +mpd_command_playlistfind(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params query_params; + struct query_params qp = { 0 }; struct db_queue_item queue_item; int ret; - memset(&query_params, 0, sizeof(struct query_params)); + ret = parse_command(&qp, NULL, NULL, in); + if (ret < 0) + RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) for command 'playlistfind'"); - 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); - - ret = db_queue_enum_start(&query_params); + ret = db_queue_enum_start(&qp); if (ret < 0) { - free(query_params.filter); - *errmsg = safe_asprintf("Failed to start queue enum for command playlistinfo: '%s'", argv[1]); - return ACK_ERROR_ARG; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_ARG, "Failed to start queue enum for command playlistinfo: '%s'", in->argv[1]); } - while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) + while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { - ret = mpd_add_db_queue_item(evbuf, &queue_item); + ret = mpd_add_db_queue_item(out->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); - return ACK_ERROR_UNKNOWN; + db_queue_enum_end(&qp); + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info"); } } - db_queue_enum_end(&query_params); - free(query_params.filter); + db_queue_enum_end(&qp); + free_query_params(&qp, 1); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_playlistsearch(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +/* + * playlistsearch {FILTER} [sort {TYPE}] [window {START:END}] + */ +static int +mpd_command_playlistsearch(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params query_params; + struct query_params qp = { 0 }; struct db_queue_item queue_item; int ret; - memset(&query_params, 0, sizeof(struct query_params)); + ret = parse_command(&qp, NULL, NULL, in); + if (ret < 0) + RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) for command 'playlistsearch'"); - 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); - - ret = db_queue_enum_start(&query_params); + ret = db_queue_enum_start(&qp); if (ret < 0) { - free(query_params.filter); - *errmsg = safe_asprintf("Failed to start queue enum for command playlistinfo: '%s'", argv[1]); - return ACK_ERROR_ARG; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_ARG, "Failed to start queue enum for command playlistinfo: '%s'", in->argv[1]); } - while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) + while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) { - ret = mpd_add_db_queue_item(evbuf, &queue_item); + ret = mpd_add_db_queue_item(out->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); - return ACK_ERROR_UNKNOWN; + db_queue_enum_end(&qp); + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info"); } } - db_queue_enum_end(&query_params); - free(query_params.filter); + db_queue_enum_end(&qp); + free_query_params(&qp, 1); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -plchanges_build_queryparams(struct query_params *query_params, int argc, char **argv, char **errmsg) +static int +plchanges_build_queryparams(struct query_params *qp, uint32_t version, const char *range) { - uint32_t version; int start_pos; int end_pos; int ret; - memset(query_params, 0, sizeof(struct query_params)); - - ret = safe_atou32(argv[1], &version); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } + memset(qp, 0, sizeof(struct query_params)); start_pos = 0; end_pos = 0; - if (argc > 2) + if (range) { - ret = mpd_pars_range_arg(argv[2], &start_pos, &end_pos); + ret = range_pos_from_arg(&start_pos, &end_pos, range); if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer or range: '%s'", argv[2]); - return ACK_ERROR_ARG; - } + return -1; if (start_pos < 0) - DPRINTF(E_DBG, L_MPD, "Command 'playlistinfo' called with pos < 0 (arg = '%s'), ignore arguments and return whole queue\n", argv[1]); + DPRINTF(E_DBG, L_MPD, "Invalid range '%s', will return entire queue\n", range); } if (start_pos < 0 || end_pos <= 0) - query_params->filter = db_mprintf("(queue_version > %d)", version); + qp->filter = db_mprintf("(queue_version > %d)", version); else - query_params->filter = db_mprintf("(queue_version > %d AND pos >= %d AND pos < %d)", version, start_pos, end_pos); + qp->filter = db_mprintf("(queue_version > %d AND pos >= %d AND pos < %d)", version, start_pos, end_pos); - return ACK_ERROR_NONE; + return 0; } /* - * Command handler function for 'plchanges' + * plchanges {VERSION} [START:END] * Lists all changed songs in the queue since the given playlist version in argv[1]. */ -static enum mpd_ack_error -mpd_command_plchanges(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_plchanges(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params query_params; + uint32_t version = in->argv_u32val[1]; + const char *range = (in->argc > 2) ? in->argv[2] : NULL; + struct query_params qp; struct db_queue_item queue_item; int ret; - enum mpd_ack_error ack_error; - ack_error = plchanges_build_queryparams(&query_params, argc, argv, errmsg); - if (ack_error != ACK_ERROR_NONE) - return ack_error; - - ret = db_queue_enum_start(&query_params); + ret = plchanges_build_queryparams(&qp, version, range); if (ret < 0) - goto error; + RETURN_ERROR(ACK_ERROR_ARG, "Invalid range '%s' provided to plchanges", range); - while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) + ret = db_queue_enum_start(&qp); + if (ret < 0) { - ret = mpd_add_db_queue_item(evbuf, &queue_item); + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start queue enum for command plchangesposid"); + } + + while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) + { + ret = mpd_add_db_queue_item(out->evbuf, &queue_item); if (ret < 0) { - DPRINTF(E_LOG, L_MPD, "Error adding media info for file with id: %d", queue_item.file_id); - goto error; + db_queue_enum_end(&qp); + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error adding media info"); } } - db_queue_enum_end(&query_params); - free_query_params(&query_params, 1); + db_queue_enum_end(&qp); + free_query_params(&qp, 1); - return ACK_ERROR_NONE; - - error: - db_queue_enum_end(&query_params); - free_query_params(&query_params, 1); - *errmsg = safe_asprintf("Failed to start queue enum for command plchanges"); - return ACK_ERROR_UNKNOWN; + return 0; } /* - * Command handler function for 'plchangesposid' - * Lists all changed songs in the queue since the given playlist version in argv[1] without metadata. + * plchangesposid {VERSION} [START:END] + * Lists all changed songs in the queue since the given playlist version in + * argv[1] without metadata. */ -static enum mpd_ack_error -mpd_command_plchangesposid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_plchangesposid(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params query_params; + uint32_t version = in->argv_u32val[1]; + const char *range = (in->argc > 2) ? in->argv[2] : NULL; + struct query_params qp; struct db_queue_item queue_item; int ret; - ret = plchanges_build_queryparams(&query_params, argc, argv, errmsg); - if (ret != 0) - return ret; - - ret = db_queue_enum_start(&query_params); + ret = plchanges_build_queryparams(&qp, version, range); if (ret < 0) - goto error; + RETURN_ERROR(ACK_ERROR_ARG, "Invalid range '%s' provided to plchangesposid", range); - while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) + ret = db_queue_enum_start(&qp); + if (ret < 0) { - evbuffer_add_printf(evbuf, + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start queue enum for command plchangesposid"); + } + + while ((ret = db_queue_enum_fetch(&qp, &queue_item)) == 0 && queue_item.id > 0) + { + evbuffer_add_printf(out->evbuf, "cpos: %d\n" "Id: %d\n", queue_item.pos, queue_item.id); } - db_queue_enum_end(&query_params); - free_query_params(&query_params, 1); + db_queue_enum_end(&qp); + free_query_params(&qp, 1); - return ACK_ERROR_NONE; - - error: - db_queue_enum_end(&query_params); - free_query_params(&query_params, 1); - *errmsg = safe_asprintf("Failed to start queue enum for command plchangesposid"); - return ACK_ERROR_UNKNOWN; + return 0; } /* * Command handler function for 'listplaylist' * Lists all songs in the playlist given by virtual-path in argv[1]. */ -static enum mpd_ack_error -mpd_command_listplaylist(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_listplaylist(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *path; struct playlist_info *pli; - struct query_params qp; + struct query_params qp = { .type = Q_PLITEMS, .idx_type = I_NONE }; struct db_media_file_info dbmfi; int ret; - if (!default_pl_dir || strstr(argv[1], ":/")) + if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' - path = prepend_slash(argv[1]); + path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory - path = safe_asprintf("%s/%s", default_pl_dir, argv[1]); + path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } pli = db_pl_fetch_byvirtualpath(path); free(path); if (!pli) - { - *errmsg = safe_asprintf("Playlist not found for path '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Playlist not found for path '%s'", in->argv[1]); - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_PLITEMS; - qp.idx_type = I_NONE; qp.id = pli->id; ret = db_query_start(&qp); if (ret < 0) { - db_query_end(&qp); - free_pli(pli, 0); - - *errmsg = safe_asprintf("Could not start query"); - return ACK_ERROR_UNKNOWN; + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "file: %s\n", (dbmfi.virtual_path + 1)); } db_query_end(&qp); - free_pli(pli, 0); - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'listplaylistinfo' * Lists all songs in the playlist given by virtual-path in argv[1] with metadata. */ -static enum mpd_ack_error -mpd_command_listplaylistinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_listplaylistinfo(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *path; struct playlist_info *pli; - struct query_params qp; + struct query_params qp = { .type = Q_PLITEMS, .idx_type = I_NONE }; struct db_media_file_info dbmfi; int ret; - if (!default_pl_dir || strstr(argv[1], ":/")) + if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' - path = prepend_slash(argv[1]); + path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory - path = safe_asprintf("%s/%s", default_pl_dir, argv[1]); + path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } pli = db_pl_fetch_byvirtualpath(path); free(path); if (!pli) - { - *errmsg = safe_asprintf("Playlist not found for path '%s'", argv[1]); - return ACK_ERROR_NO_EXIST; - } + RETURN_ERROR(ACK_ERROR_NO_EXIST, "Playlist not found for path '%s'", in->argv[1]); - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_PLITEMS; - qp.idx_type = I_NONE; qp.id = pli->id; ret = db_query_start(&qp); if (ret < 0) { - db_query_end(&qp); - free_pli(pli, 0); - - *errmsg = safe_asprintf("Could not start query"); - return ACK_ERROR_UNKNOWN; + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { - ret = mpd_add_db_media_file_info(evbuf, &dbmfi); + ret = mpd_add_db_media_file_info(out->evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); @@ -2473,55 +2179,45 @@ mpd_command_listplaylistinfo(struct evbuffer *evbuf, int argc, char **argv, char } db_query_end(&qp); - free_pli(pli, 0); - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'listplaylists' * Lists all playlists with their last modified date. */ -static enum mpd_ack_error -mpd_command_listplaylists(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_listplaylists(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params qp; + struct query_params qp = { .type = Q_PL, .idx_type = I_NONE, .sort = S_PLAYLIST }; struct db_playlist_info dbpli; char modified[32]; uint32_t time_modified; int ret; - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_PL; - qp.sort = S_PLAYLIST; - qp.idx_type = I_NONE; qp.filter = db_mprintf("(f.type = %d OR f.type = %d)", PL_PLAIN, PL_SMART); ret = db_query_start(&qp); if (ret < 0) { - db_query_end(&qp); - free(qp.filter); - - *errmsg = safe_asprintf("Could not start query"); - return ACK_ERROR_UNKNOWN; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id)) { if (safe_atou32(dbpli.db_timestamp, &time_modified) != 0) { - *errmsg = safe_asprintf("Error converting time modified to uint32_t: %s\n", dbpli.db_timestamp); db_query_end(&qp); - free(qp.filter); - return ACK_ERROR_UNKNOWN; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Error converting time modified to uint32_t"); } mpd_time(modified, sizeof(modified), time_modified); - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "playlist: %s\n" "Last-Modified: %s\n", (dbpli.virtual_path + 1), @@ -2529,17 +2225,17 @@ mpd_command_listplaylists(struct evbuffer *evbuf, int argc, char **argv, char ** } db_query_end(&qp); - free(qp.filter); + free_query_params(&qp, 1); - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'load' * Adds the playlist given by virtual-path in argv[1] to the queue. */ -static enum mpd_ack_error -mpd_command_load(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_load(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *path; struct playlist_info *pli; @@ -2547,24 +2243,21 @@ mpd_command_load(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s struct query_params qp = { .type = Q_PLITEMS }; int ret; - if (!default_pl_dir || strstr(argv[1], ":/")) + if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' - path = prepend_slash(argv[1]); + path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory - path = safe_asprintf("%s/%s", default_pl_dir, argv[1]); + path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } pli = db_pl_fetch_byvirtualpath(path); free(path); if (!pli) - { - *errmsg = safe_asprintf("Playlist not found for path '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Playlist not found for path '%s'", in->argv[1]); //TODO If a second parameter is given only add the specified range of songs to the playqueue @@ -2575,192 +2268,172 @@ mpd_command_load(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, -1, NULL, NULL); if (ret < 0) - { - *errmsg = safe_asprintf("Failed to add song '%s' to playlist", argv[1]); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to add song '%s' to playlist", in->argv[1]); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_playlistadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_playlistadd(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *vp_playlist; char *vp_item; int ret; if (!allow_modifying_stored_playlists) - { - *errmsg = safe_asprintf("Modifying stored playlists is not enabled"); - return ACK_ERROR_PERMISSION; - } + RETURN_ERROR(ACK_ERROR_PERMISSION, "Modifying stored playlists is not enabled"); - if (!default_pl_dir || strstr(argv[1], ":/")) + if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' - vp_playlist = prepend_slash(argv[1]); + vp_playlist = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory - vp_playlist = safe_asprintf("%s/%s", default_pl_dir, argv[1]); + vp_playlist = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } - vp_item = prepend_slash(argv[2]); + vp_item = prepend_slash(in->argv[2]); ret = library_playlist_item_add(vp_playlist, vp_item); free(vp_playlist); free(vp_item); if (ret < 0) - { - *errmsg = safe_asprintf("Error adding item to file '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Error adding item to file '%s'", in->argv[1]); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_rm(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_rm(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *virtual_path; int ret; if (!allow_modifying_stored_playlists) - { - *errmsg = safe_asprintf("Modifying stored playlists is not enabled"); - return ACK_ERROR_PERMISSION; - } + RETURN_ERROR(ACK_ERROR_PERMISSION, "Modifying stored playlists is not enabled"); - if (!default_pl_dir || strstr(argv[1], ":/")) + if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' - virtual_path = prepend_slash(argv[1]); + virtual_path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory - virtual_path = safe_asprintf("%s/%s", default_pl_dir, argv[1]); + virtual_path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } ret = library_playlist_remove(virtual_path); free(virtual_path); if (ret < 0) - { - *errmsg = safe_asprintf("Error removing playlist '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Error removing playlist '%s'", in->argv[1]); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_save(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_save(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { char *virtual_path; int ret; if (!allow_modifying_stored_playlists) - { - *errmsg = safe_asprintf("Modifying stored playlists is not enabled"); - return ACK_ERROR_PERMISSION; - } + RETURN_ERROR(ACK_ERROR_PERMISSION, "Modifying stored playlists is not enabled"); - if (!default_pl_dir || strstr(argv[1], ":/")) + if (!default_pl_dir || strstr(in->argv[1], ":/")) { // Argument is a virtual path, make sure it starts with a '/' - virtual_path = prepend_slash(argv[1]); + virtual_path = prepend_slash(in->argv[1]); } else { // Argument is a playlist name, prepend default playlist directory - virtual_path = safe_asprintf("%s/%s", default_pl_dir, argv[1]); + virtual_path = safe_asprintf("%s/%s", default_pl_dir, in->argv[1]); } ret = library_queue_save(virtual_path); free(virtual_path); if (ret < 0) - { - *errmsg = safe_asprintf("Error saving queue to file '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Error saving queue to file '%s'", in->argv[1]); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_count(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +/* + * count [FILTER] [group {GROUPTYPE}] + * + * TODO Support for groups (the db interface doesn't have method for this). Note + * that mpd only supports one group. Mpd has filter as optional. Without filter, + * but with group, mpd returns: + * + * Album: Welcome + * songs: 1 + * playtime: 249 + */ +static int +mpd_command_count(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params qp; + struct query_params qp = { .type = Q_COUNT_ITEMS }; struct filecount_info fci; int ret; - if (argc < 3 || ((argc - 1) % 2) != 0) - { - *errmsg = safe_asprintf("Missing argument(s) for command 'find'"); - 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); + ret = parse_command(&qp, NULL, NULL, in); + if (ret < 0) + RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) for command 'count'"); ret = db_filecount_get(&fci, &qp); if (ret < 0) { - free(qp.filter); - - *errmsg = safe_asprintf("Could not start query"); - return ACK_ERROR_UNKNOWN; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "songs: %d\n" "playtime: %" PRIu64 "\n", fci.count, (fci.length / 1000)); db_query_end(&qp); - free(qp.filter); - - return ACK_ERROR_NONE; + free_query_params(&qp, 1); + return 0; } -static enum mpd_ack_error -mpd_command_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +/* + * find "albumartist" "Blue Foundation" "album" "Life Of A Ghost" "date" "2007" "window" "0:1" + * search "(modified-since '2024-06-04T22:49:41Z')" + * find "((Album == 'No Sign Of Bad') AND (AlbumArtist == 'Led Zeppelin'))" window 0:1 + * find "(Artist == \"foo\\'bar\\\"\")" + * + * TODO not sure if we support this correctly: "An empty value string means: + * match only if the given tag type does not exist at all; this implies that + * negation with an empty value checks for the existence of the given tag type." + * MaximumMPD (search function): + * count "((Artist == \"Blue Foundation\") AND (album == \"\"))" + */ +static int +mpd_command_find(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct query_params qp; + struct query_params qp = { .type = Q_ITEMS, .idx_type = I_NONE, .sort = S_ARTIST }; struct db_media_file_info dbmfi; int ret; - if (argc < 3 || ((argc - 1) % 2) != 0) - { - *errmsg = safe_asprintf("Missing argument(s) for command 'find'"); - return ACK_ERROR_ARG; - } - - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_ITEMS; - qp.sort = S_NAME; - qp.idx_type = I_NONE; - - parse_filter_window_params(argc - 1, argv + 1, true, &qp); + ret = parse_command(&qp, NULL, NULL, in); + if (ret < 0) + RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) in '%s'", in->args_raw); ret = db_query_start(&qp); if (ret < 0) { - db_query_end(&qp); - free(qp.filter); - - *errmsg = safe_asprintf("Could not start query"); - return ACK_ERROR_UNKNOWN; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { - ret = mpd_add_db_media_file_info(evbuf, &dbmfi); + ret = mpd_add_db_media_file_info(out->evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); @@ -2768,167 +2441,133 @@ mpd_command_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, s } db_query_end(&qp); - free(qp.filter); - - return ACK_ERROR_NONE; -} - -static enum mpd_ack_error -mpd_command_findadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) -{ - struct query_params qp; - struct player_status status; - int ret; - - if (argc < 3 || ((argc - 1) % 2) != 0) - { - *errmsg = safe_asprintf("Missing argument(s) for command 'findadd'"); - return ACK_ERROR_ARG; - } - - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_ITEMS; - qp.sort = S_ARTIST; - qp.idx_type = I_NONE; - - parse_filter_window_params(argc - 1, argv + 1, true, &qp); - - player_get_status(&status); - - ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, -1, NULL, NULL); - free(qp.filter); - if (ret < 0) - { - *errmsg = safe_asprintf("Failed to add songs to playlist"); - return ACK_ERROR_UNKNOWN; - } - - return ACK_ERROR_NONE; + free_query_params(&qp, 1); + return 0; } /* - * Some MPD clients crash if the tag value includes the newline character. - * While they should normally not be included in most ID3 tags, they sometimes - * are, so we just change them to space. See #1613 for more details. + * findadd "albumartist" "Blue Foundation" "album" "Life Of A Ghost" "date" "2007" "window" "3:4" position 0 */ -static void -sanitize_value(char **strval) +static int +mpd_command_findadd(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - char *ptr = *strval; - - while(*ptr != '\0') - { - if(*ptr == '\n') - { - *ptr = ' '; - } - ptr++; - } -} - -static enum mpd_ack_error -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 db_media_file_info dbmfi; - char **strval; - int i; + struct query_params qp = { .type = Q_ITEMS, .idx_type = I_NONE, .sort = S_ARTIST }; + char *pos = NULL; + int to_pos; int ret; - if (argc < 2 || ((argc % 2) != 0)) + ret = parse_command(&qp, &pos, NULL, in); + if (ret < 0) + goto error; + + ret = to_pos_from_arg(&to_pos, pos); + if (ret < 0) + goto error; + + ret = db_queue_add_by_query(&qp, 0, 0, to_pos, NULL, NULL); + if (ret < 0) + goto error; + + free_query_params(&qp, 1); + free(pos); + return 0; + + error: + free_query_params(&qp, 1); + free(pos); + RETURN_ERROR(ACK_ERROR_ARG, "Invalid arguments"); +} + +static void +groups_from_dbcols(struct mpd_tag_map *groups[], size_t sz, const char *dbcols) +{ + char *copy = strdup(dbcols); + char *saveptr; + char *col; + int i; + + for (col = strtok_r(copy, ",", &saveptr), i = 0; col && i < sz - 1; col = strtok_r(NULL, ",", &saveptr), i++) { - if (argc != 3 || (0 != strcasecmp(argv[1], "album"))) - { - *errmsg = safe_asprintf("Missing argument(s) for command 'list'"); - return ACK_ERROR_ARG; - } + trim(col); + groups[i] = mpd_parser_tag_from_dbcol(col); } - tagtype = find_tagtype(argv[1]); + groups[i] = NULL; + free(copy); +} - if (!tagtype || tagtype->type == MPD_TYPE_SPECIAL) //FIXME allow "file" tagtype +/* + * list {TYPE} {FILTER} [group {GROUPTYPE} [group {GROUPTYPE} [...]]] + * + * Examples + * Rygelian: list Album group Date group AlbumArtistSort group AlbumArtist + * Plattenalbum: list "albumsort" "albumartist" "Bob Dylan" "group" "date" "group" "album" + * list Album "(Artist starts_with \"K\")" group AlbumArtist + * + * TODO Note that the below repeats group tags like so: + * AlbumArtist: Kasabian + * Album: Empire + * AlbumArtist: Kasabian + * Album: West Ryder Pauper Lunatic Asylum + * mpd doesn't repeat them: + * AlbumArtist: Kasabian + * Album: Empire + * Album: West Ryder Pauper Lunatic Asylum + */ +static int +mpd_command_list(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) +{ + struct query_params qp = { .type = Q_ITEMS, .sort = S_ARTIST }; + struct db_media_file_info dbmfi; + struct mpd_tag_map *groups[16]; + char **strval; + int ret; + int i; + + ret = parse_command(&qp, NULL, NULL, in); + if (ret < 0) + RETURN_ERROR(ACK_ERROR_ARG, "Unknown argument(s) in: '%s'", in->args_raw); + + // qp.group should at least include the tag type field + if (!qp.group) { - DPRINTF(E_WARN, L_MPD, "Unsupported type argument for command 'list': %s\n", argv[1]); - return ACK_ERROR_NONE; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Bug! Unknown or unsupported tag type/groups: '%s'", in->args_raw); } - 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); - } - - group = NULL; - groupsize = 0; - parse_group_params(argc - 2, argv + 2, tagtype->group_in_listcommand, &qp, &group, &groupsize); + groups_from_dbcols(groups, ARRAY_SIZE(groups), qp.group); 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; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { - strval = (char **) ((char *)&dbmfi + tagtype->mfi_offset); - - if (!(*strval) || (**strval == '\0')) - continue; - - sanitize_value(strval); - evbuffer_add_printf(evbuf, - "%s: %s\n", - tagtype->tag, - *strval); - - if (group && groupsize > 0) + for (i = 0; i < ARRAY_SIZE(groups) && groups[i]; i++) { - for (i = 0; i < groupsize; i++) - { - if (!group[i]) - continue; + strval = (char **) ((char *)&dbmfi + groups[i]->dbmfi_offset); - strval = (char **) ((char *)&dbmfi + group[i]->mfi_offset); + if (!(*strval) || (**strval == '\0')) + continue; - if (!(*strval) || (**strval == '\0')) - continue; - - evbuffer_add_printf(evbuf, - "%s: %s\n", - group[i]->tag, - *strval); - } + evbuffer_add_printf(out->evbuf, "%s: %s\n", groups[i]->name, sanitize(*strval)); } } db_query_end(&qp); - free(qp.filter); - free(qp.group); - free(group); + free_query_params(&qp, 1); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_add_directory(struct evbuffer *evbuf, int directory_id, int listall, int listinfo, char **errmsg) +static int +mpd_add_directory(struct mpd_command_output *out, int directory_id, int listall, int listinfo) { struct directory_info subdir; - struct query_params qp; + struct query_params qp = { .type = Q_PL, .idx_type = I_NONE, .sort = S_PLAYLIST }; struct directory_enum dir_enum; struct db_playlist_info dbpli; char modified[32]; @@ -2937,18 +2576,12 @@ mpd_add_directory(struct evbuffer *evbuf, int directory_id, int listall, int lis int ret; // Load playlists for dir-id - memset(&qp, 0, sizeof(struct query_params)); - qp.type = Q_PL; - qp.sort = S_PLAYLIST; - qp.idx_type = I_NONE; qp.filter = db_mprintf("(f.directory_id = %d AND (f.type = %d OR f.type = %d))", directory_id, PL_PLAIN, PL_SMART); ret = db_query_start(&qp); if (ret < 0) { - db_query_end(&qp); - free(qp.filter); - *errmsg = safe_asprintf("Could not start query"); - return ACK_ERROR_UNKNOWN; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id)) { @@ -2960,7 +2593,7 @@ mpd_add_directory(struct evbuffer *evbuf, int directory_id, int listall, int lis if (listinfo) { mpd_time(modified, sizeof(modified), time_modified); - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "playlist: %s\n" "Last-Modified: %s\n", (dbpli.virtual_path + 1), @@ -2968,29 +2601,26 @@ mpd_add_directory(struct evbuffer *evbuf, int directory_id, int listall, int lis } else { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "playlist: %s\n", (dbpli.virtual_path + 1)); } } db_query_end(&qp); - free(qp.filter); + free_query_params(&qp, 1); // Load sub directories for dir-id memset(&dir_enum, 0, sizeof(struct directory_enum)); dir_enum.parent_id = directory_id; ret = db_directory_enum_start(&dir_enum); if (ret < 0) - { - *errmsg = safe_asprintf("Failed to start directory enum for parent_id %d\n", directory_id); - db_directory_enum_end(&dir_enum); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Failed to start directory enum"); + while ((ret = db_directory_enum_fetch(&dir_enum, &subdir)) == 0 && subdir.id > 0) { if (listinfo) { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "directory: %s\n" "Last-Modified: %s\n", (subdir.virtual_path + 1), @@ -2998,14 +2628,14 @@ mpd_add_directory(struct evbuffer *evbuf, int directory_id, int listall, int lis } else { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "directory: %s\n", (subdir.virtual_path + 1)); } if (listall) { - mpd_add_directory(evbuf, subdir.id, listall, listinfo, errmsg); + mpd_add_directory(out, subdir.id, listall, listinfo); } } db_directory_enum_end(&dir_enum); @@ -3019,16 +2649,14 @@ mpd_add_directory(struct evbuffer *evbuf, int directory_id, int listall, int lis ret = db_query_start(&qp); if (ret < 0) { - db_query_end(&qp); - free(qp.filter); - *errmsg = safe_asprintf("Could not start query"); - return ACK_ERROR_UNKNOWN; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) { if (listinfo) { - ret = mpd_add_db_media_file_info(evbuf, &dbmfi); + ret = mpd_add_db_media_file_info(out->evbuf, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); @@ -3036,125 +2664,109 @@ mpd_add_directory(struct evbuffer *evbuf, int directory_id, int listall, int lis } else { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "file: %s\n", (dbmfi.virtual_path + 1)); } } db_query_end(&qp); - free(qp.filter); + free_query_params(&qp, 1); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_listall(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_listall(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int dir_id; char parent[PATH_MAX]; int ret; - if (argc < 2 || strlen(argv[1]) == 0 - || (strncmp(argv[1], "/", 1) == 0 && strlen(argv[1]) == 1)) + if (in->argc < 2 || strlen(in->argv[1]) == 0 + || (strncmp(in->argv[1], "/", 1) == 0 && strlen(in->argv[1]) == 1)) { ret = snprintf(parent, sizeof(parent), "/"); } - else if (strncmp(argv[1], "/", 1) == 0) + else if (strncmp(in->argv[1], "/", 1) == 0) { - ret = snprintf(parent, sizeof(parent), "%s/", argv[1]); + ret = snprintf(parent, sizeof(parent), "%s/", in->argv[1]); } else { - ret = snprintf(parent, sizeof(parent), "/%s", argv[1]); + ret = snprintf(parent, sizeof(parent), "/%s", in->argv[1]); } if ((ret < 0) || (ret >= sizeof(parent))) - { - *errmsg = safe_asprintf("Parent path exceeds PATH_MAX"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Parent path exceeds PATH_MAX"); // Load dir-id from db for parent-path dir_id = db_directory_id_byvirtualpath(parent); if (dir_id == 0) - { - *errmsg = safe_asprintf("Directory info not found for virtual-path '%s'", parent); - return ACK_ERROR_NO_EXIST; - } + RETURN_ERROR(ACK_ERROR_NO_EXIST, "Directory info not found for virtual-path '%s'", parent); - return mpd_add_directory(evbuf, dir_id, 1, 0, errmsg); + return mpd_add_directory(out, dir_id, 1, 0); } -static enum mpd_ack_error -mpd_command_listallinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_listallinfo(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int dir_id; char parent[PATH_MAX]; int ret; - if (argc < 2 || strlen(argv[1]) == 0 - || (strncmp(argv[1], "/", 1) == 0 && strlen(argv[1]) == 1)) + if (in->argc < 2 || strlen(in->argv[1]) == 0 + || (strncmp(in->argv[1], "/", 1) == 0 && strlen(in->argv[1]) == 1)) { ret = snprintf(parent, sizeof(parent), "/"); } - else if (strncmp(argv[1], "/", 1) == 0) + else if (strncmp(in->argv[1], "/", 1) == 0) { - ret = snprintf(parent, sizeof(parent), "%s/", argv[1]); + ret = snprintf(parent, sizeof(parent), "%s/", in->argv[1]); } else { - ret = snprintf(parent, sizeof(parent), "/%s", argv[1]); + ret = snprintf(parent, sizeof(parent), "/%s", in->argv[1]); } if ((ret < 0) || (ret >= sizeof(parent))) - { - *errmsg = safe_asprintf("Parent path exceeds PATH_MAX"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Parent path exceeds PATH_MAX"); // Load dir-id from db for parent-path dir_id = db_directory_id_byvirtualpath(parent); if (dir_id == 0) - { - *errmsg = safe_asprintf("Directory info not found for virtual-path '%s'", parent); - return ACK_ERROR_NO_EXIST; - } + RETURN_ERROR(ACK_ERROR_NO_EXIST, "Directory info not found for virtual-path '%s'", parent); - return mpd_add_directory(evbuf, dir_id, 1, 1, errmsg); + return mpd_add_directory(out, dir_id, 1, 1); } /* * Command handler function for 'lsinfo' * Lists the contents of the directory given in argv[1]. */ -static enum mpd_ack_error -mpd_command_lsinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_lsinfo(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int dir_id; char parent[PATH_MAX]; int print_playlists; int ret; - enum mpd_ack_error ack_error; - if (argc < 2 || strlen(argv[1]) == 0 - || (strncmp(argv[1], "/", 1) == 0 && strlen(argv[1]) == 1)) + if (in->argc < 2 || strlen(in->argv[1]) == 0 + || (strncmp(in->argv[1], "/", 1) == 0 && strlen(in->argv[1]) == 1)) { ret = snprintf(parent, sizeof(parent), "/"); } - else if (strncmp(argv[1], "/", 1) == 0) + else if (strncmp(in->argv[1], "/", 1) == 0) { - ret = snprintf(parent, sizeof(parent), "%s/", argv[1]); + ret = snprintf(parent, sizeof(parent), "%s/", in->argv[1]); } else { - ret = snprintf(parent, sizeof(parent), "/%s", argv[1]); + ret = snprintf(parent, sizeof(parent), "/%s", in->argv[1]); } if ((ret < 0) || (ret >= sizeof(parent))) - { - *errmsg = safe_asprintf("Parent path exceeds PATH_MAX"); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Parent path exceeds PATH_MAX"); print_playlists = 0; if ((strncmp(parent, "/", 1) == 0 && strlen(parent) == 1)) @@ -3171,20 +2783,17 @@ mpd_command_lsinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, // Load dir-id from db for parent-path dir_id = db_directory_id_byvirtualpath(parent); if (dir_id == 0) - { - *errmsg = safe_asprintf("Directory info not found for virtual-path '%s'", parent); - return ACK_ERROR_NO_EXIST; - } + RETURN_ERROR(ACK_ERROR_NO_EXIST, "Directory info not found for virtual-path '%s'", parent); - ack_error = mpd_add_directory(evbuf, dir_id, 0, 1, errmsg); + ret = mpd_add_directory(out, dir_id, 0, 1); // If the root directory was passed as argument add the stored playlists to the response - if (ack_error == ACK_ERROR_NONE && print_playlists) + if (ret == 0 && print_playlists) { - return mpd_command_listplaylists(evbuf, argc, argv, errmsg, ctx); + return mpd_command_listplaylists(out, in, ctx); } - return ack_error; + return ret; } /* @@ -3193,282 +2802,159 @@ mpd_command_lsinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, * This command should list all files including files that are not part of the library. We do not support this * and only report files in the library. */ -static enum mpd_ack_error -mpd_command_listfiles(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_listfiles(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - return mpd_command_lsinfo(evbuf, argc, argv, errmsg, ctx); -} - -/* - * 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). - * - * TYPE can also be one of the special parameter: - * - any: checks all tags - * - file: checks the virtual_path - * - base: restricts result to the given directory - * - modified-since (not supported) - * - window: limits result to the given range of "START:END" - * - * Example request: "search artist foo album bar" - */ -static enum mpd_ack_error -mpd_command_search(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) -{ - struct query_params qp; - struct db_media_file_info dbmfi; - int ret; - - if (argc < 3 || ((argc - 1) % 2) != 0) - { - *errmsg = safe_asprintf("Missing argument(s) for command 'search'"); - return ACK_ERROR_ARG; - } - - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_ITEMS; - qp.sort = S_NAME; - qp.idx_type = I_NONE; - - parse_filter_window_params(argc - 1, argv + 1, false, &qp); - - ret = db_query_start(&qp); - if (ret < 0) - { - 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) - { - ret = mpd_add_db_media_file_info(evbuf, &dbmfi); - if (ret < 0) - { - DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); - } - } - - db_query_end(&qp); - free(qp.filter); - - return ACK_ERROR_NONE; -} - -static enum mpd_ack_error -mpd_command_searchadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) -{ - struct query_params qp; - struct player_status status; - int ret; - - if (argc < 3 || ((argc - 1) % 2) != 0) - { - *errmsg = safe_asprintf("Missing argument(s) for command 'search'"); - return ACK_ERROR_ARG; - } - - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_ITEMS; - qp.sort = S_ARTIST; - qp.idx_type = I_NONE; - - parse_filter_window_params(argc - 1, argv + 1, false, &qp); - - player_get_status(&status); - - ret = db_queue_add_by_query(&qp, status.shuffle, status.item_id, -1, NULL, NULL); - free(qp.filter); - if (ret < 0) - { - *errmsg = safe_asprintf("Failed to add songs to playlist"); - return ACK_ERROR_UNKNOWN; - } - - return ACK_ERROR_NONE; + return mpd_command_lsinfo(out, in, ctx); } /* * Command handler function for 'update' * Initiates an init-rescan (scans for new files) */ -static enum mpd_ack_error -mpd_command_update(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_update(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - if (argc > 1 && strlen(argv[1]) > 0) - { - *errmsg = safe_asprintf("Update for specific uri not supported for command 'update'"); - return ACK_ERROR_ARG; - } + if (in->argc > 1 && strlen(in->argv[1]) > 0) + RETURN_ERROR(ACK_ERROR_ARG, "Update for specific uri not supported for command 'update'"); library_rescan(0); - evbuffer_add(evbuf, "updating_db: 1\n", 15); + evbuffer_add(out->evbuf, "updating_db: 1\n", 15); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_sticker_get(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, const char *virtual_path) +/* + * sticker get song "/file:/path/to/song.wav" rating + */ +static int +mpd_sticker_get(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { - struct media_file_info *mfi = NULL; + struct media_file_info *mfi; uint32_t rating; - if (strcmp(argv[4], "rating") != 0) - { - *errmsg = safe_asprintf("no such sticker"); - return ACK_ERROR_NO_EXIST; - } + if (strcmp(in->argv[4], "rating") != 0) + RETURN_ERROR(ACK_ERROR_NO_EXIST, "No such sticker"); mfi = db_file_fetch_byvirtualpath(virtual_path); if (!mfi) - { - DPRINTF(E_LOG, L_MPD, "Virtual path not found: %s\n", virtual_path); - *errmsg = safe_asprintf("unknown sticker domain"); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Unknown sticker domain"); if (mfi->rating > 0) { rating = mfi->rating / MPD_RATING_FACTOR; - evbuffer_add_printf(evbuf, "sticker: rating=%d\n", rating); + evbuffer_add_printf(out->evbuf, "sticker: rating=%d\n", rating); } free_mfi(mfi, 0); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_sticker_set(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, const char *virtual_path) +/* + * sticker set song "/file:/path/to/song.wav" rating 10 + */ +static int +mpd_sticker_set(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { uint32_t rating; int id; int ret; - if (strcmp(argv[4], "rating") != 0) - { - *errmsg = safe_asprintf("no such sticker"); - return ACK_ERROR_NO_EXIST; - } + if (strcmp(in->argv[4], "rating") != 0) + RETURN_ERROR(ACK_ERROR_NO_EXIST, "No such sticker"); - ret = safe_atou32(argv[5], &rating); + ret = safe_atou32(in->argv[5], &rating); if (ret < 0) - { - *errmsg = safe_asprintf("rating '%s' doesn't convert to integer", argv[5]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Rating '%s' doesn't convert to integer", in->argv[5]); rating *= MPD_RATING_FACTOR; if (rating > DB_FILES_RATING_MAX) - { - *errmsg = safe_asprintf("rating '%s' is greater than maximum value allowed", argv[5]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Rating '%s' is greater than maximum value allowed", in->argv[5]); id = db_file_id_byvirtualpath(virtual_path); if (id <= 0) - { - *errmsg = safe_asprintf("Invalid path '%s'", virtual_path); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Invalid path '%s'", virtual_path); library_item_attrib_save(id, LIBRARY_ATTRIB_RATING, rating); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_sticker_delete(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, const char *virtual_path) +static int +mpd_sticker_delete(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { int id; - if (strcmp(argv[4], "rating") != 0) - { - *errmsg = safe_asprintf("no such sticker"); - return ACK_ERROR_NO_EXIST; - } + if (strcmp(in->argv[4], "rating") != 0) + RETURN_ERROR(ACK_ERROR_NO_EXIST, "No such sticker"); id = db_file_id_byvirtualpath(virtual_path); if (id <= 0) - { - *errmsg = safe_asprintf("Invalid path '%s'", virtual_path); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Invalid path '%s'", virtual_path); library_item_attrib_save(id, LIBRARY_ATTRIB_RATING, 0); - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_sticker_list(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, const char *virtual_path) +/* + * sticker list song "/file:/path/to/song.wav" + */ +static int +mpd_sticker_list(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { - struct media_file_info *mfi = NULL; + struct media_file_info *mfi; uint32_t rating; mfi = db_file_fetch_byvirtualpath(virtual_path); if (!mfi) - { - DPRINTF(E_LOG, L_MPD, "Virtual path not found: %s\n", virtual_path); - *errmsg = safe_asprintf("unknown sticker domain"); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Unknown sticker domain"); if (mfi->rating > 0) { rating = mfi->rating / MPD_RATING_FACTOR; - evbuffer_add_printf(evbuf, "sticker: rating=%d\n", rating); + evbuffer_add_printf(out->evbuf, "sticker: rating=%d\n", rating); } free_mfi(mfi, 0); /* |:todo:| real sticker implementation */ - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_sticker_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, const char *virtual_path) +/* + * sticker find {TYPE} {URI} {NAME} [sort {SORTTYPE}] [window {START:END}] + * sticker find {TYPE} {URI} {NAME} = {VALUE} [sort {SORTTYPE}] [window {START:END}] + * + * Example: + * sticker find song "/file:/path" rating = 10 + */ +static int +mpd_sticker_find(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path) { - struct query_params qp; + struct query_params qp = { .type = Q_ITEMS, .idx_type = I_NONE, .sort = S_VPATH }; struct db_media_file_info dbmfi; uint32_t rating = 0; uint32_t rating_arg = 0; const char *operator; int ret = 0; - if (strcmp(argv[4], "rating") != 0) - { - *errmsg = safe_asprintf("no such sticker"); - return ACK_ERROR_NO_EXIST; - } + if (strcmp(in->argv[4], "rating") != 0) + RETURN_ERROR(ACK_ERROR_NO_EXIST, "No such sticker"); - if (argc == 6) + if (in->argc > 6) { - *errmsg = safe_asprintf("not enough arguments for 'sticker find'"); - return ACK_ERROR_ARG; - } + if (strcmp(in->argv[5], "=") != 0 && strcmp(in->argv[5], ">") != 0 && strcmp(in->argv[5], "<") != 0) + RETURN_ERROR(ACK_ERROR_ARG, "Invalid operator '%s' given to 'sticker find'", in->argv[5]); - if (argc > 6) - { - if (strcmp(argv[5], "=") != 0 && strcmp(argv[5], ">") != 0 && strcmp(argv[5], "<") != 0) - { - *errmsg = safe_asprintf("invalid operator '%s' given to 'sticker find'", argv[5]); - return ACK_ERROR_ARG; - } - operator = argv[5]; + operator = in->argv[5]; - ret = safe_atou32(argv[6], &rating_arg); + ret = safe_atou32(in->argv[6], &rating_arg); if (ret < 0) - { - *errmsg = safe_asprintf("rating '%s' doesn't convert to integer", argv[6]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Rating '%s' doesn't convert to integer", in->argv[6]); + rating_arg *= MPD_RATING_FACTOR; } else @@ -3477,27 +2963,13 @@ mpd_sticker_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, c rating_arg = 0; } - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_ITEMS; - qp.sort = S_VPATH; - qp.idx_type = I_NONE; - qp.filter = db_mprintf("(f.virtual_path LIKE '%s%%' AND f.rating > 0 AND f.rating %s %d)", virtual_path, operator, rating_arg); - if (!qp.filter) - { - *errmsg = safe_asprintf("Out of memory"); - return ACK_ERROR_UNKNOWN; - } ret = db_query_start(&qp); if (ret < 0) { - db_query_end(&qp); - free(qp.filter); - - *errmsg = safe_asprintf("Could not start query"); - return ACK_ERROR_UNKNOWN; + free_query_params(&qp, 1); + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not start query"); } while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0) @@ -3511,7 +2983,7 @@ mpd_sticker_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, c } rating /= MPD_RATING_FACTOR; - ret = evbuffer_add_printf(evbuf, + ret = evbuffer_add_printf(out->evbuf, "file: %s\n" "sticker: rating=%d\n", (dbmfi.virtual_path + 1), @@ -3521,13 +2993,13 @@ mpd_sticker_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, c } db_query_end(&qp); - free(qp.filter); - return ACK_ERROR_NONE; + free_query_params(&qp, 1); + return 0; } struct mpd_sticker_command { const char *cmd; - enum mpd_ack_error (*handler)(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, const char *virtual_path); + int (*handler)(struct mpd_command_output *out, struct mpd_command_input *in, const char *virtual_path); int need_args; }; @@ -3538,7 +3010,7 @@ static struct mpd_sticker_command mpd_sticker_handlers[] = { "set", mpd_sticker_set, 6 }, { "delete", mpd_sticker_delete, 5 }, { "list", mpd_sticker_list, 4 }, - { "find", mpd_sticker_find, 5 }, + { "find", mpd_sticker_find, 6 }, { NULL, NULL, 0 }, }; @@ -3559,129 +3031,72 @@ static struct mpd_sticker_command mpd_sticker_handlers[] = * sticker set song "file:/srv/music/VA/The Electro Swing Revolution Vol 3 1 - Hop, Hop, Hop/13 Mr. Hotcut - You Are.mp3" rating "6" * OK */ -static enum mpd_ack_error -mpd_command_sticker(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_sticker(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { struct mpd_sticker_command *cmd_param = NULL; // Quell compiler warning about uninitialized use of cmd_param - char *virtual_path = NULL; + char *virtual_path; + int ret; int i; - enum mpd_ack_error ack_error; - if (strcmp(argv[2], "song") != 0) - { - *errmsg = safe_asprintf("unknown sticker domain"); - return ACK_ERROR_ARG; - } + if (strcmp(in->argv[2], "song") != 0) + RETURN_ERROR(ACK_ERROR_ARG, "Unknown sticker domain"); for (i = 0; i < (sizeof(mpd_sticker_handlers) / sizeof(struct mpd_sticker_command)); ++i) { cmd_param = &mpd_sticker_handlers[i]; - if (cmd_param->cmd && strcmp(argv[1], cmd_param->cmd) == 0) + if (cmd_param->cmd && strcmp(in->argv[1], cmd_param->cmd) == 0) break; } + if (!cmd_param->cmd) - { - *errmsg = safe_asprintf("bad request"); - return ACK_ERROR_ARG; - } - if (argc < cmd_param->need_args) - { - *errmsg = safe_asprintf("not enough arguments"); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Bad request"); - virtual_path = prepend_slash(argv[3]); + if (in->argc < cmd_param->need_args) + RETURN_ERROR(ACK_ERROR_ARG, "Not enough arguments"); - ack_error = cmd_param->handler(evbuf, argc, argv, errmsg, virtual_path); + virtual_path = prepend_slash(in->argv[3]); + + ret = cmd_param->handler(out, in, virtual_path); free(virtual_path); - - return ack_error; + return ret; } -/* static int -mpd_command_rescan(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) -{ - int ret; - - if (argc > 1) - { - DPRINTF(E_LOG, L_MPD, "Rescan for specific uri not supported for command 'rescan'\n"); - *errmsg = safe_asprintf("Rescan for specific uri not supported for command 'rescan'"); - return ACK_ERROR_ARG; - } - - filescanner_trigger_fullrescan(); - - evbuffer_add(evbuf, "updating_db: 1\n", 15); - - return 0; -} -*/ - -static enum mpd_ack_error -mpd_command_close(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +mpd_command_close(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { ctx->must_disconnect = true; - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_password(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_password(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - char *required_password; - char *supplied_password = ""; - int unrequired; - - if (argc > 1) - { - supplied_password = argv[1]; - } + const char *required_password; + const char *supplied_password = (in->argc > 1) ? in->argv[1] : ""; + bool password_is_required; required_password = cfg_getstr(cfg_getsec(cfg, "library"), "password"); - unrequired = !required_password || required_password[0] == '\0'; + password_is_required = required_password && required_password[0] != '\0'; + if (password_is_required && strcmp(supplied_password, required_password) != 0) + RETURN_ERROR(ACK_ERROR_PASSWORD, "Wrong password. Authentication failed."); - if (unrequired || strcmp(supplied_password, required_password) == 0) - { - DPRINTF(E_DBG, L_MPD, - "Authentication succeeded with supplied password: %s%s\n", - supplied_password, - unrequired ? " although no password is required" : ""); - ctx->authenticated = true; - return ACK_ERROR_NONE; - } - - DPRINTF(E_LOG, L_MPD, - "Authentication failed with supplied password: %s" - " for required password: %s\n", - supplied_password, required_password); - *errmsg = safe_asprintf("Wrong password. Authentication failed."); - return ACK_ERROR_PASSWORD; + ctx->authenticated = true; + return 0; } -static enum mpd_ack_error -mpd_command_binarylimit(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_binarylimit(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - unsigned int size; - - if (safe_atou32(argv[1], &size) < 0) - { - DPRINTF(E_DBG, L_MPD, - "Argument %s to binarylimit is not a number\n", - argv[1]); - return ACK_ERROR_ARG; - } + uint32_t size = in->argv_u32val[1]; if (size < MPD_BINARY_SIZE_MIN) - { - *errmsg = safe_asprintf("Value too small"); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Value too small"); ctx->binarylimit = size; - return ACK_ERROR_NONE; + return 0; } /* @@ -3715,23 +3130,13 @@ output_get_cb(struct player_speaker_info *spk, void *arg) * Command handler function for 'disableoutput' * Expects argument argv[1] to be the id of the speaker to disable. */ -static enum mpd_ack_error -mpd_command_disableoutput(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_disableoutput(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct output_get_param param; - uint32_t num; + uint32_t num = in->argv_u32val[1]; + struct output_get_param param = { .shortid = num }; int ret; - ret = safe_atou32(argv[1], &num); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - - memset(¶m, 0, sizeof(struct output_get_param)); - param.shortid = num; - player_speaker_enumerate(output_get_cb, ¶m); if (param.output && param.output->selected) @@ -3740,36 +3145,23 @@ mpd_command_disableoutput(struct evbuffer *evbuf, int argc, char **argv, char ** free_output(param.output); if (ret < 0) - { - *errmsg = safe_asprintf("Speakers deactivation failed: %d", num); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Speakers deactivation failed: %d", num); } - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'enableoutput' * Expects argument argv[1] to be the id of the speaker to enable. */ -static enum mpd_ack_error -mpd_command_enableoutput(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_enableoutput(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct output_get_param param; - uint32_t num; + uint32_t num = in->argv_u32val[1]; + struct output_get_param param = { .shortid = num }; int ret; - ret = safe_atou32(argv[1], &num); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - - memset(¶m, 0, sizeof(struct output_get_param)); - param.shortid = num; - player_speaker_enumerate(output_get_cb, ¶m); if (param.output && !param.output->selected) @@ -3778,36 +3170,23 @@ mpd_command_enableoutput(struct evbuffer *evbuf, int argc, char **argv, char **e free_output(param.output); if (ret < 0) - { - *errmsg = safe_asprintf("Speakers deactivation failed: %d", num); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Speakers deactivation failed: %d", num); } - return ACK_ERROR_NONE; + return 0; } /* * Command handler function for 'toggleoutput' * Expects argument argv[1] to be the id of the speaker to enable/disable. */ -static enum mpd_ack_error -mpd_command_toggleoutput(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_toggleoutput(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct output_get_param param; - uint32_t num; + uint32_t num = in->argv_u32val[1]; + struct output_get_param param = { .shortid = num }; int ret; - ret = safe_atou32(argv[1], &num); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } - - memset(¶m, 0, sizeof(struct output_get_param)); - param.shortid = num; - player_speaker_enumerate(output_get_cb, ¶m); if (param.output) @@ -3820,13 +3199,10 @@ mpd_command_toggleoutput(struct evbuffer *evbuf, int argc, char **argv, char **e free_output(param.output); if (ret < 0) - { - *errmsg = safe_asprintf("Toggle speaker failed: %d", num); - return ACK_ERROR_UNKNOWN; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Toggle speaker failed: %d", num); } - return ACK_ERROR_NONE; + return 0; } /* @@ -3878,18 +3254,17 @@ speaker_enum_cb(struct player_speaker_info *spk, void *arg) * Command handler function for 'output' * Returns a lists with the avaiable speakers. */ -static enum mpd_ack_error -mpd_command_outputs(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_outputs(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - struct output_outputs_param param; + struct output_outputs_param param = { 0 }; /* Reference: * https://mpd.readthedocs.io/en/latest/protocol.html#audio-output-devices * the ID returned by mpd may change between excutions, so what we do * is simply enumerate the speakers, and for get/set commands we count * ID times to the output referenced. */ - memset(¶m, 0, sizeof(param)); - param.buf = evbuf; + param.buf = out->evbuf; player_speaker_enumerate(speaker_enum_cb, ¶m); @@ -3897,7 +3272,7 @@ mpd_command_outputs(struct evbuffer *evbuf, int argc, char **argv, char **errmsg * element when configured to do so */ if (mpd_plugin_httpd) { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "outputid: %u\n" "outputname: MP3 stream\n" "plugin: httpd\n" @@ -3906,62 +3281,38 @@ mpd_command_outputs(struct evbuffer *evbuf, int argc, char **argv, char **errmsg param.nextid++; } - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -outputvolume_set(uint32_t shortid, int volume, char **errmsg) +static int +outputvolume_set(uint32_t shortid, int volume) { - struct output_get_param param; + struct output_get_param param = { .shortid = shortid }; int ret; - memset(¶m, 0, sizeof(struct output_get_param)); - param.shortid = shortid; - player_speaker_enumerate(output_get_cb, ¶m); - if (param.output) - { - ret = player_volume_setabs_speaker(param.output->id, volume); - free_output(param.output); + if (!param.output) + return -1; - if (ret < 0) - { - *errmsg = safe_asprintf("Setting volume to %d for speaker with short-id %d failed", volume, shortid); - return ACK_ERROR_UNKNOWN; - } - } - else - { - *errmsg = safe_asprintf("No speaker found for short id: %d", shortid); - return ACK_ERROR_UNKNOWN; - } + ret = player_volume_setabs_speaker(param.output->id, volume); - return ACK_ERROR_NONE; + free_output(param.output); + return ret; } -static enum mpd_ack_error -mpd_command_outputvolume(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_outputvolume(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - uint32_t shortid; - int volume; + uint32_t shortid = in->argv_u32val[1]; + int32_t volume = in->argv_i32val[2]; int ret; - ret = safe_atou32(argv[1], &shortid); + ret = outputvolume_set(shortid, volume); if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[1]); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_UNKNOWN, "Could not set volume for speaker with id: %d", shortid); - ret = safe_atoi32(argv[2], &volume); - if (ret < 0) - { - *errmsg = safe_asprintf("Argument doesn't convert to integer: '%s'", argv[2]); - return ACK_ERROR_ARG; - } - - return outputvolume_set(shortid, volume, errmsg); + return 0; } static void @@ -3971,7 +3322,6 @@ channel_outputvolume(const char *message) int volume; char *tmp; char *ptr; - char *errmsg = NULL; int ret; tmp = strdup(message); @@ -4001,9 +3351,9 @@ channel_outputvolume(const char *message) return; } - outputvolume_set(shortid, volume, &errmsg); - if (errmsg) - DPRINTF(E_LOG, L_MPD, "Failed to set output volume from message: '%s' (error='%s')\n", message, errmsg); + ret = outputvolume_set(shortid, volume); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Failed to set output volume from message: '%s'\n", message); free(tmp); } @@ -4064,74 +3414,77 @@ mpd_find_channel(const char *name) return NULL; } -static enum mpd_ack_error -mpd_command_channels(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_channels(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int i; for (i = 0; mpd_channels[i].handler; i++) { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "channel: %s\n", mpd_channels[i].channel); } - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_sendmessage(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_sendmessage(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { const char *channelname; const char *message; struct mpd_channel *channel; - if (argc < 3) - { - *errmsg = safe_asprintf("Missing argument for command 'sendmessage'"); - return ACK_ERROR_ARG; - } - - channelname = argv[1]; - message = argv[2]; + channelname = in->argv[1]; + message = in->argv[2]; channel = mpd_find_channel(channelname); if (!channel) { // Just ignore the message, only log an error message DPRINTF(E_LOG, L_MPD, "Unsupported channel '%s'\n", channelname); - return ACK_ERROR_NONE; + return 0; } channel->handler(message); - return ACK_ERROR_NONE; + return 0; } /* * Dummy function to handle commands that are not supported and should * not raise an error. */ -static enum mpd_ack_error -mpd_command_ignore(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_ignore(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { //do nothing - DPRINTF(E_DBG, L_MPD, "Ignore command %s\n", argv[0]); - return ACK_ERROR_NONE; + DPRINTF(E_DBG, L_MPD, "Ignore command %s\n", in->argv[0]); + return 0; } -static enum mpd_ack_error -mpd_command_commands(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_commands(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int i; for (i = 0; mpd_handlers[i].handler; i++) { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "command: %s\n", - mpd_handlers[i].mpdcommand); + mpd_handlers[i].name); } - return ACK_ERROR_NONE; + return 0; +} + +static void +tagtypes_enum(struct mpd_tag_map *tag, void *arg) +{ + struct evbuffer *evbuf = arg; + + if (tag->type != MPD_TYPE_SPECIAL) + evbuffer_add_printf(evbuf, "tagtype: %s\n", tag->name); } /* @@ -4139,18 +3492,11 @@ mpd_command_commands(struct evbuffer *evbuf, int argc, char **argv, char **errms * Returns a lists with supported tags in the form: * tagtype: Artist */ -static enum mpd_ack_error -mpd_command_tagtypes(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_tagtypes(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - 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 ACK_ERROR_NONE; + mpd_parser_enum_tagtypes(tagtypes_enum, out->evbuf); + return 0; } /* @@ -4158,10 +3504,10 @@ mpd_command_tagtypes(struct evbuffer *evbuf, int argc, char **argv, char **errms * Returns a lists with supported tags in the form: * handler: protocol:// */ -static enum mpd_ack_error -mpd_command_urlhandlers(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_urlhandlers(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { - evbuffer_add_printf(evbuf, + evbuffer_add_printf(out->evbuf, "handler: http://\n" // handlers supported by MPD 0.19.12 // "handler: https://\n" @@ -4181,7 +3527,7 @@ mpd_command_urlhandlers(struct evbuffer *evbuf, int argc, char **argv, char **er // "handler: alsa://\n" ); - return ACK_ERROR_NONE; + return 0; } /* @@ -4191,60 +3537,56 @@ mpd_command_urlhandlers(struct evbuffer *evbuf, int argc, char **argv, char **er * The server only uses libav/ffmepg for decoding and does not support decoder plugins, * therefor the function reports only ffmpeg as available. */ -static enum mpd_ack_error -mpd_command_decoders(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_decoders(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { int i; - evbuffer_add_printf(evbuf, "plugin: ffmpeg\n"); + evbuffer_add_printf(out->evbuf, "plugin: ffmpeg\n"); for (i = 0; ffmpeg_suffixes[i]; i++) { - evbuffer_add_printf(evbuf, "suffix: %s\n", ffmpeg_suffixes[i]); + evbuffer_add_printf(out->evbuf, "suffix: %s\n", ffmpeg_suffixes[i]); } for (i = 0; ffmpeg_mime_types[i]; i++) { - evbuffer_add_printf(evbuf, "mime_type: %s\n", ffmpeg_mime_types[i]); + evbuffer_add_printf(out->evbuf, "mime_type: %s\n", ffmpeg_mime_types[i]); } - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_command_list_begin(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_command_list_begin(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { ctx->cmd_list_type = COMMAND_LIST_BEGIN; - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_command_list_ok_begin(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_command_list_ok_begin(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { ctx->cmd_list_type = COMMAND_LIST_OK_BEGIN; - return ACK_ERROR_NONE; + return 0; } -static enum mpd_ack_error -mpd_command_command_list_end(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, struct mpd_client_ctx *ctx) +static int +mpd_command_command_list_end(struct mpd_command_output *out, struct mpd_command_input *in, struct mpd_client_ctx *ctx) { if (ctx->cmd_list_type == COMMAND_LIST_BEGIN) ctx->cmd_list_type = COMMAND_LIST_END; else if (ctx->cmd_list_type == COMMAND_LIST_OK_BEGIN) ctx->cmd_list_type = COMMAND_LIST_OK_END; else - { - *errmsg = safe_asprintf("Got with command list end without preceeding list start"); - return ACK_ERROR_ARG; - } + RETURN_ERROR(ACK_ERROR_ARG, "Got with command list end without preceeding list start"); - return ACK_ERROR_NONE; + return 0; } - static struct mpd_command mpd_handlers[] = { - /* commandname | handler function | minimum argument count*/ + /* commandname | handler function | min arg count | handler requires int args */ // Commands for querying status { "clearerror", mpd_command_ignore, -1 }, @@ -4255,26 +3597,26 @@ static struct mpd_command mpd_handlers[] = { "stats", mpd_command_stats, -1 }, // Playback options - { "consume", mpd_command_consume, 2 }, + { "consume", mpd_command_consume, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "crossfade", mpd_command_ignore, -1 }, { "mixrampdb", mpd_command_ignore, -1 }, { "mixrampdelay", mpd_command_ignore, -1 }, - { "random", mpd_command_random, 2 }, - { "repeat", mpd_command_repeat, 2 }, - { "setvol", mpd_command_setvol, 2 }, + { "random", mpd_command_random, 2, MPD_WANTS_NUM_ARG1_UVAL }, + { "repeat", mpd_command_repeat, 2, MPD_WANTS_NUM_ARG1_UVAL }, + { "setvol", mpd_command_setvol, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "single", mpd_command_single, 2 }, { "replay_gain_mode", mpd_command_ignore, -1 }, { "replay_gain_status", mpd_command_replay_gain_status, -1 }, - { "volume", mpd_command_volume, 2 }, + { "volume", mpd_command_volume, 2, MPD_WANTS_NUM_ARG1_IVAL }, // Controlling playback { "next", mpd_command_next, -1 }, - { "pause", mpd_command_pause, -1 }, - { "play", mpd_command_play, -1 }, - { "playid", mpd_command_playid, -1 }, + { "pause", mpd_command_pause, 1, MPD_WANTS_NUM_ARG1_UVAL }, + { "play", mpd_command_play, 1, MPD_WANTS_NUM_ARG1_UVAL }, + { "playid", mpd_command_playid, 1, MPD_WANTS_NUM_ARG1_UVAL }, { "previous", mpd_command_previous, -1 }, - { "seek", mpd_command_seek, 3 }, - { "seekid", mpd_command_seekid, 3 }, + { "seek", mpd_command_seek, 3, MPD_WANTS_NUM_ARG1_UVAL }, + { "seekid", mpd_command_seekid, 3, MPD_WANTS_NUM_ARG1_UVAL }, { "seekcur", mpd_command_seekcur, 2 }, { "stop", mpd_command_stop, -1 }, @@ -4283,16 +3625,16 @@ static struct mpd_command mpd_handlers[] = { "addid", mpd_command_addid, 2 }, { "clear", mpd_command_clear, -1 }, { "delete", mpd_command_delete, -1 }, - { "deleteid", mpd_command_deleteid, 2 }, + { "deleteid", mpd_command_deleteid, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "move", mpd_command_move, 3 }, - { "moveid", mpd_command_moveid, 3 }, + { "moveid", mpd_command_moveid, 3, MPD_WANTS_NUM_ARG1_UVAL }, { "playlist", mpd_command_playlistinfo, -1 }, // According to the mpd protocol the use of "playlist" is deprecated - { "playlistfind", mpd_command_playlistfind, -1 }, - { "playlistid", mpd_command_playlistid, -1 }, + { "playlistfind", mpd_command_playlistfind, 2 }, + { "playlistid", mpd_command_playlistid, 1, MPD_WANTS_NUM_ARG1_UVAL }, { "playlistinfo", mpd_command_playlistinfo, -1 }, - { "playlistsearch", mpd_command_playlistsearch, -1 }, - { "plchanges", mpd_command_plchanges, 2 }, - { "plchangesposid", mpd_command_plchangesposid, 2 }, + { "playlistsearch", mpd_command_playlistsearch, 2 }, + { "plchanges", mpd_command_plchanges, 2, MPD_WANTS_NUM_ARG1_UVAL }, + { "plchangesposid", mpd_command_plchangesposid, 2, MPD_WANTS_NUM_ARG1_UVAL }, // { "prio", mpd_command_prio, -1 }, // { "prioid", mpd_command_prioid, -1 }, // { "rangeid", mpd_command_rangeid, -1 }, @@ -4317,16 +3659,16 @@ static struct mpd_command mpd_handlers[] = // The music database { "count", mpd_command_count, -1 }, - { "find", mpd_command_find, -1 }, - { "findadd", mpd_command_findadd, -1 }, - { "list", mpd_command_list, -1 }, + { "find", mpd_command_find, 2 }, + { "findadd", mpd_command_findadd, 2 }, + { "search", mpd_command_find, 2 }, + { "searchadd", mpd_command_findadd, 2 }, + { "list", mpd_command_list, 2 }, { "listall", mpd_command_listall, -1 }, { "listallinfo", mpd_command_listallinfo, -1 }, { "listfiles", mpd_command_listfiles, -1 }, { "lsinfo", mpd_command_lsinfo, -1 }, // { "readcomments", mpd_command_readcomments, -1 }, - { "search", mpd_command_search, -1 }, - { "searchadd", mpd_command_searchadd, -1 }, // { "searchaddpl", mpd_command_searchaddpl, -1 }, { "update", mpd_command_update, -1 }, // { "rescan", mpd_command_rescan, -1 }, @@ -4345,24 +3687,23 @@ static struct mpd_command mpd_handlers[] = // { "kill", mpd_command_kill, -1 }, { "password", mpd_command_password, -1 }, { "ping", mpd_command_ignore, -1 }, - { "binarylimit", mpd_command_binarylimit, 2 }, - /* missing: tagtypes */ + { "binarylimit", mpd_command_binarylimit, 2, MPD_WANTS_NUM_ARG1_UVAL }, // Audio output devices - { "disableoutput", mpd_command_disableoutput, 2 }, - { "enableoutput", mpd_command_enableoutput, 2 }, - { "toggleoutput", mpd_command_toggleoutput, 2 }, + { "disableoutput", mpd_command_disableoutput, 2, MPD_WANTS_NUM_ARG1_UVAL }, + { "enableoutput", mpd_command_enableoutput, 2, MPD_WANTS_NUM_ARG1_UVAL }, + { "toggleoutput", mpd_command_toggleoutput, 2, MPD_WANTS_NUM_ARG1_UVAL }, { "outputs", mpd_command_outputs, -1 }, // Custom command outputvolume (not supported by mpd) - { "outputvolume", mpd_command_outputvolume, 3 }, + { "outputvolume", mpd_command_outputvolume, 3, MPD_WANTS_NUM_ARG1_UVAL | MPD_WANTS_NUM_ARG2_IVAL }, // Client to client { "subscribe", mpd_command_ignore, -1 }, { "unsubscribe", mpd_command_ignore, -1 }, { "channels", mpd_command_channels, -1 }, { "readmessages", mpd_command_ignore, -1 }, - { "sendmessage", mpd_command_sendmessage, -1 }, + { "sendmessage", mpd_command_sendmessage, 3 }, // Reflection // { "config", mpd_command_config, -1 }, @@ -4394,7 +3735,7 @@ mpd_find_command(const char *name) for (i = 0; mpd_handlers[i].handler; i++) { - if (0 == strcmp(name, mpd_handlers[i].mpdcommand)) + if (0 == strcmp(name, mpd_handlers[i].name)) { return &mpd_handlers[i]; } @@ -4403,170 +3744,173 @@ mpd_find_command(const char *name) return NULL; } -// Parses the argument string into an array of strings. Arguments are separated -// by a whitespace character and may be wrapped in double quotes. ack_error is -// set and errmsg allocated if there is an error. -static enum mpd_ack_error -mpd_parse_args(char **argv, int argv_size, int *argc, char *args, bool *must_disconnect, char **errmsg) +static void +mpd_command_input_free(struct mpd_command_input *input) { - char *input = args; - int arg_count = 0; + if (!input) + return; - DPRINTF(E_SPAM, L_MPD, "Parse args: args = \"%s\"\n", input); + free(input->args_split); + free(input); +} - while (*input != 0 && arg_count < argv_size) +static int +mpd_command_input_create(struct mpd_command_input **out, const char *line) +{ + struct mpd_command_input *in; + int ret; + int i; + + CHECK_NULL(L_MPD, in = calloc(1, sizeof(struct mpd_command_input))); + + in->args_raw = line; + + ret = mpd_split_args(in->argv, sizeof(in->argv), &in->argc, &in->args_split, line); + if (ret < 0) + goto error; + + // Many of the handlers need numeric input. If you change this, then also + // review command_has_num(). + for (i = MPD_WANTS_NUM_ARGV_MIN; i < in->argc && i <= MPD_WANTS_NUM_ARGV_MAX; i++) { - // Ignore whitespace characters - if (*input == ' ') - { - input++; - continue; - } - - // Check if the parameter is wrapped in double quotes - if (*input == '"') - { - argv[arg_count] = mpd_pars_quoted(&input); - if (!argv[arg_count]) - goto error; - } - else - { - argv[arg_count] = mpd_pars_unquoted(&input); - } - - arg_count++; + if (!isdigit(in->argv[i][0]) && in->argv[i][0] != '-') + continue; // Save some cycles if clearly not a number + if (safe_atoi32(in->argv[i], &in->argv_i32val[i]) == 0) + in->has_num |= 1 << i; + if (safe_atou32(in->argv[i], &in->argv_u32val[i]) == 0) + in->has_num |= 1 << (i + MPD_WANTS_NUM_ARGV_MAX); } - - DPRINTF(E_SPAM, L_MPD, "Parse args: args count = \"%d\"\n", arg_count); - *argc = arg_count; - - if (arg_count == 0) - { - *errmsg = safe_asprintf("No command given"); - *must_disconnect = true; // in this case MPD disconnects the client - return ACK_ERROR_ARG; - } - - if (*input != 0 && arg_count == argv_size) - { - *errmsg = safe_asprintf("Too many arguments: %d allowed", argv_size); - return ACK_ERROR_ARG; // in this case MPD doesn't disconnect the client - } - - return ACK_ERROR_NONE; + + *out = in; + return 0; error: - *errmsg = safe_asprintf("Error parsing arguments"); - *must_disconnect = true; // in this case MPD disconnects the client - return ACK_ERROR_UNKNOWN; + mpd_command_input_free(in); + return -1; } -static enum mpd_ack_error -mpd_list_add(struct mpd_client_ctx *client_ctx, const char *line) +// Check if input has the numeric arguments required for the command, taking +// into account that some commands have optional numeric args (purpose of mask) +static bool +command_has_num(int wants_num, int has_num, int argc) { - size_t sz = strlen(line) + 1; - int ret; + int ival_mask = (1 << argc) - 1; // If argc == 2 becomes ...00000011 + int uval_mask = (ival_mask << MPD_WANTS_NUM_ARGV_MAX); // If ..MAX == 3 becomes 00011000 + int mask = (ival_mask | uval_mask); // becomes 00011011 - if (evbuffer_get_length(client_ctx->cmd_list_buffer) + sz > MPD_MAX_COMMAND_LIST_SIZE) - { - DPRINTF(E_LOG, L_MPD, "Max command list size (%uKB) exceeded\n", (MPD_MAX_COMMAND_LIST_SIZE / 1024)); - client_ctx->must_disconnect = true; - return ACK_ERROR_NONE; - } - - ret = evbuffer_add(client_ctx->cmd_list_buffer, line, sz); - if (ret < 0) - { - DPRINTF(E_LOG, L_MPD, "Failed to add to command list\n"); - client_ctx->must_disconnect = true; - return ACK_ERROR_NONE; - } - - return ACK_ERROR_NONE; + return (wants_num & mask) == (has_num & wants_num & mask); } -static enum mpd_ack_error -mpd_process_command_line(struct evbuffer *output, char *line, int cmd_num, struct mpd_client_ctx *client_ctx) +static bool +mpd_must_process_command_now(const char *line, struct mpd_client_ctx *client_ctx) { - enum mpd_ack_error ack_error; - bool got_noidle; - char *argv[MPD_COMMAND_ARGV_MAX] = { 0 }; // Zero init just to silence false positive from scan-build - int argc = 0; // Also to silence scan-build - char *cmd_name = NULL; - struct mpd_command *command; - char *errmsg = NULL; + size_t line_len = strlen(line); // We're in command list mode, just add command to buffer and return if ((client_ctx->cmd_list_type == COMMAND_LIST_BEGIN || client_ctx->cmd_list_type == COMMAND_LIST_OK_BEGIN) && strcmp(line, "command_list_end") != 0) { - return mpd_list_add(client_ctx, line); + if (evbuffer_get_length(client_ctx->cmd_list_buffer) + line_len + 1 > MPD_MAX_COMMAND_LIST_SIZE) + { + DPRINTF(E_LOG, L_MPD, "Max command list size (%uKB) exceeded\n", (MPD_MAX_COMMAND_LIST_SIZE / 1024)); + client_ctx->must_disconnect = true; + return false; + } + + evbuffer_add(client_ctx->cmd_list_buffer, line, line_len + 1); + return false; } - got_noidle = (strcmp(line, "noidle") == 0); - if (got_noidle && !client_ctx->is_idle) + if (strcmp(line, "noidle") == 0 && !client_ctx->is_idle) { - return ACK_ERROR_NONE; // Just ignore, don't proceed to send an OK + return false; // Just ignore, don't proceed to send an OK } - else if (!got_noidle && client_ctx->is_idle) + + return true; +} + +static enum mpd_ack_error +mpd_process_command_line(struct evbuffer *evbuf, const char *line, int cmd_num, struct mpd_client_ctx *client_ctx) +{ + struct mpd_command_input *in = NULL; + struct mpd_command_output out = { .evbuf = evbuf, .ack_error = ACK_ERROR_NONE }; + struct mpd_command *command; + const char *cmd_name = NULL; + int ret; + + if (!mpd_must_process_command_now(line, client_ctx)) + { + return ACK_ERROR_NONE; + } + + ret = mpd_command_input_create(&in, line); + if (ret < 0) + { + client_ctx->must_disconnect = true; // This is what MPD does + out.errmsg = safe_asprintf("Could not read command: '%s'", line); + out.ack_error = ACK_ERROR_ARG; + goto error; + } + + cmd_name = in->argv[0]; + + if (strcmp(cmd_name, "noidle") != 0 && client_ctx->is_idle) { - errmsg = safe_asprintf("Only 'noidle' is allowed during idle"); - ack_error = ACK_ERROR_ARG; client_ctx->must_disconnect = true; + out.errmsg = safe_asprintf("Only 'noidle' is allowed during idle"); + out.ack_error = ACK_ERROR_ARG; goto error; } - // Split the read line into command name and arguments - ack_error = mpd_parse_args(argv, MPD_COMMAND_ARGV_MAX, &argc, line, &client_ctx->must_disconnect, &errmsg); - if (ack_error != ACK_ERROR_NONE) - { - goto error; - } - - CHECK_NULL(L_MPD, argv[0]); - - cmd_name = argv[0]; if (strcmp(cmd_name, "password") != 0 && !client_ctx->authenticated) { - errmsg = safe_asprintf("Not authenticated"); - ack_error = ACK_ERROR_PERMISSION; + out.errmsg = safe_asprintf("Not authenticated"); + out.ack_error = ACK_ERROR_PERMISSION; goto error; } command = mpd_find_command(cmd_name); if (!command) { - errmsg = safe_asprintf("Unknown command"); - ack_error = ACK_ERROR_UNKNOWN; + out.errmsg = safe_asprintf("Unknown command"); + out.ack_error = ACK_ERROR_UNKNOWN; goto error; } - else if (command->min_argc > argc) + else if (command->min_argc > in->argc) { - errmsg = safe_asprintf("Missing argument(s), expected %d, given %d", command->min_argc, argc); - ack_error = ACK_ERROR_ARG; + out.errmsg = safe_asprintf("Missing argument(s), expected %d, given %d", command->min_argc - 1, in->argc - 1); + out.ack_error = ACK_ERROR_ARG; + goto error; + } + else if (!command_has_num(command->wants_num, in->has_num, in->argc)) + { + out.errmsg = safe_asprintf("Missing or invalid numeric values in command: '%s'", line); + out.ack_error = ACK_ERROR_ARG; goto error; } - ack_error = command->handler(output, argc, argv, &errmsg, client_ctx); - if (ack_error != ACK_ERROR_NONE) + ret = command->handler(&out, in, client_ctx); + if (ret < 0) { goto error; } if (client_ctx->cmd_list_type == COMMAND_LIST_NONE && !client_ctx->is_idle) - evbuffer_add_printf(output, "OK\n"); + evbuffer_add_printf(out.evbuf, "OK\n"); - return ack_error; + mpd_command_input_free(in); + + return out.ack_error; error: - DPRINTF(E_LOG, L_MPD, "Error processing command '%s': %s\n", line, errmsg); + DPRINTF(E_LOG, L_MPD, "Error processing command '%s': %s\n", line, out.errmsg); if (cmd_name) - evbuffer_add_printf(output, "ACK [%d@%d] {%s} %s\n", ack_error, cmd_num, cmd_name, errmsg); + evbuffer_add_printf(out.evbuf, "ACK [%d@%d] {%s} %s\n", out.ack_error, cmd_num, cmd_name, out.errmsg); - free(errmsg); + mpd_command_input_free(in); + free(out.errmsg); - return ack_error; + return out.ack_error; } // Process the commands that were added to client_ctx->cmd_list_buffer @@ -4578,7 +3922,7 @@ mpd_process_command_line(struct evbuffer *output, char *line, int cmd_num, struc // for each successful command executed in the command list. // On success for all commands, OK is returned. static void -mpd_process_command_list(struct evbuffer *output, struct mpd_client_ctx *client_ctx) +mpd_process_command_list(struct evbuffer *evbuf, struct mpd_client_ctx *client_ctx) { char *line; enum mpd_ack_error ack_error = ACK_ERROR_NONE; @@ -4586,7 +3930,8 @@ mpd_process_command_list(struct evbuffer *output, struct mpd_client_ctx *client_ while ((line = evbuffer_readln(client_ctx->cmd_list_buffer, NULL, EVBUFFER_EOL_NUL))) { - ack_error = mpd_process_command_line(output, line, cmd_num, client_ctx); + ack_error = mpd_process_command_line(evbuf, line, cmd_num, client_ctx); + cmd_num++; free(line); @@ -4595,11 +3940,11 @@ mpd_process_command_list(struct evbuffer *output, struct mpd_client_ctx *client_ break; if (client_ctx->cmd_list_type == COMMAND_LIST_OK_END) - evbuffer_add_printf(output, "list_OK\n"); + evbuffer_add_printf(evbuf, "list_OK\n"); } if (ack_error == ACK_ERROR_NONE) - evbuffer_add_printf(output, "OK\n"); + evbuffer_add_printf(evbuf, "OK\n"); // Back to single-command mode evbuffer_drain(client_ctx->cmd_list_buffer, -1); @@ -4791,9 +4136,7 @@ mpd_notify_idle(void *arg, int *retval) { DPRINTF(E_DBG, L_MPD, "Notify client #%d\n", i); - notify_idle_client(client, event_mask); - - evbuffer_add_printf(client->evbuffer, "OK\n"); + notify_idle_client(client, event_mask, true); client = client->next; i++; diff --git a/src/parsers/mpd_lexer.l b/src/parsers/mpd_lexer.l new file mode 100644 index 00000000..d6e60ebc --- /dev/null +++ b/src/parsers/mpd_lexer.l @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2021-2022 Espen Jürgensen + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* =========================== BOILERPLATE SECTION ===========================*/ + +/* This is to avoid compiler warnings about unused functions. More options are + noyyalloc noyyrealloc noyyfree. */ +%option noyywrap nounput noinput + +/* Thread safe scanner */ +%option reentrant + +/* To avoid symbol name conflicts with multiple lexers */ +%option prefix="mpd_" + +/* Automake's ylwrap expexts the output to have this name */ +%option outfile="lex.yy.c" + +/* Makes a Bison-compatible yylex */ +%option bison-bridge + +%{ +#include +#include +#include +#include +#include "mpd_parser.h" + +/* Unknown why this is required despite using prefix */ +#define YYSTYPE MPD_STYPE +%} + + +/* ========================= NON-BOILERPLATE SECTION =========================*/ + +%option case-insensitive + +singlequoted '(\\.|[^'\\])*' +doublequoted \"(\\.|[^"\\])*\" +yyyymmdd [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] + +%% + +[\n\r\t ]+ /* Ignore whitespace */ + +^playlistfind { return MPD_T_CMDPLAYLISTFIND; } +^playlistsearch { return MPD_T_CMDPLAYLISTSEARCH; } +^count { return MPD_T_CMDCOUNT; } +^find { return MPD_T_CMDFIND; } +^findadd { return MPD_T_CMDFINDADD; } +^list { return MPD_T_CMDLIST; } +^search { return MPD_T_CMDSEARCH; } +^searchadd { return MPD_T_CMDSEARCHADD; } +^searchcount { return MPD_T_CMDSEARCHCOUNT; } + +sort { return MPD_T_SORT; } +window { return MPD_T_WINDOW; } +position { return MPD_T_POSITION; } +group { return MPD_T_GROUP; } + +artist { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +artistsort { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +albumartist { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +albumartistsort { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +album { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +albumsort { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +title { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +titlesort { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +composer { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +composersort { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +genre { yylval->str = strdup(yytext); return MPD_T_STRTAG; } +file { yylval->str = strdup(yytext); return MPD_T_STRTAG; } + +base { yylval->str = strdup(yytext); return MPD_T_BASETAG; } + +track { yylval->str = strdup(yytext); return MPD_T_INTTAG; } +disc { yylval->str = strdup(yytext); return MPD_T_INTTAG; } +date { yylval->str = strdup(yytext); return MPD_T_INTTAG; } + +modified-since { yylval->str = strdup(yytext); return MPD_T_SINCETAG; } +added-since { yylval->str = strdup(yytext); return MPD_T_SINCETAG; } + +contains { return (yylval->ival = MPD_T_CONTAINS); } +starts_with { return (yylval->ival = MPD_T_STARTSWITH); } +ends_with { return (yylval->ival = MPD_T_ENDSWITH); } +== { return (yylval->ival = MPD_T_EQUAL); } +!= { return (yylval->ival = MPD_T_NOTEQUAL); } +\<= { return (yylval->ival = MPD_T_LESSEQUAL); } +\< { return (yylval->ival = MPD_T_LESS); } +\>= { return (yylval->ival = MPD_T_GREATEREQUAL); } +\> { return (yylval->ival = MPD_T_GREATER); } + +audioformat { return MPD_T_AUDIOFORMATTAG; } +any { return MPD_T_ANYTAG; } + +or { return MPD_T_OR; } +and { return MPD_T_AND; } +not { return MPD_T_NOT; } +! { return MPD_T_NOT; } + +{singlequoted} { yylval->str = mpd_parser_quoted(yytext); return MPD_T_STRING; } +{doublequoted} { yylval->str = mpd_parser_quoted(yytext); return MPD_T_STRING; } + +[0-9]+ { yylval->ival=atoi(yytext); return MPD_T_NUM; } + +. { return yytext[0]; } + +%% + diff --git a/src/parsers/mpd_parser.y b/src/parsers/mpd_parser.y new file mode 100644 index 00000000..74f8e8ee --- /dev/null +++ b/src/parsers/mpd_parser.y @@ -0,0 +1,806 @@ +/* + * Copyright (C) 2021-2022 Espen Jürgensen + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* =========================== BOILERPLATE SECTION ===========================*/ + +/* No global variables and yylex has scanner as argument */ +%define api.pure true + +/* Change prefix of symbols from yy to avoid clashes with any other parsers we + may want to link */ +%define api.prefix {mpd_} + +/* Gives better errors than "syntax error" */ +%define parse.error verbose + +/* Enables debug mode */ +%define parse.trace + +/* Adds output parameter to the parser */ +%parse-param {struct mpd_result *result} + +/* Adds "scanner" as argument to the parses calls to yylex, which is required + when the lexer is in reentrant mode. The type is void because caller caller + shouldn't need to know about yyscan_t */ +%param {void *scanner} + +%code provides { +/* Convenience functions for caller to use instead of interfacing with lexer and + parser directly */ +int mpd_lex_cb(char *input, void (*cb)(int, const char *)); +int mpd_lex_parse(struct mpd_result *result, const char *input); +} + +/* Implementation of the convenience function and the parsing error function + required by Bison */ +%code { + #include "mpd_lexer.h" + + int mpd_lex_cb(char *input, void (*cb)(int, const char *)) + { + int ret; + yyscan_t scanner; + YY_BUFFER_STATE buf; + YYSTYPE val; + + if ((ret = mpd_lex_init(&scanner)) != 0) + return ret; + + buf = mpd__scan_string(input, scanner); + + while ((ret = mpd_lex(&val, scanner)) > 0) + cb(ret, mpd_get_text(scanner)); + + mpd__delete_buffer(buf, scanner); + mpd_lex_destroy(scanner); + return 0; + } + + int mpd_lex_parse(struct mpd_result *result, const char *input) + { + YY_BUFFER_STATE buffer; + yyscan_t scanner; + int retval = -1; + int ret; + + result->errmsg[0] = '\0'; // For safety + + ret = mpd_lex_init(&scanner); + if (ret != 0) + goto error_init; + + buffer = mpd__scan_string(input, scanner); + if (!buffer) + goto error_buffer; + + ret = mpd_parse(result, scanner); + if (ret != 0) + goto error_parse; + + retval = 0; + + error_parse: + mpd__delete_buffer(buffer, scanner); + error_buffer: + mpd_lex_destroy(scanner); + error_init: + return retval; + } + + void mpd_error(struct mpd_result *result, yyscan_t scanner, const char *msg) + { + snprintf(result->errmsg, sizeof(result->errmsg), "%s", msg); + } + +} + +/* ============ ABSTRACT SYNTAX TREE (AST) BOILERPLATE SECTION ===============*/ + +%code { + struct ast + { + int type; + struct ast *l; + struct ast *r; + void *data; + int ival; + }; + + __attribute__((unused)) static struct ast * ast_new(int type, struct ast *l, struct ast *r) + { + struct ast *a = calloc(1, sizeof(struct ast)); + + a->type = type; + a->l = l; + a->r = r; + return a; + } + + /* Note *data is expected to be freeable with regular free() */ + __attribute__((unused)) static struct ast * ast_data(int type, void *data) + { + struct ast *a = calloc(1, sizeof(struct ast)); + + a->type = type; + a->data = data; + return a; + } + + __attribute__((unused)) static struct ast * ast_int(int type, int ival) + { + struct ast *a = calloc(1, sizeof(struct ast)); + + a->type = type; + a->ival = ival; + return a; + } + + __attribute__((unused)) static void ast_free(struct ast *a) + { + if (!a) + return; + + ast_free(a->l); + ast_free(a->r); + free(a->data); + free(a); + } +} + +%destructor { free($$); } +%destructor { ast_free($$); } + + +/* ========================= NON-BOILERPLATE SECTION =========================*/ + +/* Includes required by the parser rules */ +%code top { +#ifndef _GNU_SOURCE +#define _GNU_SOURCE // For asprintf +#endif +#include +#include +#include +#include // For vsnprintf +#include +#include +#include + +#define INVERT_MASK 0x80000000 +} + +/* Dependencies, mocked or real */ +%code top { +#ifndef DEBUG_PARSER_MOCK +#include "db.h" +#include "misc.h" +#else +#include "owntonefunctions.h" +#endif +} + +/* Definition of struct that will hold the parsing result + * Some users have sizeable smart playlists, e.g. listing many artist names, + * which translate to sizeable sql queries. + */ +%code requires { +struct mpd_result_part { + char str[8192]; + int offset; +}; + +struct mpd_result { + struct mpd_result_part where_part; + struct mpd_result_part order_part; + struct mpd_result_part group_part; + char tagtype_buf[64]; + char position_buf[64]; + + // Pointers to the strings in mpd_result_part + const char *where; + const char *order; + const char *group; + + const char *tagtype; + const char *position; + + // Set to 0 if not found + int offset; + int limit; + + int err; + char errmsg[128]; +}; + +enum mpd_type { + MPD_TYPE_INT, + MPD_TYPE_STRING, + MPD_TYPE_SPECIAL, +}; + +struct mpd_tag_map { + const char *name; + const char *dbcol; + enum mpd_type type; + int dbmfi_offset; +}; + +char *mpd_parser_quoted(const char *str); +struct mpd_tag_map *mpd_parser_tag_from_dbcol(const char *dbcol); +void mpd_parser_enum_tagtypes(void (*func)(struct mpd_tag_map *, void *), void *arg); +} + +%code { +enum sql_append_type { + SQL_APPEND_OPERATOR, + SQL_APPEND_OPERATOR_STR, + SQL_APPEND_OPERATOR_LIKE, + SQL_APPEND_FIELD, + SQL_APPEND_STR, + SQL_APPEND_INT, + SQL_APPEND_TIME, + SQL_APPEND_ORDER, + SQL_APPEND_PARENS, +}; + +static struct mpd_tag_map mpd_tag_map[] = +{ + { "Artist", "f.artist", MPD_TYPE_STRING, dbmfi_offsetof(artist), }, + { "ArtistSort", "f.artist_sort", MPD_TYPE_STRING, dbmfi_offsetof(artist_sort), }, + { "AlbumArtist", "f.album_artist", MPD_TYPE_STRING, dbmfi_offsetof(album_artist), }, + { "AlbumArtistSort", "f.album_artist_sort", MPD_TYPE_STRING, dbmfi_offsetof(album_artist_sort), }, + { "Album", "f.album", MPD_TYPE_STRING, dbmfi_offsetof(album), }, + { "AlbumSort", "f.album_sort", MPD_TYPE_STRING, dbmfi_offsetof(album_sort), }, + { "Title", "f.title", MPD_TYPE_STRING, dbmfi_offsetof(title), }, + { "TitleSort", "f.title_sort", MPD_TYPE_STRING, dbmfi_offsetof(title_sort), }, + { "Genre", "f.genre", MPD_TYPE_STRING, dbmfi_offsetof(genre), }, + { "Composer", "f.composer", MPD_TYPE_STRING, dbmfi_offsetof(composer), }, + { "ComposerSort", "f.composer_sort", MPD_TYPE_STRING, dbmfi_offsetof(composer_sort), }, + { "file", "f.virtual_path", MPD_TYPE_SPECIAL, dbmfi_offsetof(virtual_path), }, + + { "base", "f.virtual_path", MPD_TYPE_SPECIAL, dbmfi_offsetof(virtual_path), }, + + { "Track", "f.track", MPD_TYPE_INT, dbmfi_offsetof(track), }, + { "Disc", "f.disc", MPD_TYPE_INT, dbmfi_offsetof(disc), }, + { "Date", "f.year", MPD_TYPE_INT, dbmfi_offsetof(year), }, + + { "modified-since", "f.time_modified", MPD_TYPE_SPECIAL, dbmfi_offsetof(time_modified), }, + { "added-since", "f.time_added", MPD_TYPE_SPECIAL, dbmfi_offsetof(time_added), }, + + // AudioFormat tag + { "samplerate", "f.samplerate", MPD_TYPE_INT, dbmfi_offsetof(samplerate), }, + { "bits_per_sample", "f.bits_per_sample", MPD_TYPE_INT, dbmfi_offsetof(bits_per_sample), }, + { "channels", "f.channels", MPD_TYPE_INT, dbmfi_offsetof(channels), }, + + { NULL }, +}; + +static const char * +tag_to_dbcol(const char *tag) +{ + struct mpd_tag_map *mapptr; + + for (mapptr = mpd_tag_map; mapptr->name; mapptr++) + { + if (strcasecmp(tag, mapptr->name) == 0) + return mapptr->dbcol; + } + + return "error"; // Should never happen, means tag_to_db_map is out of sync with lexer +} + +struct mpd_tag_map * +mpd_parser_tag_from_dbcol(const char *dbcol) +{ + struct mpd_tag_map *mapptr; + + if (!dbcol) + return NULL; + + for (mapptr = mpd_tag_map; mapptr->name; mapptr++) + { + if (strcasecmp(dbcol, mapptr->dbcol) == 0) + return mapptr; + } + + return NULL; +} + +void +mpd_parser_enum_tagtypes(void (*func)(struct mpd_tag_map *, void *), void *arg) +{ + struct mpd_tag_map *mapptr; + + for (mapptr = mpd_tag_map; mapptr->name; mapptr++) + { + func(mapptr, arg); + } +} + +// Remove any backslash that was used to escape single or double quotes +char * +mpd_parser_quoted(const char *str) +{ + char *out = strdup(str + 1); // Copy from after the first quote + size_t len = strlen(out); + const char *src; + char *dst; + + out[len - 1] = '\0'; // Remove terminating quote + + // Remove escaping backslashes + for (src = dst = out; *src != '\0'; src++, dst++) + { + if (*src == '\\') + src++; + if (*src == '\0') + break; + + *dst = *src; + } + + *dst = '\0'; + + return out; +} + +static void sql_from_ast(struct mpd_result *, struct mpd_result_part *, struct ast *); + +// Escapes any '%' or '_' that might be in the string +static void sql_like_escape(char **value, char *escape_char) +{ + char *s = *value; + size_t len = strlen(s); + char *new; + + *escape_char = 0; + + // Fast path, nothing to escape + if (!strpbrk(s, "_%")) + return; + + len = 2 * len; // Enough for every char to be escaped + new = realloc(s, len); + safe_snreplace(new, len, "%", "\\%"); + safe_snreplace(new, len, "_", "\\_"); + *escape_char = '\\'; + *value = new; +} + +static void sql_str_escape(char **value) +{ + char *old = *value; + *value = db_escape_string(old); + free(old); +} + +static void sql_append(struct mpd_result *result, struct mpd_result_part *part, const char *fmt, ...) +{ + va_list ap; + int remaining = sizeof(part->str) - part->offset; + int ret; + + if (remaining <= 0) + goto nospace; + + va_start(ap, fmt); + ret = vsnprintf(part->str + part->offset, remaining, fmt, ap); + va_end(ap); + if (ret < 0 || ret >= remaining) + goto nospace; + + part->offset += ret; + return; + + nospace: + snprintf(result->errmsg, sizeof(result->errmsg), "Parser output buffer too small (%zu bytes)", sizeof(part->str)); + result->err = -2; +} + +static void sql_append_recursive(struct mpd_result *result, struct mpd_result_part *part, struct ast *a, const char *op, const char *op_not, bool is_not, enum sql_append_type append_type) +{ + char escape_char; + + switch (append_type) + { + case SQL_APPEND_OPERATOR: + sql_from_ast(result, part, a->l); + sql_append(result, part, " %s ", is_not ? op_not : op); + sql_from_ast(result, part, a->r); + break; + case SQL_APPEND_OPERATOR_STR: + sql_from_ast(result, part, a->l); + sql_append(result, part, " %s '", is_not ? op_not : op); + sql_from_ast(result, part, a->r); + sql_append(result, part, "'"); + break; + case SQL_APPEND_OPERATOR_LIKE: + sql_from_ast(result, part, a->l); + sql_append(result, part, " %s '%s", is_not ? op_not : op, a->type == MPD_T_STARTSWITH ? "" : "%"); + sql_like_escape((char **)(&a->r->data), &escape_char); + sql_from_ast(result, part, a->r); + sql_append(result, part, "%s'", a->type == MPD_T_ENDSWITH ? "" : "%"); + if (escape_char) + sql_append(result, part, " ESCAPE '%c'", escape_char); + break; + case SQL_APPEND_FIELD: + assert(a->l == NULL); + assert(a->r == NULL); + sql_append(result, part, "%s", tag_to_dbcol((char *)a->data)); + break; + case SQL_APPEND_STR: + assert(a->l == NULL); + assert(a->r == NULL); + sql_str_escape((char **)&a->data); + sql_append(result, part, "%s", (char *)a->data); + break; + case SQL_APPEND_INT: + assert(a->l == NULL); + assert(a->r == NULL); + sql_append(result, part, "%d", a->ival); + break; + case SQL_APPEND_TIME: + assert(a->l == NULL); + assert(a->r == NULL); + sql_str_escape((char **)&a->data); + // MPD docs say the value can be a unix timestamp or ISO 8601 + sql_append(result, part, "strftime('%%s', datetime('%s', '%s'))", (char *)a->data, strchr((char *)a->data, '-') ? "utc" : "unixepoch"); + break; + case SQL_APPEND_ORDER: + assert(a->l == NULL); + assert(a->r == NULL); + if (a->data) + sql_append(result, part, "%s ", tag_to_dbcol((char *)a->data)); + sql_append(result, part, "%s", is_not ? op_not : op); + break; + case SQL_APPEND_PARENS: + assert(a->r == NULL); + if (is_not ? op_not : op) + sql_append(result, part, "%s ", is_not ? op_not : op); + sql_append(result, part, "("); + sql_from_ast(result, part, a->l); + sql_append(result, part, ")"); + break; + } +} + +/* Creates the parsing result from the AST. Errors are set via result->err. */ +static void sql_from_ast(struct mpd_result *result, struct mpd_result_part *part, struct ast *a) { + if (!a || result->err < 0) + return; + + bool is_not = (a->type & INVERT_MASK); + a->type &= ~INVERT_MASK; + + switch (a->type) + { + case MPD_T_LESS: + sql_append_recursive(result, part, a, "<", ">=", is_not, SQL_APPEND_OPERATOR); break; + case MPD_T_LESSEQUAL: + sql_append_recursive(result, part, a, "<=", ">", is_not, SQL_APPEND_OPERATOR); break; + case MPD_T_GREATER: + sql_append_recursive(result, part, a, ">", ">=", is_not, SQL_APPEND_OPERATOR); break; + case MPD_T_GREATEREQUAL: + sql_append_recursive(result, part, a, ">=", "<", is_not, SQL_APPEND_OPERATOR); break; + case MPD_T_EQUAL: + sql_append_recursive(result, part, a, "=", "!=", is_not, SQL_APPEND_OPERATOR_STR); break; + case MPD_T_NOTEQUAL: + sql_append_recursive(result, part, a, "!=", "=", is_not, SQL_APPEND_OPERATOR_STR); break; + case MPD_T_CONTAINS: + case MPD_T_STARTSWITH: + case MPD_T_ENDSWITH: + sql_append_recursive(result, part, a, "LIKE", "NOT LIKE", is_not, SQL_APPEND_OPERATOR_LIKE); break; + case MPD_T_AND: + sql_append_recursive(result, part, a, "AND", "AND NOT", is_not, SQL_APPEND_OPERATOR); break; + case MPD_T_OR: + sql_append_recursive(result, part, a, "OR", "OR NOT", is_not, SQL_APPEND_OPERATOR); break; + case MPD_T_STRING: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_STR); break; + case MPD_T_STRTAG: + case MPD_T_INTTAG: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_FIELD); break; + case MPD_T_NUM: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_INT); break; + case MPD_T_TIME: + sql_append_recursive(result, part, a, NULL, NULL, 0, SQL_APPEND_TIME); break; + case MPD_T_SORT: + sql_append_recursive(result, part, a, "ASC", "DESC", is_not, SQL_APPEND_ORDER); break; + case MPD_T_GROUP: + sql_append_recursive(result, part, a, ",", ",", is_not, SQL_APPEND_OPERATOR); break; + case MPD_T_PARENS: + sql_append_recursive(result, part, a, NULL, "NOT", is_not, SQL_APPEND_PARENS); break; + default: + snprintf(result->errmsg, sizeof(result->errmsg), "Parser produced unrecognized AST type %d", a->type); + result->err = -1; + } +} + +static int result_set(struct mpd_result *result, char *tagtype, struct ast *filter, struct ast *sort, struct ast *window, char *position, struct ast *group) +{ + memset(result, 0, sizeof(struct mpd_result)); + + if (tagtype) + { + snprintf(result->tagtype_buf, sizeof(result->tagtype_buf), "%s", tag_to_dbcol(tagtype)); + result->tagtype = result->tagtype_buf; + } + + sql_from_ast(result, &result->where_part, filter); + if (result->where_part.offset) + result->where = result->where_part.str; + + sql_from_ast(result, &result->order_part, sort); + if (result->order_part.offset) + result->order = result->order_part.str; + + sql_from_ast(result, &result->group_part, group); + if (tagtype) + sql_append(result, &result->group_part, result->group_part.offset ? " , %s" : "%s", tag_to_dbcol(tagtype)); + if (result->group_part.offset) + result->group = result->group_part.str; + + if (position) + { + snprintf(result->position_buf, sizeof(result->position_buf), "%s", position); + result->position = result->position_buf; + } + + if (window && window->l->ival <= window->r->ival) + { + result->offset = window->l->ival; + result->limit = window->r->ival - window->l->ival + 1; + } + + free(tagtype); + free(position); + ast_free(filter); + ast_free(sort); + ast_free(group); + ast_free(window); + + return result->err; +} + +static struct ast * ast_new_strnode(int type, const char *tag, const char *value) +{ + return ast_new(type, ast_data(MPD_T_STRTAG, strdup(tag)), ast_data(MPD_T_STRING, strdup(value))); +} + +/* This creates an OR ast tree with each tag in the tags array */ +static struct ast * ast_new_any(int type, const char *value) +{ + const char *tags[] = { "albumartist", "artist", "album", "title", NULL, }; + const char **tagptr = tags; + struct ast *a; + int op = (type == MPD_T_NOTEQUAL) ? MPD_T_AND : MPD_T_OR; + + a = ast_new_strnode(type, *tagptr, value); + for (tagptr++; *tagptr; tagptr++) + a = ast_new(op, a, ast_new_strnode(type, *tagptr, value)); + + // In case the expression will be negated, e.g. !(any contains 'cat'), we must + // group the result + return ast_new(MPD_T_PARENS, a, NULL); +} + +static struct ast * ast_new_audioformat(int type, const char *value) +{ + const char *tags[] = { "samplerate", "bits_per_sample", "channels", NULL, }; + const char **tagptr = tags; + char *value_copy = strdup(value); + char *saveptr; + char *token; + struct ast *a; + + token = strtok_r(value_copy, ":", &saveptr); + + a = ast_new_strnode(type, "samplerate", value_copy); + if (!token) + goto end; // If there was no ':' we assume we just have a samplerate + + for (tagptr++, token = strtok_r(NULL, ":", &saveptr); *tagptr && token; tagptr++, token = strtok_r(NULL, ":", &saveptr)) + a = ast_new(MPD_T_AND, a, ast_new_strnode(type, *tagptr, token)); + + end: + free(value_copy); + + // In case the expression will be negated we group the result + return ast_new(MPD_T_PARENS, a, NULL); +} +} + +%union { + unsigned int ival; + char *str; + struct ast *ast; +} + +/* mpd commands */ +%token MPD_T_CMDSEARCH +%token MPD_T_CMDSEARCHADD +%token MPD_T_CMDFIND +%token MPD_T_CMDFINDADD +%token MPD_T_CMDCOUNT +%token MPD_T_CMDSEARCHCOUNT +%token MPD_T_CMDPLAYLISTFIND +%token MPD_T_CMDPLAYLISTSEARCH +%token MPD_T_CMDLIST + +%token MPD_T_SORT +%token MPD_T_WINDOW +%token MPD_T_POSITION +%token MPD_T_GROUP + +/* A string that was quoted. Quotes were stripped by lexer. */ +%token MPD_T_STRING + +/* Numbers (integers) */ +%token MPD_T_NUM + +/* Since time is a quoted string the lexer will just use a MPD_T_STRING token. + The parser will recognize it as time based on the keyword and use MPD_T_TIME + for the AST tree. */ +%token MPD_T_TIME + +/* The semantic value holds the actual name of the field */ +%token MPD_T_STRTAG +%token MPD_T_INTTAG +%token MPD_T_SINCETAG +%token MPD_T_BASETAG + +%token MPD_T_ANYTAG +%token MPD_T_AUDIOFORMATTAG +%token MPD_T_PARENS +%token MPD_T_OR +%token MPD_T_AND +%token MPD_T_NOT + +/* The below are only ival so we can set intbool, datebool and strbool via the + default rule for semantic values, i.e. $$ = $1. The semantic value (ival) is + set to the token value by the lexer. */ +%token MPD_T_CONTAINS +%token MPD_T_STARTSWITH +%token MPD_T_ENDSWITH +%token MPD_T_EQUAL +%token MPD_T_NOTEQUAL +%token MPD_T_LESS +%token MPD_T_LESSEQUAL +%token MPD_T_GREATER +%token MPD_T_GREATEREQUAL + +%left MPD_T_OR +%left MPD_T_AND +%left MPD_T_NOT + +%type tagtype +%type filter +%type sort +%type window +%type position +%type group +%type groups +%type predicate +%type strbool +%type intbool + +%% + +/* + * Version 0.24: + * Type cmd_fsw (filter sort window): + * playlistfind {FILTER} [sort {TYPE}] [window {START:END}] + * playlistsearch {FILTER} [sort {TYPE}] [window {START:END}] + * find {FILTER} [sort {TYPE}] [window {START:END}] + * search {FILTER} [sort {TYPE}] [window {START:END}] + * Type fswp (filter sort window position): + * findadd {FILTER} [sort {TYPE}] [window {START:END}] [position POS] + * searchadd {FILTER} [sort {TYPE}] [window {START:END}] [position POS] + * Type fg (filter group): + * count {FILTER} [group {GROUPTYPE} group {GROUPTYPE} ...] + * searchcount {FILTER} [group {GROUPTYPE} group {GROUPTYPE} ...] + * Type tfg (type filter group): + * list {TYPE} {FILTER} [group {GROUPTYPE} group {GROUPTYPE} ...] + * Not implemented: + * searchaddpl {NAME} {FILTER} [sort {TYPE}] [window {START:END}] [position POS] + * searchplaylist {NAME} {FILTER} [{START:END}] + * case sensitivity for find + */ + +command: cmd_fsw filter sort window { if (result_set(result, NULL, $2, $3, $4, NULL, NULL) < 0) YYABORT; } +| cmd_fswp filter sort window position { if (result_set(result, NULL, $2, $3, $4, $5, NULL) < 0) YYABORT; } +| cmd_fg filter groups { if (result_set(result, NULL, $2, NULL, NULL, NULL, $3) < 0) YYABORT; } +| cmd_fg groups { if (result_set(result, NULL, NULL, NULL, NULL, NULL, $2) < 0) YYABORT; } +| cmd_tfg tagtype filter groups { if (result_set(result, $2, $3, NULL, NULL, NULL, $4) < 0) YYABORT; } +| cmd_tfg tagtype groups { if (result_set(result, $2, NULL, NULL, NULL, NULL, $3) < 0) YYABORT; } +; + +tagtype: MPD_T_STRTAG { if (asprintf(&($$), "%s", $1) < 0) YYABORT; } +; + +filter: filter MPD_T_AND filter { $$ = ast_new(MPD_T_AND, $1, $3); } +| filter MPD_T_OR filter { $$ = ast_new(MPD_T_OR, $1, $3); } +| '(' filter ')' { $$ = ast_new(MPD_T_PARENS, $2, NULL); } +| MPD_T_NOT filter { struct ast *a = $2; a->type |= INVERT_MASK; $$ = $2; } +| predicate +; + +sort: MPD_T_SORT MPD_T_STRTAG { $$ = ast_data(MPD_T_SORT, $2); } +| MPD_T_SORT '-' MPD_T_STRTAG { $$ = ast_data(MPD_T_SORT | INVERT_MASK, $3); } +| %empty { $$ = NULL; } +; + +window: MPD_T_WINDOW MPD_T_NUM ':' MPD_T_NUM { $$ = ast_new(MPD_T_WINDOW, ast_int(MPD_T_NUM, $2), ast_int(MPD_T_NUM, $4)); } +| %empty { $$ = NULL; } +; + +position: MPD_T_POSITION MPD_T_NUM { if (asprintf(&($$), "%d", $2) < 0) YYABORT; } +| MPD_T_POSITION '+' MPD_T_NUM { if (asprintf(&($$), "+%d", $3) < 0) YYABORT; } +| MPD_T_POSITION '-' MPD_T_NUM { if (asprintf(&($$), "-%d", $3) < 0) YYABORT; } +| %empty { $$ = NULL; } +; + +groups: groups group { $$ = $1 ? ast_new(MPD_T_GROUP, $2, $1) : $2; } +| %empty { $$ = NULL; } +; + +group: MPD_T_GROUP MPD_T_STRTAG { $$ = ast_data(MPD_T_STRTAG, $2); } +| MPD_T_GROUP MPD_T_INTTAG { $$ = ast_data(MPD_T_INTTAG, $2); } +; + +// We accept inttags with numeric and string values, so both date == 2007 and date == '2007' +predicate: '(' MPD_T_STRTAG strbool MPD_T_STRING ')' { $$ = ast_new($3, ast_data(MPD_T_STRTAG, $2), ast_data(MPD_T_STRING, $4)); } +| '(' MPD_T_INTTAG strbool MPD_T_STRING ')' { $$ = ast_new($3, ast_data(MPD_T_STRTAG, $2), ast_data(MPD_T_STRING, $4)); } +| '(' MPD_T_INTTAG intbool MPD_T_NUM ')' { $$ = ast_new($3, ast_data(MPD_T_INTTAG, $2), ast_int(MPD_T_NUM, $4)); } +| '(' MPD_T_ANYTAG strbool MPD_T_STRING ')' { $$ = ast_new_any($3, $4); } +| '(' MPD_T_AUDIOFORMATTAG strbool MPD_T_STRING ')' { $$ = ast_new_audioformat($3, $4); } +| '(' MPD_T_SINCETAG MPD_T_STRING ')' { $$ = ast_new(MPD_T_GREATEREQUAL, ast_data(MPD_T_STRTAG, $2), ast_data(MPD_T_TIME, $3)); } +| '(' MPD_T_BASETAG MPD_T_STRING ')' { $$ = ast_new(MPD_T_STARTSWITH, ast_data(MPD_T_STRTAG, $2), ast_data(MPD_T_STRING, $3)); } +; + +strbool: MPD_T_EQUAL +| MPD_T_NOTEQUAL +| MPD_T_CONTAINS +| MPD_T_STARTSWITH +| MPD_T_ENDSWITH +; + +intbool: MPD_T_LESS +| MPD_T_LESSEQUAL +| MPD_T_GREATER +| MPD_T_GREATEREQUAL +; + +cmd_fsw: MPD_T_CMDPLAYLISTFIND +| MPD_T_CMDPLAYLISTSEARCH +| MPD_T_CMDSEARCH +| MPD_T_CMDFIND +; + +cmd_fswp: MPD_T_CMDFINDADD +| MPD_T_CMDSEARCHADD +; + +cmd_fg: MPD_T_CMDCOUNT +| MPD_T_CMDSEARCHCOUNT +; + +cmd_tfg: MPD_T_CMDLIST +; + +%% +