/*
 * Copyright (C) 2016-2018 Espen Jürgensen <espenjurgensen@gmail.com>
 * Copyright (C) 2009-2011 Julien BLACHE <jb@jblache.org>
 * Copyright (C) 2010 Kai Elwert <elwertk@googlemail.com>
 *
 * Adapted from mt-daapd:
 * Copyright (C) 2003-2007 Ron Pedde <ron@pedde.com>
 *
 * 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
 */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <sys/queue.h>
#include <sys/types.h>
#include <limits.h>
#include <stdint.h>
#include <inttypes.h>
#include <time.h>
#include <ctype.h>

#include <uninorm.h>
#include <unistd.h>

#include <gcrypt.h>
#include <event2/event.h>

#include "httpd_internal.h"
#include "httpd_daap.h"
#include "logger.h"
#include "db.h"
#include "conffile.h"
#include "misc.h"
#include "transcode.h"
#include "artwork.h"
#include "dmap_common.h"
#include "cache.h"


/* Max number of sessions and session timeout
 * Many clients (including iTunes) don't seem to respect the timeout capability
 * that we announce, and just keep using the same session. Therefore we take a
 * lenient approach to actually timing out: We wait an entire week, and to
 * avoid running a timer for that long, we only check for expiration when adding
 * new sessions - see daap_session_cleanup().
 */
#define DAAP_SESSION_MAX 200
#define DAAP_SESSION_TIMEOUT 604800            // One week in seconds
/* We announce this timeout to the client when returning server capabilities */
#define DAAP_SESSION_TIMEOUT_CAPABILITY 1800   // 30 minutes
/* Update requests refresh interval in seconds */
#define DAAP_UPDATE_REFRESH  0

/* Database number for the Radio item */
#define DAAP_DB_RADIO 2

/* Errors that the reply handlers may return */
enum daap_reply_result
{
  DAAP_REPLY_LOGOUT          =  4,
  DAAP_REPLY_NONE            =  3,
  DAAP_REPLY_NO_CONTENT      =  2,
  DAAP_REPLY_OK_NO_GZIP      =  1,
  DAAP_REPLY_OK              =  0,
  DAAP_REPLY_NO_CONNECTION   = -1,
  DAAP_REPLY_ERROR           = -2,
  DAAP_REPLY_FORBIDDEN       = -3,
  DAAP_REPLY_BAD_REQUEST     = -4,
  DAAP_REPLY_SERVUNAVAIL     = -5,
};

struct daap_session {
  uint32_t id;
  time_t mtime;
  bool is_remote;

  struct daap_session *next;
};

struct daap_update_request {
  struct httpd_request *hreq;

  /* Refresh tiemout */
  struct event *timeout;

  struct daap_update_request *next;
};

struct sort_ctx {
  struct evbuffer *headerlist;
  int16_t mshc;
  uint32_t mshi;
  uint32_t mshn;
  uint32_t misc_mshn;
};


/* Default meta tags if not provided in the query */
static char *default_meta_plsongs = "dmap.itemkind,dmap.itemid,dmap.itemname,dmap.containeritemid,dmap.parentcontainerid";
static char *default_meta_pl = "dmap.itemid,dmap.itemname,dmap.persistentid,com.apple.itunes.smart-playlist";
static char *default_meta_group = "dmap.itemname,dmap.persistentid,daap.songalbumartist";

/* DAAP session tracking */
static struct daap_session *daap_sessions;
static pthread_mutex_t daap_session_lck;

/* Update requests */
static int current_rev;
static struct daap_update_request *update_requests;
static struct timeval daap_update_refresh_tv = { DAAP_UPDATE_REFRESH, 0 };


/* -------------------------- SESSION HANDLING ------------------------------ */

static void
daap_session_free(struct daap_session *s)
{
  free(s);
}

static void
daap_session_remove_locked(struct daap_session *s)
{
  struct daap_session *ptr;
  struct daap_session *prev;

  prev = NULL;
  for (ptr = daap_sessions; ptr; ptr = ptr->next)
    {
      if (ptr == s)
	break;

      prev = ptr;
    }

  if (!ptr)
    {
      DPRINTF(E_LOG, L_DAAP, "Error: Request to remove non-existent or ad-hoc session. BUG!\n");
      return;
    }

  if (!prev)
    daap_sessions = s->next;
  else
    prev->next = s->next;

  daap_session_free(s);
}

static struct daap_session *
daap_session_get_locked(uint32_t id)
{
  struct daap_session *s;

  for (s = daap_sessions; s; s = s->next)
    {
      if (id == s->id)
	return s;
    }

  return NULL;
}

static bool
daap_session_copy(struct daap_session *dst, uint32_t id)
{
  struct daap_session *s;
  bool is_found = false;

  CHECK_ERR(L_DAAP, pthread_mutex_lock(&daap_session_lck));

  s = daap_session_get_locked(id);
  if (s)
    {
      *dst = *s;
      dst->next = NULL;
      is_found = true;
    }

  CHECK_ERR(L_DAAP, pthread_mutex_unlock(&daap_session_lck));

  return is_found;
}

static bool
daap_session_touch(uint32_t id)
{
  struct daap_session *s;
  bool is_found = false;

  CHECK_ERR(L_DAAP, pthread_mutex_lock(&daap_session_lck));

  s = daap_session_get_locked(id);
  if (s)
    {
      s->mtime = time(NULL);
      is_found = true;
    }

  CHECK_ERR(L_DAAP, pthread_mutex_unlock(&daap_session_lck));

  return is_found;
}

static void
daap_session_remove(uint32_t id)
{
  struct daap_session *s;

  CHECK_ERR(L_DAAP, pthread_mutex_lock(&daap_session_lck));

  s = daap_session_get_locked(id);
  if (s)
    daap_session_remove_locked(s);

  CHECK_ERR(L_DAAP, pthread_mutex_unlock(&daap_session_lck));
}

/* Removes stale sessions and also drops the oldest sessions if DAAP_SESSION_MAX
 * will otherwise be exceeded
 */
static void
daap_session_cleanup_locked(void)
{
  struct daap_session *s;
  struct daap_session *next;
  time_t now;
  int count;

  count = 0;
  now = time(NULL);

  for (s = daap_sessions; s; s = next)
    {
      count++;
      next = s->next;

      if ((difftime(now, s->mtime) > DAAP_SESSION_TIMEOUT) || (count > DAAP_SESSION_MAX))
	{
	  DPRINTF(E_LOG, L_DAAP, "Cleaning up DAAP session (id %" PRIu32 ")\n", s->id);

	  daap_session_remove_locked(s);
	}
    }
}

static int
daap_session_add(uint32_t *session_id, bool is_remote, uint32_t request_session_id)
{
  struct daap_session *s;

  CHECK_NULL(L_DAAP, s = calloc(1, sizeof(struct daap_session)));

  CHECK_ERR(L_DAAP, pthread_mutex_lock(&daap_session_lck));

  daap_session_cleanup_locked();

  if (request_session_id)
    {
      if (daap_session_get_locked(request_session_id))
	{
	  DPRINTF(E_LOG, L_DAAP, "Session id requested in login (%d) is not available\n", request_session_id);
	  CHECK_ERR(L_DAAP, pthread_mutex_unlock(&daap_session_lck));
	  free(s);
	  return -1;
	}

      s->id = request_session_id;
    }
  else do
    {
      gcry_randomize(&s->id, sizeof(s->id), GCRY_STRONG_RANDOM);
      if (s->id < 100)
	s->id += 100;
    }
  while (daap_session_get_locked(s->id) != NULL);

  s->mtime = time(NULL);

  s->is_remote = is_remote;

  if (daap_sessions)
    s->next = daap_sessions;

  daap_sessions = s;

  *session_id = s->id;

  CHECK_ERR(L_DAAP, pthread_mutex_unlock(&daap_session_lck));

  return 0;
}

/* ---------------------- UPDATE REQUESTS HANDLERS -------------------------- */

static void
update_free(struct daap_update_request *ur)
{
  if (!ur)
    return;

  if (ur->timeout)
    event_free(ur->timeout);

  free(ur);
}

static void
update_remove(struct daap_update_request *ur)
{
  struct daap_update_request *p;

  if (ur == update_requests)
    update_requests = ur->next;
  else
    {
      for (p = update_requests; p && (p->next != ur); p = p->next)
	;

      if (!p)
	{
	  DPRINTF(E_LOG, L_DAAP, "WARNING: struct daap_update_request not found in list; BUG!\n");
	  return;
	}

      p->next = ur->next;
    }

  update_free(ur);
}

static void
update_refresh_cb(int fd, short event, void *arg)
{
  struct daap_update_request *ur = arg;
  struct httpd_request *hreq = ur->hreq;

  current_rev++;

  /* Send back current revision */
  dmap_add_container(hreq->out_body, "mupd", 24);
  dmap_add_int(hreq->out_body, "mstt", 200);         /* 12 */
  dmap_add_int(hreq->out_body, "musr", current_rev); /* 12 */

  httpd_send_reply(hreq, HTTP_OK, "OK", 0);

  update_remove(ur);
}

static void
update_fail_cb(void *arg)
{
  struct daap_update_request *ur = arg;

  DPRINTF(E_DBG, L_DAAP, "Update request: client closed connection\n");

  update_remove(ur);
}


/* ------------------------- SORT HEADERS HELPERS --------------------------- */

static struct sort_ctx *
daap_sort_context_new(void)
{
  struct sort_ctx *ctx;
  int ret;

  ctx = calloc(1, sizeof(struct sort_ctx));
  if (!ctx)
    {
      DPRINTF(E_LOG, L_DAAP, "Out of memory for sorting context\n");

      return NULL;
    }

  memset(ctx, 0, sizeof(struct sort_ctx));

  ctx->headerlist = evbuffer_new();
  if (!ctx->headerlist)
    {
      DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP sort headers list\n");

      free(ctx);
      return NULL;
    }

  ret = evbuffer_expand(ctx->headerlist, 512);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP sort headers list\n");

      evbuffer_free(ctx->headerlist);
      free(ctx);
      return NULL;
    }

  ctx->mshc = -1;

  return ctx;
}

