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