static void
daap_sort_context_free(struct sort_ctx *ctx)
{
  evbuffer_free(ctx->headerlist);
  free(ctx);
}

static int
daap_sort_build(struct sort_ctx *ctx, char *str)
{
  uint8_t *ret;
  size_t len;
  char fl;

  len = strlen(str);
  if (len > 0)
    {
      ret = u8_normalize(UNINORM_NFD, (uint8_t *)str, len + 1, NULL, &len);
      if (!ret)
	{
	  DPRINTF(E_LOG, L_DAAP, "Could not normalize string for sort header\n");

	  return -1;
	}

      fl = ret[0];
      free(ret);
    }
  else
    fl = 0;

  if (isascii(fl) && isalpha(fl))
    {
      fl = toupper(fl);

      /* Init */
      if (ctx->mshc == -1)
	ctx->mshc = fl;

      if (fl == ctx->mshc)
	ctx->mshn++;
      else
        {
	  dmap_add_container(ctx->headerlist, "mlit", 34);
	  dmap_add_short(ctx->headerlist, "mshc", ctx->mshc); /* 10 */
	  dmap_add_int(ctx->headerlist, "mshi", ctx->mshi);   /* 12 */
	  dmap_add_int(ctx->headerlist, "mshn", ctx->mshn);   /* 12 */

	  DPRINTF(E_DBG, L_DAAP, "Added sort header: mshc = %c, mshi = %u, mshn = %u fl %c\n", ctx->mshc, ctx->mshi, ctx->mshn, fl);

	  ctx->mshi = ctx->mshi + ctx->mshn;
	  ctx->mshn = 1;
	  ctx->mshc = fl;
	}
    }
  else
    {
      /* Non-ASCII, goes to misc category */
      ctx->misc_mshn++;
    }

  return 0;
}

static void
daap_sort_finalize(struct sort_ctx *ctx)
{
  /* Add current entry, if any */
  if (ctx->mshc != -1)
    {
      dmap_add_container(ctx->headerlist, "mlit", 34);
      dmap_add_short(ctx->headerlist, "mshc", ctx->mshc); /* 10 */
      dmap_add_int(ctx->headerlist, "mshi", ctx->mshi);   /* 12 */
      dmap_add_int(ctx->headerlist, "mshn", ctx->mshn);   /* 12 */

      ctx->mshi = ctx->mshi + ctx->mshn;

      DPRINTF(E_DBG, L_DAAP, "Added sort header: mshc = %c, mshi = %u, mshn = %u (final)\n", ctx->mshc, ctx->mshi, ctx->mshn);
    }

  /* Add misc category */
  dmap_add_container(ctx->headerlist, "mlit", 34);
  dmap_add_short(ctx->headerlist, "mshc", '0');          /* 10 */
  dmap_add_int(ctx->headerlist, "mshi", ctx->mshi);      /* 12 */
  dmap_add_int(ctx->headerlist, "mshn", ctx->misc_mshn); /* 12 */
}


/* ----------------------------- OTHER HELPERS ------------------------------ */

/* We try not to return items that the client cannot play (like Spotify and
 * internet streams in iTunes), or which are inappropriate (like internet streams
 * in the album tab of remotes). Note that the function must never append a
 * filter if the SELECT is not from the files table.
 */
static void
user_agent_filter(struct query_params *qp, struct httpd_request *hreq)
{
  struct daap_session *s = hreq->extra_data;
  char *filter;

  if (s->is_remote)
    {
      // This makes sure 1) the SELECT is from files, 2) that the Remote query
      // contained extended_media_kind:1, which characterise the queries we want
      // to filter. TODO: Not a really nice way of doing this, but best I could
      // think of.
      if (!qp->filter || !strstr(qp->filter, "f.media_kind = 1"))
	return;

      filter = safe_asprintf("%s AND (f.data_kind <> %d)", qp->filter, DATA_KIND_HTTP);
    }
  else
    {
      if (qp->type != Q_ITEMS)
	return;

      if (qp->filter)
	filter = safe_asprintf("%s AND (f.data_kind = %d)", qp->filter, DATA_KIND_FILE);
      else
	filter = safe_asprintf("(f.data_kind = %d)", DATA_KIND_FILE);
    }

  free(qp->filter);
  qp->filter = filter;

  DPRINTF(E_DBG, L_DAAP, "SQL filter w/client mod: %s\n", qp->filter);
}

static void
query_params_set(struct query_params *qp, int *sort_headers, struct httpd_request *hreq, enum query_type type)
{
  const char *param;
  char *ptr;
  int low;
  int high;
  int ret;

  low = 0;
  high = -1; /* No limit */

  memset(qp, 0, sizeof(struct query_params));

  param = httpd_query_value_find(hreq->query, "index");
  if (param)
    {
      if (param[0] == '-') /* -n, last n entries */
	DPRINTF(E_LOG, L_DAAP, "Unsupported index range: %s\n", param);
      else
	{
	  ret = safe_atoi32(param, &low);
	  if (ret < 0)
	    DPRINTF(E_LOG, L_DAAP, "Could not parse index range: %s\n", param);
	  else
	    {
	      ptr = strchr(param, '-');
	      if (!ptr) /* single item */
		high = low;
	      else
		{
		  ptr++;
		  if (*ptr != '\0') /* low-high */
		    {
		      ret = safe_atoi32(ptr, &high);
		      if (ret < 0)
			  DPRINTF(E_LOG, L_DAAP, "Could not parse high index in range: %s\n", param);
		    }
		}
	    }
	}

      DPRINTF(E_DBG, L_DAAP, "Index range %s: low %d, high %d (offset %d, limit %d)\n", param, low, high, qp->offset, qp->limit);
    }

  if (high < low)
    high = -1; /* No limit */

  qp->offset = low;
  if (high < 0)
    qp->limit = -1; /* No limit */
  else
    qp->limit = (high - low) + 1;

  if (qp->limit == -1 && qp->offset == 0)
    qp->idx_type = I_NONE;
  else
    qp->idx_type = I_SUB;

  qp->sort = S_NONE;
  param = httpd_query_value_find(hreq->query, "sort");
  if (param)
    {
      if (strcmp(param, "name") == 0)
	qp->sort = S_NAME;
      else if (strcmp(param, "album") == 0 && (type != Q_BROWSE_ALBUMS)) // Only set if non-default sort requested
	qp->sort = S_ALBUM;
      else if (strcmp(param, "artist") == 0 && (type != Q_BROWSE_ARTISTS)) // Only set if non-default sort requested
	qp->sort = S_ARTIST;
      else if (strcmp(param, "releasedate") == 0)
	qp->sort = S_DATE_RELEASED;
      else
	DPRINTF(E_DBG, L_DAAP, "Unknown sort param: %s\n", param);

      if (qp->sort != S_NONE)
	DPRINTF(E_DBG, L_DAAP, "Sorting songlist by %s\n", param);
    }

  if (sort_headers)
    {
      *sort_headers = 0;
      param = httpd_query_value_find(hreq->query, "include-sort-headers");
      if (param && (strcmp(param, "1") == 0))
	{
	  *sort_headers = 1;
	  DPRINTF(E_SPAM, L_DAAP, "Sort headers requested\n");
	}
    }

  param = httpd_query_value_find(hreq->query, "query");
  if (!param)
    param = httpd_query_value_find(hreq->query, "filter");

  if (param)
    {
      DPRINTF(E_DBG, L_DAAP, "DAAP browse query filter: %s\n", param);

      qp->filter = dmap_query_parse_sql(param);
      if (!qp->filter)
	DPRINTF(E_LOG, L_DAAP, "Ignoring improper DAAP query: %s\n", param);

      /* iTunes seems to default to this when there is a query (which there is for audiobooks, but not for normal playlists) */
      if (!qp->sort && !(type & Q_F_BROWSE))
	qp->sort = S_ALBUM;
    }

  qp->type = type;

  user_agent_filter(qp, hreq);
}

#define NMETA_MAX 128
static int
parse_meta(const struct dmap_field ***out_meta, const char *param)
{
  const struct dmap_field *meta[NMETA_MAX];
  const struct dmap_field *dfield;
  size_t out_size;
  char *saveptr;
  char *field;
  char *param_copy;
  bool is_duplicate;
  int nmeta;
  int i;

  CHECK_NULL(L_DAAP, param_copy = strdup(param));
  field = strtok_r(param_copy, ",", &saveptr);
  nmeta = 0;
  for (; field && nmeta < NMETA_MAX; field = strtok_r(NULL, ",", &saveptr))
    {
      dfield = dmap_find_field_wrapper(field);
      if (!dfield)
	{
	  DPRINTF(E_DBG, L_DAAP, "Could not find requested meta field '%s' in '%s'\n", field, param);
	  continue;
	}

      is_duplicate = false;
      for (i = 0; i < nmeta && !is_duplicate; i++)
	is_duplicate = (dfield == meta[i]);
      if (is_duplicate)
	{
	  DPRINTF(E_SPAM, L_DAAP, "Parser will ignore duplicate occurrence of meta field '%s'\n", field);
	  continue;
	}

      meta[nmeta] = dfield;
      nmeta++;
    }

  if (nmeta == 0)
    {
      printf("No known metadata fields found in input '%s'\n", param);
      goto out;
    }

  // meta is a list of pointers to dmf entries, here we copy it to the heap
  out_size = nmeta * sizeof(struct dmap_field *);
  CHECK_NULL(L_DAAP, *out_meta = malloc(out_size));
  memcpy(*out_meta, meta, out_size);

 out:
  free(param_copy);
  return nmeta;
}
#undef NMETA_MAX

static void
daap_reply_send(struct httpd_request *hreq, enum daap_reply_result result)
{
  switch (result)
    {
      case DAAP_REPLY_LOGOUT:
	httpd_send_reply(hreq, HTTP_NOCONTENT, "Logout Successful", 0);
	break;
      case DAAP_REPLY_NO_CONTENT:
	httpd_send_reply(hreq, HTTP_NOCONTENT, "No Content", HTTPD_SEND_NO_GZIP);
	break;
      case DAAP_REPLY_OK:
	httpd_send_reply(hreq, HTTP_OK, "OK", 0);
	break;
      case DAAP_REPLY_OK_NO_GZIP:
      case DAAP_REPLY_ERROR:
	httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP);
	break;
      case DAAP_REPLY_FORBIDDEN:
	httpd_send_error(hreq, HTTP_FORBIDDEN, "Forbidden");
	break;
      case DAAP_REPLY_BAD_REQUEST:
	httpd_send_error(hreq, HTTP_BADREQUEST, "Bad Request");
	break;
      case DAAP_REPLY_SERVUNAVAIL:
	httpd_send_error(hreq, HTTP_SERVUNAVAIL, "Internal Server Error");
	break;
      case DAAP_REPLY_NO_CONNECTION:
      case DAAP_REPLY_NONE:
	// Send nothing
	break;
    }
}

// The request is authorized if one of these conditions is met:
//
// a) the client's address is in 'trusted_networks'
// b) the client has a pairing-guid we know about (Remotes)
// c) the client has a session id we know about
// d) the client has valid password for httpd_basic_auth (iTunes/Apple Music)
static int
daap_request_authorize(struct httpd_request *hreq)
{
  struct daap_session *session;
  struct pairing_info pi = { 0 };
  const char *param;
  char *passwd;
  int ret;

  if (httpd_request_is_trusted(hreq))
    return 0;

  param = httpd_query_value_find(hreq->query, "pairing-guid");
  if (param)
    {
      if (strlen(param) < 3)
	{
	  DPRINTF(E_LOG, L_DAAP, "Request from %s with invalid pairing-guid: %s\n", hreq->peer_address, param);
	  goto unauthorized;
	}

      pi.guid = strdup(param + 2); /* Skip leading 0X */

      ret = db_pairing_fetch_byguid(&pi);
      if (ret < 0)
	{
	  DPRINTF(E_LOG, L_DAAP, "Request from %s with invalid pairing-guid: %s\n", hreq->peer_address, param);
	  free_pi(&pi, 1);
	  goto unauthorized;
	}

      DPRINTF(E_INFO, L_DAAP, "Remote '%s' (%s) logging in with GUID %s\n", pi.name, hreq->peer_address, pi.guid);
      free_pi(&pi, 1);
      return 0;
    }

  session = hreq->extra_data;
  if (session && session->id != 0)
    {
      if (daap_session_touch(session->id))
	{
	  session->mtime = time(NULL);
	  return 0;
	}
    }

  passwd = cfg_getstr(cfg_getsec(cfg, "library"), "password");
  if (!passwd)
    {
      DPRINTF(E_LOG, L_DAAP, "Unauthorized request from '%s'\n", hreq->peer_address);
      goto unauthorized;
    }

  ret = httpd_basic_auth(hreq, NULL, passwd, cfg_getstr(cfg_getsec(cfg, "library"), "name"));
  if (ret == 0)
    {
      return 0;
    }

  // httpd_basic_auth sent a response, so we just exit
  return -1;

 unauthorized:
  httpd_send_error(hreq, HTTP_UNAUTHORIZED, "Unauthorized");;
  return -1;
}


/* --------------------------- REPLY HANDLERS ------------------------------- */
/* Note that some handlers can be called without a connection (needed for     */
/* cache regeneration), while others cannot. Those that cannot should check   */
/* that hreq->backend is not null.                                            */

static enum daap_reply_result
daap_reply_server_info(struct httpd_request *hreq)
{
  struct evbuffer *content;
  char *name;
  char *passwd;
  const char *clientver;
  size_t len;
  int mpro;
  int apro;

  if (!hreq->backend)
    {
      DPRINTF(E_LOG, L_DAAP, "Bug! daap_reply_server_info() cannot be called without an actual connection\n");
      return DAAP_REPLY_NO_CONNECTION;
    }

  passwd = cfg_getstr(cfg_getsec(cfg, "library"), "password");
  name = cfg_getstr(cfg_getsec(cfg, "library"), "name");

  CHECK_NULL(L_DAAP, content = evbuffer_new());
  CHECK_ERR(L_DAAP, evbuffer_expand(content, 512));

  mpro = 2 << 16 | 10;
  apro = 3 << 16 | 12;

  if (hreq->in_headers && (clientver = httpd_header_find(hreq->in_headers, "Client-DAAP-Version")))
    {
      if (strcmp(clientver, "1.0") == 0)
	{
	  mpro = 1 << 16;
	  apro = 1 << 16;
	}
      else if (strcmp(clientver, "2.0") == 0)
	{
	  mpro = 1 << 16;
	  apro = 2 << 16;
	}
    }

  dmap_add_int(content, "mstt", 200);
  dmap_add_int(content, "mpro", mpro);       // dmap.protocolversion
  dmap_add_string(content, "minm", name);    // dmap.itemname (server name)

  dmap_add_int(content, "apro", apro);       // daap.protocolversion
  dmap_add_int(content, "aeSV", apro);       // com.apple.itunes.music-sharing-version (determines if itunes shows share types)

  dmap_add_short(content, "ated", 7);        // daap.supportsextradata

  // Sub-optimal user-agent sniffing to solve the problem that iTunes 12.1 and
  // Apple Music do not work if we announce support for groups
  if (hreq->user_agent && (strncmp(hreq->user_agent, "iTunes", strlen("iTunes")) == 0))
    dmap_add_short(content, "asgr", 0);      // daap.supportsgroups (1=artists, 2=albums, 3=both)
  else if (hreq->user_agent && (strncmp(hreq->user_agent, "Music", strlen("Music")) == 0))
    dmap_add_short(content, "asgr", 0);      // daap.supportsgroups (1=artists, 2=albums, 3=both)
  else
    dmap_add_short(content, "asgr", 3);      // daap.supportsgroups (1=artists, 2=albums, 3=both)

//  dmap_add_long(content, "asse", 0x80000); // unknown - used by iTunes

  dmap_add_char(content, "aeMQ", 1);         // unknown - used by iTunes

//  dmap_add_long(content, "mscu", );        // unknown - used by iTunes
//  dmap_add_char(content, "aeFR", );        // unknown - used by iTunes

  dmap_add_char(content, "aeTr", 1);         // unknown - used by iTunes
  dmap_add_char(content, "aeSL", 1);         // unknown - used by iTunes
  dmap_add_char(content, "aeSR", 1);         // unknown - used by iTunes
//  dmap_add_char(content, "aeFP", 2);       // triggers FairPlay request
//  dmap_add_long(content, "aeSX", );        // unknown - used by iTunes

//  dmap_add_int(content, "ppro", );         // dpap.protocolversion

  dmap_add_char(content, "msed", 0);         // dmap.supportsedit? - we don't support playlist editing

  dmap_add_char(content, "mslr", 1);         // dmap.loginrequired
  dmap_add_int(content, "mstm", DAAP_SESSION_TIMEOUT_CAPABILITY); // dmap.timeoutinterval
  dmap_add_char(content, "msal", 1);         // dmap.supportsautologout
//  dmap_add_char(content, "msas", 3);       // dmap.authenticationschemes
  dmap_add_char(content, "msau", (passwd) ? 2 : 0); // dmap.authenticationmethod

  dmap_add_char(content, "msup", 1);         // dmap.supportsupdate
  dmap_add_char(content, "mspi", 1);         // dmap.supportspersistentids
  dmap_add_char(content, "msex", 1);         // dmap.supportsextensions
  dmap_add_char(content, "msbr", 1);         // dmap.supportsbrowse
  dmap_add_char(content, "msqy", 1);         // dmap.supportsquery
  dmap_add_char(content, "msix", 1);         // dmap.supportsindex
//  dmap_add_char(content, "msrs", 1);       // dmap.supportsresolve

  dmap_add_int(content, "msdc", 2);          // dmap.databasescount

//  dmap_add_int(content, "mstc", );          // dmap.utctime
//  dmap_add_int(content, "msto", );          // dmap.utcoffset

  // Create container
  len = evbuffer_get_length(content);
  dmap_add_container(hreq->out_body, "msrv", len);

  CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, content));

  evbuffer_free(content);

  return DAAP_REPLY_OK;
}

static enum daap_reply_result
daap_reply_content_codes(struct httpd_request *hreq)
{
  const struct dmap_field *dmap_fields;
  size_t len;
  int nfields;
  int i;

  dmap_fields = dmap_get_fields_table(&nfields);

  len = 12;
  for (i = 0; i < nfields; i++)
    len += 8 + 12 + 10 + 8 + strlen(dmap_fields[i].desc);

  CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, len + 8));

  dmap_add_container(hreq->out_body, "mccr", len);
  dmap_add_int(hreq->out_body, "mstt", 200);

  for (i = 0; i < nfields; i++)
    {
      len = 12 + 10 + 8 + strlen(dmap_fields[i].desc);

      dmap_add_container(hreq->out_body, "mdcl", len);
      dmap_add_string(hreq->out_body, "mcnm", dmap_fields[i].tag);  /* 12 */
      dmap_add_string(hreq->out_body, "mcna", dmap_fields[i].desc); /* 8 + strlen(desc) */
      dmap_add_short(hreq->out_body, "mcty", dmap_fields[i].type);  /* 10 */
    }

  return DAAP_REPLY_OK;
}

// We don't check credentials here, that's done by daap_request_authorize()
static enum daap_reply_result
daap_reply_login(struct httpd_request *hreq)
{
  struct daap_session *dummy = hreq->extra_data;
  const char *param;
  uint32_t session_id;
  uint32_t request_session_id = 0;
  int ret;

  DPRINTF(E_INFO, L_DAAP, "Client '%s' logging in from %s\n", hreq->user_agent ? hreq->user_agent : "(no user-agent)", hreq->peer_address);

  param = httpd_query_value_find(hreq->query, "request-session-id");
  if (param)
    {
      ret = safe_atou32(param, &request_session_id);
      if (ret < 0)
	DPRINTF(E_LOG, L_DAAP, "Login request where request-session-id is not an integer\n");
    }

  ret = daap_session_add(&session_id, dummy->is_remote, request_session_id);
  if (ret < 0)
    {
      dmap_error_make(hreq->out_body, "mlog", "Could not start session");
      return DAAP_REPLY_ERROR;
    }

  CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 32));

  dmap_add_container(hreq->out_body, "mlog", 24);
  dmap_add_int(hreq->out_body, "mstt", 200);          /* 12 */
  dmap_add_int(hreq->out_body, "mlid", session_id);   /* 12 */

  return DAAP_REPLY_OK;
}

static enum daap_reply_result
daap_reply_logout(struct httpd_request *hreq)
{
  struct daap_session *session = hreq->extra_data;

  if (!session || session->id == 0)
    return DAAP_REPLY_FORBIDDEN;

  daap_session_remove(session->id);

  memset(session, 0, sizeof(struct daap_session));

  return DAAP_REPLY_LOGOUT;
}

static enum daap_reply_result
daap_reply_update(struct httpd_request *hreq)
{
  struct daap_update_request *ur;
  const char *param;
  int reqd_rev;
  int ret;

  if (!hreq->backend)
    {
      DPRINTF(E_LOG, L_DAAP, "Bug! daap_reply_update() cannot be called without an actual connection\n");
      return DAAP_REPLY_NO_CONNECTION;
    }

  param = httpd_query_value_find(hreq->query, "revision-number");
  if (!param)
    {
      DPRINTF(E_DBG, L_DAAP, "Missing revision-number in client update request\n");
      /* Some players (Amarok, Banshee) don't supply a revision number.
	 They get a standard update of everything. */
      param = "1";  /* Default to "1" will ensure an update */
    }

  ret = safe_atoi32(param, &reqd_rev);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Parameter revision-number not an integer\n");

      dmap_error_make(hreq->out_body, "mupd", "Invalid request");
      return DAAP_REPLY_ERROR;
    }

  if (reqd_rev == 1) /* Or revision is not valid */
    {
      CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 32));

      /* Send back current revision */
      dmap_add_container(hreq->out_body, "mupd", 24);
      dmap_add_int(hreq->out_body, "mstt", 200);         /* 12 */
      dmap_add_int(hreq->out_body, "musr", current_rev); /* 12 */

      return DAAP_REPLY_OK;
    }

  /* Else, just let the request hang until we have changes to push back */
  ur = calloc(1, sizeof(struct daap_update_request));
  if (!ur)
    {
      DPRINTF(E_LOG, L_DAAP, "Out of memory for update request\n");

      dmap_error_make(hreq->out_body, "mupd", "Out of memory");
      return DAAP_REPLY_ERROR;
    }

  if (DAAP_UPDATE_REFRESH > 0)
    {
      ur->timeout = evtimer_new(hreq->evbase, update_refresh_cb, ur);
      if (ur->timeout)
	ret = evtimer_add(ur->timeout, &daap_update_refresh_tv);
      else
	ret = -1;

      if (ret < 0)
	{
	  DPRINTF(E_LOG, L_DAAP, "Out of memory for update request event\n");

	  dmap_error_make(hreq->out_body, "mupd", "Could not register timer");	
	  update_free(ur);
	  return DAAP_REPLY_ERROR;
	}
    }

  /* NOTE: we may need to keep reqd_rev in there too */
  ur->hreq = hreq;

  ur->next = update_requests;
  update_requests = ur;

  /* If the connection fails before we have an update to push out
   * to the client, we need to know.
   */
  httpd_request_close_cb_set(hreq, update_fail_cb, ur);

  return DAAP_REPLY_NONE;
}

static enum daap_reply_result
daap_reply_activity(struct httpd_request *hreq)
{
  /* That's so nice, thanks for letting us know */
  return DAAP_REPLY_NO_CONTENT;
}

static enum daap_reply_result
daap_reply_dblist(struct httpd_request *hreq)
{
  struct evbuffer *content;
  struct evbuffer *item;
  char *name;
  char *name_radio;
  size_t len;
  uint32_t count = 0;

  name = cfg_getstr(cfg_getsec(cfg, "library"), "name");
  name_radio = cfg_getstr(cfg_getsec(cfg, "library"), "name_radio");

  CHECK_NULL(L_DAAP, content = evbuffer_new());
  CHECK_NULL(L_DAAP, item = evbuffer_new());
  CHECK_ERR(L_DAAP, evbuffer_expand(item, 512));
  CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 1024));

  // Add db entry for library with dbid = 1
  dmap_add_int(item, "miid", 1);
  dmap_add_long(item, "mper", 1);
  dmap_add_int(item, "mdbk", 1);
  dmap_add_int(item, "aeCs", 1);
  dmap_add_string(item, "minm", name);
  db_files_get_count(&count, NULL, NULL);
  dmap_add_int(item, "mimc", (int)count);
  db_pl_get_count(&count); // TODO Don't count empty smart playlists, because they get excluded in aply
  dmap_add_int(item, "mctc", (int)count);
//  dmap_add_int(content, "aeMk", 0x405);   // com.apple.itunes.extended-media-kind (OR of all in library)
  dmap_add_int(item, "meds", 3);

  // Create container for library db
  len = evbuffer_get_length(item);
  dmap_add_container(content, "mlit", len);

  CHECK_ERR(L_DAAP, evbuffer_add_buffer(content, item));

  // Add second db entry for radio with dbid = DAAP_DB_RADIO
  CHECK_ERR(L_DAAP, evbuffer_expand(item, 512));

  dmap_add_int(item, "miid", DAAP_DB_RADIO);
  dmap_add_long(item, "mper", DAAP_DB_RADIO);
  dmap_add_int(item, "mdbk", 0x64);
  dmap_add_int(item, "aeCs", 0);
  dmap_add_string(item, "minm", name_radio);
  db_pl_get_count(&count); // TODO This counts too much, should only include stream playlists
  dmap_add_int(item, "mimc", (int)count);
  dmap_add_int(item, "mctc", 0);
  dmap_add_int(item, "aeMk", 1);   // com.apple.itunes.extended-media-kind (OR of all in library)
  dmap_add_int(item, "meds", 3);

  // Create container for radio db
  len = evbuffer_get_length(item);
  dmap_add_container(content, "mlit", len);

  CHECK_ERR(L_DAAP, evbuffer_add_buffer(content, item));

  // Create container
  len = evbuffer_get_length(content);
  dmap_add_container(hreq->out_body, "avdb", len + 53);
  dmap_add_int(hreq->out_body, "mstt", 200);     /* 12 */
  dmap_add_char(hreq->out_body, "muty", 0);      /* 9 */
  dmap_add_int(hreq->out_body, "mtco", 2);       /* 12 */
  dmap_add_int(hreq->out_body, "mrco", 2);       /* 12 */
  dmap_add_container(hreq->out_body, "mlcl", len); /* 8 */

  CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, content));

  evbuffer_free(item);
  evbuffer_free(content);

  return DAAP_REPLY_OK;
}

static enum daap_reply_result
daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
{
  struct query_params qp;
  struct db_media_file_info dbmfi;
  struct evbuffer *song;
  struct evbuffer *songlist;
  struct daap_session *s;
  const struct dmap_field **meta = NULL;
  struct sort_ctx *sctx;
  const char *param;
  const char *accept_codecs;
  const char *tag;
  size_t len;
  enum transcode_profile spk_profile;
  enum transcode_profile profile;
  struct transcode_metadata_string xcode_metadata;
  struct media_quality quality = { 0 };
  uint32_t len_ms;
  int nmeta = 0;
  int sort_headers;
  int nsongs;
  int ret;

  DPRINTF(E_DBG, L_DAAP, "Fetching song list for playlist %d\n", playlist);

  s = hreq->extra_data;
  if (!s)
    {
      DPRINTF(E_LOG, L_DAAP, "Bug! daap_reply_songlist_generic() called with NULL session (playlist %d)\n", playlist);
      return DAAP_REPLY_ERROR;
    }

  if (playlist != -1)
    {
      // Songs in playlist
      tag = "apso";
      query_params_set(&qp, &sort_headers, hreq, Q_PLITEMS);
      qp.id = playlist;
    }
  else
    {
      // Songs in database
      tag = "adbs";
      query_params_set(&qp, &sort_headers, hreq, Q_ITEMS);
    }

  CHECK_NULL(L_DAAP, songlist = evbuffer_new());
  CHECK_NULL(L_DAAP, song = evbuffer_new());
  CHECK_NULL(L_DAAP, sctx = daap_sort_context_new());
  CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 61));
  CHECK_ERR(L_DAAP, evbuffer_expand(songlist, 4096));
  CHECK_ERR(L_DAAP, evbuffer_expand(song, 512));

  param = httpd_query_value_find(hreq->query, "meta");
  if (!param)
    {
      DPRINTF(E_DBG, L_DAAP, "No meta parameter in query, using default\n");

      if (playlist != -1)
	param = default_meta_plsongs;
    }

  if (param)
    {
      nmeta = parse_meta(&meta, param);
      if (nmeta < 0)
	{
	  DPRINTF(E_LOG, L_DAAP, "Failed to parse meta parameter in DAAP query\n");
	  goto error;
	}
    }

  ret = db_query_start(&qp);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Could not start query\n");

      dmap_error_make(hreq->out_body, tag, "Could not start query");
      goto error;
    }

  accept_codecs = NULL;
  if (!s->is_remote && hreq->in_headers)
    {
      accept_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
    }

  spk_profile = httpd_xcode_profile_get(hreq);

  DPRINTF(E_DBG, L_DAAP, "Speaker check of '%s' (codecs '%s') returned %d\n", hreq->user_agent, accept_codecs, spk_profile);

  nsongs = 0;
  while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0)
    {
      nsongs++;

      // Not sure if the is_remote path is really needed. Note that if you
      // change the below you might need to do the same in rsp_reply_playlist()
      profile = s->is_remote ? XCODE_WAV : transcode_needed(hreq->user_agent, accept_codecs, dbmfi.codectype);
      if (profile == XCODE_UNKNOWN)
	{
	  DPRINTF(E_LOG, L_DAAP, "Cannot transcode '%s', codec type is unknown\n", dbmfi.fname);
	}
      else if (profile != XCODE_NONE)
	{
	  if (spk_profile != XCODE_NONE)
	    profile = spk_profile;

	  if (safe_atou32(dbmfi.song_length, &len_ms) < 0)
	    len_ms = 3 * 60 * 1000; // just a fallback default

	  safe_atoi32(dbmfi.samplerate, &quality.sample_rate);
	  safe_atoi32(dbmfi.bits_per_sample, &quality.bits_per_sample);
	  safe_atoi32(dbmfi.channels, &quality.channels);
	  quality.bit_rate = cfg_getint(cfg_getsec(cfg, "streaming"), "bit_rate");

	  transcode_metadata_strings_set(&xcode_metadata, profile, &quality, len_ms);
	  dbmfi.type        = xcode_metadata.type;
	  dbmfi.codectype   = xcode_metadata.codectype;
	  dbmfi.description = xcode_metadata.description;
	  dbmfi.file_size   = xcode_metadata.file_size;
	  dbmfi.bitrate     = xcode_metadata.bitrate;
	}

      ret = dmap_encode_file_metadata(songlist, song, &dbmfi, meta, nmeta, sort_headers);
      if (ret < 0)
	{
	  DPRINTF(E_LOG, L_DAAP, "Failed to encode song metadata\n");

	  ret = -100;
	  break;
	}

      if (sort_headers)
	{
	  ret = daap_sort_build(sctx, dbmfi.title_sort);
	  if (ret < 0)
	    {
	      DPRINTF(E_LOG, L_DAAP, "Could not add sort header to DAAP song list reply\n");

	      ret = -100;
	      break;
	    }
   	}

      DPRINTF(E_SPAM, L_DAAP, "Done with song\n");
    }

  DPRINTF(E_DBG, L_DAAP, "Done with song list, %d songs\n", nsongs);

  db_query_end(&qp);

  if (ret == -100)
    {
      dmap_error_make(hreq->out_body, tag, "Out of memory");
      goto error;
    }
  else if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Error fetching results\n");
      dmap_error_make(hreq->out_body, tag, "Error fetching query results");
      goto error;
    }

  /* Add header to evbuf, add songlist to evbuf */
  len = evbuffer_get_length(songlist);
  if (sort_headers)
    {
      daap_sort_finalize(sctx);
      dmap_add_container(hreq->out_body, tag, len + evbuffer_get_length(sctx->headerlist) + 61);
    }
  else
    dmap_add_container(hreq->out_body, tag, len + 53);

  dmap_add_int(hreq->out_body, "mstt", 200);        /* 12 */
  dmap_add_char(hreq->out_body, "muty", 0);         /* 9 */
  dmap_add_int(hreq->out_body, "mtco", qp.results); /* 12 */
  dmap_add_int(hreq->out_body, "mrco", nsongs);     /* 12 */
  dmap_add_container(hreq->out_body, "mlcl", len); /* 8 */

  CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, songlist));

  if (sort_headers)
    {
      len = evbuffer_get_length(sctx->headerlist);
      dmap_add_container(hreq->out_body, "mshl", len); /* 8 */

      CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, sctx->headerlist));
    }

  free(meta);
  daap_sort_context_free(sctx);
  evbuffer_free(song);
  evbuffer_free(songlist);
  free_query_params(&qp, 1);

  return DAAP_REPLY_OK;

 error:
  free(meta);
  daap_sort_context_free(sctx);
  evbuffer_free(song);
  evbuffer_free(songlist);
  free_query_params(&qp, 1);

  return DAAP_REPLY_ERROR;
}

static enum daap_reply_result
daap_reply_dbsonglist(struct httpd_request *hreq)
{
  return daap_reply_songlist_generic(hreq, -1);
}

static enum daap_reply_result
daap_reply_plsonglist(struct httpd_request *hreq)
{
  int playlist;
  int ret;

  ret = safe_atoi32(hreq->path_parts[3], &playlist);
  if (ret < 0)
    {
      dmap_error_make(hreq->out_body, "apso", "Invalid playlist ID");
      return DAAP_REPLY_ERROR;
    }

  // This is a work-around for Remote for iTunes that for unknown reasons
  // sometimes requests playlist 0
  if (playlist == 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Client '%s' made invalid request for playlist 0, returning playlist 1\n", hreq->user_agent);

      playlist = 1;
    }

  return daap_reply_songlist_generic(hreq, playlist);
}

static enum daap_reply_result
daap_reply_playlists(struct httpd_request *hreq)
{
  struct query_params qp;
  struct db_playlist_info dbpli;
  struct evbuffer *playlistlist;
  struct evbuffer *playlist;
  const struct dmap_field_map *dfm;
  const struct dmap_field *df;
  const struct dmap_field **meta = NULL;
  const char *param;
  char **strval;
  size_t len;
  int database;
  int cfg_radiopl;
  int nmeta;
  int npls;
  int32_t plid;
  int32_t pltype;
  int32_t plitems;
  int32_t plstreams;
  int32_t plparent;
  int i;
  int ret;

  cfg_radiopl = cfg_getbool(cfg_getsec(cfg, "library"), "radio_playlists");

  ret = safe_atoi32(hreq->path_parts[1], &database);
  if (ret < 0)
    {
      dmap_error_make(hreq->out_body, "aply", "Invalid database ID");
      return DAAP_REPLY_ERROR;
    }

  query_params_set(&qp, NULL, hreq, Q_PL);
  qp.sort = S_PLAYLIST; // Only S_PLAYLIST (and S_NONE) works for Q_PL

  CHECK_NULL(L_DAAP, playlistlist = evbuffer_new());
  CHECK_NULL(L_DAAP, playlist = evbuffer_new());
  CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 61));
  CHECK_ERR(L_DAAP, evbuffer_expand(playlistlist, 1024));
  CHECK_ERR(L_DAAP, evbuffer_expand(playlist, 128));

  param = httpd_query_value_find(hreq->query, "meta");
  if (!param)
    {
      DPRINTF(E_LOG, L_DAAP, "No meta parameter in query, using default\n");

      param = default_meta_pl;
    }

  nmeta = parse_meta(&meta, param);
  if (nmeta < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Failed to parse meta parameter in DAAP query\n");

      dmap_error_make(hreq->out_body, "aply", "Failed to parse query");
      goto error;
    }

  ret = db_query_start(&qp);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Could not start query\n");

      dmap_error_make(hreq->out_body, "aply", "Could not start query");
      goto error;
    }

  npls = 0;
  while (((ret = db_query_fetch_pl(&dbpli, &qp)) == 0) && (dbpli.id))
    {
      plid = 1;
      if (safe_atoi32(dbpli.id, &plid) != 0)
	continue;

      pltype = 0;
      if (safe_atoi32(dbpli.type, &pltype) != 0)
	continue;

      plitems = 0;
      if (safe_atoi32(dbpli.items, &plitems) != 0)
	continue;

      plstreams = 0;
      if (safe_atoi32(dbpli.streams, &plstreams) != 0)
	continue;

      /* Database DAAP_DB_RADIO is radio, so for that db skip playlists without
       * streams and for other databases skip playlists which are just streams
       */
      if ((database == DAAP_DB_RADIO) && (plstreams == 0))
	continue;
      if (!cfg_radiopl && (database != DAAP_DB_RADIO) && (plstreams > 0) && (plstreams == plitems))
	continue;

      /* Don't add empty Special playlists */
      if ((plid > 1) && (plitems == 0) && (pltype == PL_SPECIAL))
	continue;

      npls++;

      for (i = 0; i < nmeta; i++)
	{
	  df = meta[i];
	  dfm = df->dfm;

	  /* dmap.itemcount - always added */
	  if (dfm == &dfm_dmap_mimc)
	    continue;

	  /* Add field "com.apple.itunes.smart-playlist" for special and smart playlists
	     (excluding the special playlist for "library" with id = 1) */
	  if (dfm == &dfm_dmap_aeSP)
	    {
	      if ((pltype == PL_SMART) || ((pltype == PL_SPECIAL) && (plid != 1)))
		{
		  dmap_add_char(playlist, "aeSP", 1);
		}

	      /* Add field "com.apple.itunes.special-playlist" for special playlists
		 (excluding the special playlist for "library" with id = 1) */
	      if ((pltype == PL_SPECIAL) && (plid != 1))
		{
		  int32_t aePS = 0;
		  ret = safe_atoi32(dbpli.special_id, &aePS);
		  if ((ret == 0) && (aePS > 0))
		    dmap_add_char(playlist, "aePS", aePS);
		}

	      continue;
	    }

	  /* Not in struct playlist_info */
	  if (dfm->pli_offset < 0)
	    continue;

          strval = (char **) ((char *)&dbpli + dfm->pli_offset);

          if (!(*strval) || (**strval == '\0'))
            continue;

	  dmap_add_field(playlist, df, *strval, 0);

	  DPRINTF(E_SPAM, L_DAAP, "Done with meta tag %s (%s)\n", df->desc, *strval);
	}

      /* Item count (mimc) */
      dmap_add_int(playlist, "mimc", plitems);

      /* Container ID (mpco) */
      ret = safe_atoi32(dbpli.parent_id, &plparent);
      if (ret == 0)
	dmap_add_int(playlist, "mpco", plparent);
      else
	dmap_add_int(playlist, "mpco", 0);

      /* Base playlist (abpl), id = 1 */
      if (plid == 1)
	dmap_add_char(playlist, "abpl", 1);

      DPRINTF(E_SPAM, L_DAAP, "Done with playlist\n");

      len = evbuffer_get_length(playlist);
      dmap_add_container(playlistlist, "mlit", len);
      ret = evbuffer_add_buffer(playlistlist, playlist);
      if (ret < 0)
	{
	  DPRINTF(E_LOG, L_DAAP, "Could not add playlist to playlist list for DAAP playlists reply\n");

	  ret = -100;
	  break;
	}
    }

  db_query_end(&qp);

  DPRINTF(E_DBG, L_DAAP, "Done with playlist list, %d playlists\n", npls);

  if (ret == -100)
    {
      dmap_error_make(hreq->out_body, "aply", "Out of memory");
      goto error;
    }
  else if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Error fetching results\n");
      dmap_error_make(hreq->out_body, "aply", "Error fetching query results");
      goto error;
    }

  /* Add header to evbuf, add playlistlist to evbuf */
  len = evbuffer_get_length(playlistlist);
  dmap_add_container(hreq->out_body, "aply", len + 53);
  dmap_add_int(hreq->out_body, "mstt", 200); /* 12 */
  dmap_add_char(hreq->out_body, "muty", 0);  /* 9 */
  dmap_add_int(hreq->out_body, "mtco", qp.results); /* 12 */
  dmap_add_int(hreq->out_body,"mrco", npls); /* 12 */
  dmap_add_container(hreq->out_body, "mlcl", len);

  CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, playlistlist));

  free(meta);
  evbuffer_free(playlist);
  evbuffer_free(playlistlist);
  free_query_params(&qp, 1);

  return DAAP_REPLY_OK;

 error:
  free(meta);
  evbuffer_free(playlist);
  evbuffer_free(playlistlist);
  free_query_params(&qp, 1);

  return DAAP_REPLY_ERROR;
}

static enum daap_reply_result
daap_reply_groups(struct httpd_request *hreq)
{
  struct query_params qp;
  struct db_group_info dbgri;
  struct evbuffer *group;
  struct evbuffer *grouplist;
  const struct dmap_field_map *dfm;
  const struct dmap_field *df;
  const struct dmap_field **meta = NULL;
  struct sort_ctx *sctx;
  cfg_t *lib;
  const char *param;
  char **strval;
  char *tag;
  size_t len;
  int nmeta;
  int sort_headers;
  int ngrp;
  int32_t val;
  int i;
  int ret;

  param = httpd_query_value_find(hreq->query, "group-type");
  if (param && strcmp(param, "artists") == 0)
    {
      // Request from Remote may have the form:
      //  groups?meta=dmap.xxx,dma...&type=music&group-type=artists&sort=album&include-sort-headers=1&query=('...')&session-id=...
      // Note: Since grouping by artist and sorting by album is crazy we override
      tag = "agar";
      query_params_set(&qp, &sort_headers, hreq, Q_GROUP_ARTISTS);
      qp.sort = S_ARTIST;
    }
  else
    {
      // Request from Remote may have the form:
      //  groups?meta=dmap.xxx,dma...&type=music&group-type=albums&sort=artist&include-sort-headers=0&query=('...'))&session-id=...
      // Sort may also be 'album'
      tag = "agal";
      query_params_set(&qp, &sort_headers, hreq, Q_GROUP_ALBUMS);
      if (qp.sort == S_NONE)
	qp.sort = S_ALBUM;
    }

  CHECK_NULL(L_DAAP, grouplist = evbuffer_new());
  CHECK_NULL(L_DAAP, group = evbuffer_new());
  CHECK_NULL(L_DAAP, sctx = daap_sort_context_new());
  CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 61));
  CHECK_ERR(L_DAAP, evbuffer_expand(grouplist, 1024));
  CHECK_ERR(L_DAAP, evbuffer_expand(group, 128));

  param = httpd_query_value_find(hreq->query, "meta");
  if (!param)
    {
      DPRINTF(E_LOG, L_DAAP, "No meta parameter in query, using default\n");

      param = default_meta_group;
    }

  nmeta = parse_meta(&meta, param);
  if (nmeta < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Failed to parse meta parameter in DAAP query\n");

      dmap_error_make(hreq->out_body, tag, "Failed to parse query");
      goto error;
    }

  ret = db_query_start(&qp);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Could not start query\n");

      dmap_error_make(hreq->out_body, tag, "Could not start query");
      goto error;
    }

  ngrp = 0;
  while ((ret = db_query_fetch_group(&dbgri, &qp)) == 0)
    {
      /* Don't add item if no name (eg blank album name) */
      if (strlen(dbgri.itemname) == 0)
	continue;

      /* Don't add single item albums/artists if configured to hide */
      lib = cfg_getsec(cfg, "library");
      if (cfg_getbool(lib, "hide_singles") && (strcmp(dbgri.itemcount, "1") == 0))
	continue;

      ngrp++;

      for (i = 0; i < nmeta; i++)
	{
	  df = meta[i];
	  if (!df)
	    continue;

	  dfm = df->dfm;

	  /* dmap.itemcount - always added */
	  if (dfm == &dfm_dmap_mimc)
	    continue;

	  /* Not in struct group_info */
	  if (dfm->gri_offset < 0)
	    continue;

          strval = (char **) ((char *)&dbgri + dfm->gri_offset);

          if (!(*strval) || (**strval == '\0'))
            continue;

	  dmap_add_field(group, df, *strval, 0);

	  DPRINTF(E_SPAM, L_DAAP, "Done with meta tag %s (%s)\n", df->desc, *strval);
	}

      if (sort_headers)
	{
	  ret = daap_sort_build(sctx, dbgri.itemname_sort);
	  if (ret < 0)
	    {
	      DPRINTF(E_LOG, L_DAAP, "Could not add sort header to DAAP groups reply\n");

	      ret = -100;
	      break;
	    }
	}

      /* Item count, always added (mimc) */
      val = 0;
      ret = safe_atoi32(dbgri.itemcount, &val);
      if ((ret == 0) && (val > 0))
	dmap_add_int(group, "mimc", val);

      /* Song album artist (asaa), always added if group-type is albums  */
      if (qp.type == Q_GROUP_ALBUMS)
        dmap_add_string(group, "asaa", dbgri.songalbumartist);

      /* Item id (miid) */
      val = 0;
      ret = safe_atoi32(dbgri.id, &val);
      if ((ret == 0) && (val > 0))
	dmap_add_int(group, "miid", val);

      DPRINTF(E_SPAM, L_DAAP, "Done with group\n");

      len = evbuffer_get_length(group);
      dmap_add_container(grouplist, "mlit", len);
      ret = evbuffer_add_buffer(grouplist, group);
      if (ret < 0)
	{
	  DPRINTF(E_LOG, L_DAAP, "Could not add group to group list for DAAP groups reply\n");

	  ret = -100;
	  break;
	}
    }

  db_query_end(&qp);

  DPRINTF(E_DBG, L_DAAP, "Done with group list, %d groups\n", ngrp);

  if (ret == -100)
    {
      dmap_error_make(hreq->out_body, tag, "Out of memory");
      goto error;
    }
  else if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Error fetching results\n");
      dmap_error_make(hreq->out_body, tag, "Error fetching query results");
      goto error;
    }

  /* Add header to evbuf, add grouplist to evbuf */
  len = evbuffer_get_length(grouplist);
  if (sort_headers)
    {
      daap_sort_finalize(sctx);
      dmap_add_container(hreq->out_body, tag, len + evbuffer_get_length(sctx->headerlist) + 61);
    }
  else
    dmap_add_container(hreq->out_body, tag, len + 53);

  dmap_add_int(hreq->out_body, "mstt", 200);        /* 12 */
  dmap_add_char(hreq->out_body, "muty", 0);         /* 9 */
  dmap_add_int(hreq->out_body, "mtco", qp.results); /* 12 */
  dmap_add_int(hreq->out_body,"mrco", ngrp);        /* 12 */
  dmap_add_container(hreq->out_body, "mlcl", len);  /* 8 */

  CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, grouplist));

  if (sort_headers)
    {
      len = evbuffer_get_length(sctx->headerlist);
      dmap_add_container(hreq->out_body, "mshl", len); /* 8 */

      CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, sctx->headerlist));
    }

  free(meta);
  daap_sort_context_free(sctx);
  evbuffer_free(group);
  evbuffer_free(grouplist);
  free_query_params(&qp, 1);

  return DAAP_REPLY_OK;

 error:
  free(meta);
  daap_sort_context_free(sctx);
  evbuffer_free(group);
  evbuffer_free(grouplist);
  free_query_params(&qp, 1);

  return DAAP_REPLY_ERROR;
}

static enum daap_reply_result
daap_reply_browse(struct httpd_request *hreq)
{
  struct query_params qp;
  struct evbuffer *itemlist;
  struct sort_ctx *sctx;
  char *browse_item;
  char *sort_item;
  char *tag;
  size_t len;
  int sort_headers;
  int nitems;
  int ret;

  if (strcmp(hreq->path_parts[3], "artists") == 0)
    {
      tag = "abar";
      query_params_set(&qp, &sort_headers, hreq, Q_BROWSE_ARTISTS);
    }
  else if (strcmp(hreq->path_parts[3], "albums") == 0)
    {
      tag = "abal";
      query_params_set(&qp, &sort_headers, hreq, Q_BROWSE_ALBUMS);
    }
  else if (strcmp(hreq->path_parts[3], "genres") == 0)
    {
      tag = "abgn";
      query_params_set(&qp, &sort_headers, hreq, Q_BROWSE_GENRES);
    }
  else if (strcmp(hreq->path_parts[3], "composers") == 0)
    {
      tag = "abcp";
      query_params_set(&qp, &sort_headers, hreq, Q_BROWSE_COMPOSERS);
    }
  else
    {
      DPRINTF(E_LOG, L_DAAP, "Invalid DAAP browse request type '%s'\n", hreq->path_parts[3]);
      dmap_error_make(hreq->out_body, "abro", "Invalid browse type");
      return DAAP_REPLY_ERROR;
    }

  CHECK_NULL(L_DAAP, itemlist = evbuffer_new());
  CHECK_NULL(L_DAAP, sctx = daap_sort_context_new());
  CHECK_ERR(L_DAAP, evbuffer_expand(hreq->out_body, 52));
  CHECK_ERR(L_DAAP, evbuffer_expand(itemlist, 1024)); // Just a starting alloc, it'll expand as needed

  ret = db_query_start(&qp);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Could not start query\n");

      dmap_error_make(hreq->out_body, "abro", "Could not start query");
      goto error;
    }

  nitems = 0;
  while (((ret = db_query_fetch_string_sort(&browse_item, &sort_item, &qp)) == 0) && (browse_item))
    {
      nitems++;

      if (sort_headers)
	{
	  ret = daap_sort_build(sctx, sort_item);
	  if (ret < 0)
	    {
	      DPRINTF(E_LOG, L_DAAP, "Could not add sort header to DAAP browse reply\n");
	      break;
	    }
	}

      dmap_add_string(itemlist, "mlit", browse_item);
    }

  db_query_end(&qp);

  if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Error fetching/building results\n");

      dmap_error_make(hreq->out_body, "abro", "Error fetching/building query results");
      goto error;
    }

  len = evbuffer_get_length(itemlist);
  if (sort_headers)
    {
      daap_sort_finalize(sctx);
      dmap_add_container(hreq->out_body, "abro", len + evbuffer_get_length(sctx->headerlist) + 52);
    }
  else
    dmap_add_container(hreq->out_body, "abro", len + 44);

  dmap_add_int(hreq->out_body, "mstt", 200);        /* 12 */
  dmap_add_int(hreq->out_body, "mtco", qp.results); /* 12 */
  dmap_add_int(hreq->out_body, "mrco", nitems);     /* 12 */
  dmap_add_container(hreq->out_body, tag, len);     /* 8 */

  CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, itemlist));

  if (sort_headers)
    {
      len = evbuffer_get_length(sctx->headerlist);
      dmap_add_container(hreq->out_body, "mshl", len); /* 8 */

      CHECK_ERR(L_DAAP, evbuffer_add_buffer(hreq->out_body, sctx->headerlist));
    }

  daap_sort_context_free(sctx);
  evbuffer_free(itemlist);
  free_query_params(&qp, 1);

  return DAAP_REPLY_OK;

 error:
  daap_sort_context_free(sctx);
  evbuffer_free(itemlist);
  free_query_params(&qp, 1);

  return DAAP_REPLY_ERROR;
}

/* NOTE: We only handle artwork at the moment */
static enum daap_reply_result
daap_reply_extra_data(struct httpd_request *hreq)
{
  char clen[32];
  const char *param;
  char *ctype;
  size_t len;
  int id;
  int max_w;
  int max_h;
  int ret;

  if (!hreq->backend)
    {
      DPRINTF(E_LOG, L_DAAP, "Bug! daap_reply_extra_data() cannot be called without an actual connection\n");
      return DAAP_REPLY_NO_CONNECTION;
    }

  ret = safe_atoi32(hreq->path_parts[3], &id);
  if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Could not convert id parameter to integer: '%s'\n", hreq->path_parts[3]);
      return DAAP_REPLY_BAD_REQUEST;
    }

  if (httpd_query_value_find(hreq->query, "mw") && httpd_query_value_find(hreq->query, "mh"))
    {
      param = httpd_query_value_find(hreq->query, "mw");
      ret = safe_atoi32(param, &max_w);
      if (ret < 0)
	{
	  DPRINTF(E_LOG, L_DAAP, "Could not convert mw parameter to integer: '%s'\n", param);
	  return DAAP_REPLY_BAD_REQUEST;
	}

      param = httpd_query_value_find(hreq->query, "mh");
      ret = safe_atoi32(param, &max_h);
      if (ret < 0)
	{
	  DPRINTF(E_LOG, L_DAAP, "Could not convert mh parameter to integer: '%s'\n", param);
	  return DAAP_REPLY_BAD_REQUEST;
	}
    }
  else
    {
      DPRINTF(E_DBG, L_DAAP, "Request for artwork without mw or mh parameter\n");

      max_w = 0;
      max_h = 0;
    }

  if (strcmp(hreq->path_parts[2], "groups") == 0)
    ret = artwork_get_by_group_id(hreq->out_body, id, max_w, max_h, 0);
  else if (strcmp(hreq->path_parts[2], "items") == 0)
    ret = artwork_get_by_file_id(hreq->out_body, id, max_w, max_h, 0);

  len = evbuffer_get_length(hreq->out_body);

  switch (ret)
    {
      case ART_FMT_PNG:
	ctype = "image/png";
	break;

      case ART_FMT_JPEG:
	ctype = "image/jpeg";
	break;

      default:
	if (len > 0)
	  evbuffer_drain(hreq->out_body, len);

	goto no_artwork;
    }

  httpd_header_remove(hreq->out_headers, "Content-Type");
  httpd_header_add(hreq->out_headers, "Content-Type", ctype);
  snprintf(clen, sizeof(clen), "%ld", (long)len);
  httpd_header_add(hreq->out_headers, "Content-Length", clen);

  return DAAP_REPLY_OK_NO_GZIP;

 no_artwork:
  return DAAP_REPLY_NO_CONTENT;
}

static enum daap_reply_result
daap_stream(struct httpd_request *hreq)
{
  int id;
  int ret;

  if (!hreq->backend)
    {
      DPRINTF(E_LOG, L_DAAP, "Bug! daap_stream() cannot be called without an actual connection\n");
      return DAAP_REPLY_NO_CONNECTION;
    }

  ret = safe_atoi32(hreq->path_parts[3], &id);
  if (ret < 0)
    return DAAP_REPLY_BAD_REQUEST;

  httpd_stream_file(hreq, id);

  return DAAP_REPLY_NONE;
}

#ifdef DMAP_TEST
static const struct dmap_field dmap_TEST = { "test.container", "TEST", NULL, DMAP_TYPE_LIST };
static const struct dmap_field dmap_TST1 = { "test.ubyte",     "TST1", NULL, DMAP_TYPE_UBYTE };
static const struct dmap_field dmap_TST2 = { "test.byte",      "TST2", NULL, DMAP_TYPE_BYTE };
static const struct dmap_field dmap_TST3 = { "test.ushort",    "TST3", NULL, DMAP_TYPE_USHORT };
static const struct dmap_field dmap_TST4 = { "test.short",     "TST4", NULL, DMAP_TYPE_SHORT };
static const struct dmap_field dmap_TST5 = { "test.uint",      "TST5", NULL, DMAP_TYPE_UINT };
static const struct dmap_field dmap_TST6 = { "test.int",       "TST6", NULL, DMAP_TYPE_INT };
static const struct dmap_field dmap_TST7 = { "test.ulong",     "TST7", NULL, DMAP_TYPE_ULONG };
static const struct dmap_field dmap_TST8 = { "test.long",      "TST8", NULL, DMAP_TYPE_LONG };
static const struct dmap_field dmap_TST9 = { "test.string",    "TST9", NULL, DMAP_TYPE_STRING };

static enum daap_reply_result
daap_reply_dmap_test(struct httpd_request *hreq)
{
  struct evbuffer *test;
  char buf[64];
  int ret;

  CHECK_NULL(L_DAAP, test = evbuffer_new());

  /* UBYTE */
  snprintf(buf, sizeof(buf), "%" PRIu8, UINT8_MAX);
  dmap_add_field(test, &dmap_TST1, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);

  /* BYTE */
  snprintf(buf, sizeof(buf), "%" PRIi8, INT8_MIN);
  dmap_add_field(test, &dmap_TST2, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);
  snprintf(buf, sizeof(buf), "%" PRIi8, INT8_MAX);
  dmap_add_field(test, &dmap_TST2, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);

  /* USHORT */
  snprintf(buf, sizeof(buf), "%" PRIu16, UINT16_MAX);
  dmap_add_field(test, &dmap_TST3, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);

  /* SHORT */
  snprintf(buf, sizeof(buf), "%" PRIi16, INT16_MIN);
  dmap_add_field(test, &dmap_TST4, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);
  snprintf(buf, sizeof(buf), "%" PRIi16, INT16_MAX);
  dmap_add_field(test, &dmap_TST4, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);

  /* UINT */
  snprintf(buf, sizeof(buf), "%" PRIu32, UINT32_MAX);
  dmap_add_field(test, &dmap_TST5, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);

  /* INT */
  snprintf(buf, sizeof(buf), "%" PRIi32, INT32_MIN);
  dmap_add_field(test, &dmap_TST6, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);
  snprintf(buf, sizeof(buf), "%" PRIi32, INT32_MAX);
  dmap_add_field(test, &dmap_TST6, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);

  /* ULONG */
  snprintf(buf, sizeof(buf), "%" PRIu64, UINT64_MAX);
  dmap_add_field(test, &dmap_TST7, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);

  /* LONG */
  snprintf(buf, sizeof(buf), "%" PRIi64, INT64_MIN);
  dmap_add_field(test, &dmap_TST8, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);
  snprintf(buf, sizeof(buf), "%" PRIi64, INT64_MAX);
  dmap_add_field(test, &dmap_TST8, buf, 0);
  dmap_add_field(test, &dmap_TST9, buf, 0);

  dmap_add_container(hreq->out_body, dmap_TEST.tag, evbuffer_get_length(test));

  ret = evbuffer_add_buffer(hreq->out_body, test);
  evbuffer_free(test);

  if (ret < 0)
    {
      DPRINTF(E_LOG, L_DAAP, "Could not add test results to DMAP test reply\n");

      dmap_error_make(hreq->out_body, dmap_TEST.tag, "Out of memory");
      return DAAP_REPLY_ERROR;
    }

  return DAAP_REPLY_OK_NO_GZIP;
}
#endif /* DMAP_TEST */

static struct httpd_uri_map daap_handlers[] =
  {
    {
      .regexp = "^/server-info$",
      .handler = daap_reply_server_info
    },
    {
      .regexp = "^/content-codes$",
      .handler = daap_reply_content_codes
    },
    {
      .regexp = "^/login$",
      .handler = daap_reply_login
    },
    {
      .regexp = "^/logout$",
      .handler = daap_reply_logout
    },
    {
      .regexp = "^/update$",
      .handler = daap_reply_update,
    },
    {
      .regexp = "^/activity$",
      .handler = daap_reply_activity
    },
    {
      .regexp = "^/databases$",
      .handler = daap_reply_dblist
    },
    {
      .regexp = "^/databases/[[:digit:]]+/browse/[^/]+$",
      .handler = daap_reply_browse
    },
    {
      .regexp = "^/databases/[[:digit:]]+/items$",
      .handler = daap_reply_dbsonglist
    },
    {
      .regexp = "^/databases/[[:digit:]]+/items/[[:digit:]]+[.][^/]+$",
      .handler = daap_stream
    },
    {
      .regexp = "^/databases/[[:digit:]]+/items/[[:digit:]]+/extra_data/artwork$",
      .handler = daap_reply_extra_data
    },
    {
      .regexp = "^/databases/[[:digit:]]+/containers$",
      .handler = daap_reply_playlists
    },
    {
      .regexp = "^/databases/[[:digit:]]+/containers/[[:digit:]]+/items$",
      .handler = daap_reply_plsonglist
    },
    {
      .regexp = "^/databases/[[:digit:]]+/groups$",
      .handler = daap_reply_groups
    },
    {
      .regexp = "^/databases/[[:digit:]]+/groups/[[:digit:]]+/extra_data/artwork$",
      .handler = daap_reply_extra_data
    },
#ifdef DMAP_TEST
    {
      .regexp = "^/dmap-test$",
      .handler = daap_reply_dmap_test
    },
#endif /* DMAP_TEST */
    {
      .regexp = NULL,
      .handler = NULL
    }
  };


/* ------------------------------- DAAP API --------------------------------- */

/* iTunes 9 gives us an absolute request-uri like
 *  daap://10.1.1.20:3689/server-info
 * iTunes 12.1 gives us an absolute request-uri for streaming like
 *  http://10.1.1.20:3689/databases/1/items/1.mp3
 */
static void
daap_request(struct httpd_request *hreq)
{
  struct timespec start;
  struct timespec end;
  struct daap_session session;
  const char *param;
  uint32_t id;
  int ret;
  int msec;

  if (!hreq->handler)
    {
      DPRINTF(E_LOG, L_DAAP, "Unrecognized path in DAAP request: '%s'\n", hreq->uri);
      daap_reply_send(hreq, DAAP_REPLY_BAD_REQUEST);
      return;
    }

  // Check if we have a session and point hreq->extra_data to it
  param = httpd_query_value_find(hreq->query, "session-id");
  memset(&session, 0, sizeof(struct daap_session));
  session.is_remote = (httpd_query_value_find(hreq->query, "pairing-guid") != NULL);

  if (param)
    {
      ret = safe_atou32(param, &id);
      if (ret < 0)
	DPRINTF(E_LOG, L_DAAP, "Ignoring non-numeric session id in DAAP request: '%s'\n", hreq->uri);
      else
	daap_session_copy(&session, id);
    }

  // Handlers only need a request-local session snapshot
  hreq->extra_data = &session;

  if (strcmp(hreq->path, "/server-info") != 0 && strcmp(hreq->path, "/content-codes") != 0)
    {
      ret = daap_request_authorize(hreq);
      if (ret < 0)
	return; // daap_request_authorize will have sent a response
    }

  // Set reply headers
  httpd_header_add(hreq->out_headers, "Accept-Ranges", "bytes");
  httpd_header_add(hreq->out_headers, "DAAP-Server", PACKAGE_NAME "/" VERSION);
  // Content-Type for all replies, even the actual audio streaming. Note that
  // video streaming will override this Content-Type with a more appropriate
  // video/<type> Content-Type as expected by clients like Front Row.
  httpd_header_add(hreq->out_headers, "Content-Type", "application/x-dmap-tagged");

  // Try the cache
  ret = cache_daap_get(hreq->out_body, hreq->uri);
  if (ret == 0)
    {
      // The cache will return the data gzipped, so httpd_send_reply won't need to do it
      httpd_header_add(hreq->out_headers, "Content-Encoding", "gzip");
      httpd_send_reply(hreq, HTTP_OK, "OK", HTTPD_SEND_NO_GZIP); // TODO not all want this reply
      return;
    }

  // No dice, let's call the handler so it can construct a reply and then send it (note that the reply may be an error)
  clock_gettime(CLOCK_MONOTONIC, &start);

  ret = hreq->handler(hreq);

  clock_gettime(CLOCK_MONOTONIC, &end);
  msec = (end.tv_sec * 1000 + end.tv_nsec / 1000000) - (start.tv_sec * 1000 + start.tv_nsec / 1000000);

  DPRINTF(E_DBG, L_DAAP, "DAAP request handled in %d milliseconds\n", msec);

  if (ret == DAAP_REPLY_OK && msec > cache_daap_threshold_get() && hreq->user_agent)
    cache_daap_add(hreq->uri, hreq->user_agent, ((struct daap_session *)hreq->extra_data)->is_remote, msec);

  daap_reply_send(hreq, ret); // hreq is deallocted
}

int
daap_session_is_valid(uint32_t id)
{
  return daap_session_touch(id) ? 1 : 0;
}

// Thread: Cache
struct evbuffer *
daap_reply_build(const char *uri, const char *user_agent, int is_remote)
{
  struct httpd_request *hreq;
  struct evbuffer *out_body = NULL;
  struct daap_session session;
  int ret;

  DPRINTF(E_DBG, L_DAAP, "Building reply for DAAP request: '%s'\n", uri);

  hreq = httpd_request_new(NULL, NULL, uri, user_agent);
  if (!hreq)
    {
      DPRINTF(E_LOG, L_DAAP, "Error building request: '%s'\n", uri);
      goto out;
    }

  httpd_request_handler_set(hreq);
  if (!hreq->handler)
    {
      DPRINTF(E_LOG, L_DAAP, "Cannot build reply, unrecognized path in request: '%s'\n", uri);
      goto out;
    }

  memset(&session, 0, sizeof(struct daap_session));
  session.is_remote = (bool)is_remote;

  hreq->extra_data = &session;

  ret = hreq->handler(hreq);
  if (ret < 0)
    {
      goto out;
    }

  // Take ownership of the reply
  out_body = hreq->out_body;
  hreq->out_body = NULL;

 out:
  httpd_request_free(hreq);

  return out_body;
}

static int
daap_init(void)
{
  srand((unsigned)time(NULL));
  current_rev = 2;
  CHECK_ERR(L_DAAP, mutex_init(&daap_session_lck));

  return 0;
}

static void
daap_deinit(void)
{
  struct daap_session *s;
  struct daap_update_request *ur;

  CHECK_ERR(L_DAAP, pthread_mutex_lock(&daap_session_lck));
  for (s = daap_sessions; daap_sessions; s = daap_sessions)
    {
      daap_sessions = s->next;
      daap_session_free(s);
    }
  CHECK_ERR(L_DAAP, pthread_mutex_unlock(&daap_session_lck));
  CHECK_ERR(L_DAAP, pthread_mutex_destroy(&daap_session_lck));

  for (ur = update_requests; update_requests; ur = update_requests)
    {
      update_requests = ur->next;

      daap_reply_send(ur->hreq, DAAP_REPLY_SERVUNAVAIL);
      update_free(ur);
    }
}

struct httpd_module httpd_daap =
{
  .name = "DAAP",
  .type = MODULE_DAAP,
  .logdomain = L_DAAP,
  .subpaths = { "/databases/", NULL },
#ifdef DMAP_TEST
  .fullpaths = { "/databases", "/server-info", "/content-codes", "/login", "/update", "/activity", "/logout", "/dmap-test", NULL },
#else
  .fullpaths = { "/databases", "/server-info", "/content-codes", "/login", "/update", "/activity", "/logout", NULL },
#endif
  .handlers = daap_handlers,
  .init = daap_init,
  .deinit = daap_deinit,
  .request = daap_request,
};
