[SCM] forked-daapd/master: Imported Upstream version 22.0

rbalint at users.alioth.debian.org rbalint at users.alioth.debian.org
Fri Oct 17 07:01:17 UTC 2014


The following commit has been merged in the master branch:
commit 712fc7a3609d99366962cd8c05ed6dfce5a5a942
Author: Balint Reczey <balint at balintreczey.hu>
Date:   Thu Oct 16 23:28:13 2014 +0200

    Imported Upstream version 22.0

diff --git a/ChangeLog b/ChangeLog
index c08e13e..287167d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,19 @@
 ChangeLog for forked-daapd
 --------------------------
 
+version 22.0:
+	- queue handling improvements
+	- added DAAP cache, good for low-power devices like the RPi
+	- support for LastFM scrobbling
+	- support for .pls playlists
+	- added compability with ffmpeg libswresample
+	- added options to modify the operation of the SQLite database
+	- stop init-rescan/full-rescan from running twice
+	- fix misc local audio problems
+	- fix some FreeBSD sound timing problems
+	- fix segfault on invalid utf8 while sorting
+	- fix misc bugs
+
 version 21.0:
 	- filescanner performance enhancements (db transactions)
 	- support for queue editing
diff --git a/INSTALL b/INSTALL
index f53b6ba..470a6a6 100644
--- a/INSTALL
+++ b/INSTALL
@@ -29,6 +29,8 @@ sudo apt-get install \
 Depending on the version of libav/ffmpeg in your distribution you may also need
 libavresample-dev.
 
+To build with LastFM support, you should also install libcurl4-openssl-dev.
+
 Note that while forked-daapd will work with versions of libevent between 2.0.0
 and 2.1.3, it is recommended to use either libevent 1 or 2.1.4+. Otherwise you
 will not have support for Shoutcast metadata and simultaneous streaming to
@@ -108,6 +110,8 @@ Libraries:
         from <http://github.com/JonathanBeck/libplist/downloads>
  - libspotify (optional - Spotify support)
         from <https://developer.spotify.com>
+ - libcurl (optional - LastFM support)
+        from <http://curl.haxx.se/libcurl/>
 
 If using binary packages, remember that you need the development packages to
 build forked-daapd (usually named -dev or -devel).
@@ -161,6 +165,9 @@ though you compiled with --enable-spotify, the executable will still be able
 to run on systems without libspotify (the Spotify features will then be 
 disabled).
 
+Support for LastFM scrobbling is optional. Use --enable-lastfm to enable this
+feature.
+
 Support for iTunes Music Library XML format is optional. Use --enable-itunes
 to enable this feature.
 
diff --git a/README b/README
index 2f4cbb6..cad4bb2 100644
--- a/README
+++ b/README
@@ -4,7 +4,7 @@ forked-daapd
 forked-daapd is a Linux/FreeBSD DAAP (iTunes) and RSP (Roku) media server.
 
 It has support for AirPlay devices/speakers, Apple Remote (and compatibles),
-internet radio and Spotify.
+internet radio, Spotify and LastFM. It does not support AirPlay video.
 
 DAAP stands for Digital Audio Access Protocol, and is the protocol used
 by iTunes and friends to share/stream media libraries over the network.
@@ -26,6 +26,7 @@ forked-daapd is a complete rewrite of mt-daapd (Firefly Media Server).
 Contents of this README
 -----------------------
 
+- Getting started
 - Supported clients
 - Using Remote
 - AirPlay devices/speakers
@@ -37,6 +38,21 @@ Contents of this README
 - Library
 - Command line and web interface
 - Spotify
+- LastFM
+
+
+Getting started
+---------------
+
+After installation (see INSTALL) do the following:
+
+ 1. Edit the configuration file (usually /etc/forked-daapd.conf) to suit your
+    needs
+ 2. Start or restart the server (usually /etc/init.d/forked-daapd restart)
+ 3. Wait for the library scan to complete. You can follow the progress with
+    tail -f /var/log/forked-daapd.log
+ 4. If you are going to use a remote app, pair it following the procedure
+    described below
 
 
 Supported clients
@@ -236,11 +252,14 @@ them. Watch out for that.
 Playlists and internet radio
 ----------------------------
 
-forked-daapd supports M3U playlists. Just drop your playlist somewhere in
-your library with an .m3u extension and it will pick it up.
+forked-daapd supports M3U and PLS playlists. Just drop your playlist somewhere
+in your library with an .m3u or .pls extension and it will pick it up.
+
+If the playlist contains an http URL it will be added as an internet radio
+station, and the URL will be probed for Shoutcast (ICY) metadata.
 
-If the m3u contains an http URL it will be added as an internet radio station,
-and the URL will be probed for Shoutcast (ICY) metadata.
+forked-daapd does not support playlists in playlists (so for instance a .m3u
+where one of the items is another .m3u or .pls).
 
 Support for iTunes Music Library XML format is available as a compile-time
 option. By default, metadata from our parsers is preferred over what's in
@@ -420,3 +439,21 @@ You will not be able to do any playlist management through forked-daapd - use
 a Spotify client for that. You also can only listen to your music by letting
 forked-daapd do the playback - so that means you can't stream from forked-daapd
 to iTunes.
+
+
+LastFM
+------
+
+If forked-daapd was built with LastFM scrobbling enabled (see the INSTALL file)
+you can have it scrobble the music you listen to. To set up scrobbling you must
+create a text file with the file name ending ".lastfm". The file must have two
+lines: The first is your LastFM user name, and the second is your password. Move
+the file to your forked-daapd library. Forked-daapd will then log in and get a
+permanent session key.
+
+You should delete the .lastfm file immediately after completing the first login.
+For safety, forked-daapd will not store your LastFM username/password, only the
+session key. The session key does not expire.
+
+To stop scrobbling from forked-daapd, add an empty ".lastfm" file to your
+library.
diff --git a/configure.ac b/configure.ac
index 79db34a..4fc034c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,6 +1,6 @@
 dnl Process this file with autoconf to produce a configure script.
 
-AC_INIT([forked-daapd], [21.0])
+AC_INIT([forked-daapd], [22.0])
 AC_CONFIG_SRCDIR([config.h.in])
 AC_CONFIG_MACRO_DIR([m4])
 AC_CONFIG_HEADERS([config.h])
@@ -44,6 +44,7 @@ AC_CHECK_FUNCS(posix_fadvise)
 AC_CHECK_FUNCS(strptime)
 AC_CHECK_FUNCS(strtok_r)
 AC_CHECK_FUNCS(timegm)
+AC_CHECK_FUNCS(euidaccess)
 
 dnl Large File Support (LFS)
 AC_SYS_LARGEFILE
@@ -65,10 +66,15 @@ AC_ARG_ENABLE(spotify, AS_HELP_STRING([--enable-spotify], [enable Spotify librar
                       use_spotify=true;
                       CPPFLAGS="${CPPFLAGS} -DSPOTIFY")
 
+AC_ARG_ENABLE(lastfm, AS_HELP_STRING([--enable-lastfm], [enable LastFM support (default=no)]),
+                      use_lastfm=true;
+                      CPPFLAGS="${CPPFLAGS} -DLASTFM")
+
 AM_CONDITIONAL(COND_FLAC, test x$use_flac = xtrue)
 AM_CONDITIONAL(COND_MUSEPACK, test x$use_musepack = xtrue)
 AM_CONDITIONAL(COND_ITUNES, test x$use_itunes = xtrue)
 AM_CONDITIONAL(COND_SPOTIFY, test x$use_spotify = xtrue)
+AM_CONDITIONAL(COND_LASTFM, test x$use_lastfm = xtrue)
 
 AC_ARG_WITH(oss4, AS_HELP_STRING([--with-oss4=includedir], [use OSS4 with soundcard.h in includedir (default /usr/lib/oss/include/sys)]),
   [ case "$withval" in
@@ -136,11 +142,21 @@ AC_RUN_IFELSE(
 AC_LANG_POP([C])
 LIBS="$save_LIBS"
 
-PKG_CHECK_EXISTS([ libavcodec >= 54.35 ], [ 
-  PKG_CHECK_MODULES(LIBAV, [ libavformat libavcodec libswscale libavresample libavutil ]) 
-], [ 
-  PKG_CHECK_MODULES(LIBAV, [ libavformat libavcodec libswscale libavutil ])
-])
+PKG_CHECK_EXISTS([ libavcodec >= 54.35 ],
+	[PKG_CHECK_EXISTS([ libswresample ],
+		libxxresample=libswresample;
+	,
+		libxxresample=libavresample;
+	)]
+)
+
+if test x$libxxresample = xlibswresample; then
+	AC_DEFINE(HAVE_LIBSWRESAMPLE, 1, [Define to 1 if you have ffmpeg's libswresample])
+elif test x$libxxresample = xlibavresample; then
+	AC_DEFINE(HAVE_LIBAVRESAMPLE, 1, [Define to 1 if you have libav's libavresample])
+fi
+
+PKG_CHECK_MODULES(LIBAV, [ libavformat libavcodec libswscale libavutil $libxxresample ])
 
 save_LIBS="$LIBS"
 dnl Check for av_lock_manager (ffmpeg >= 0.5.1)
@@ -220,6 +236,12 @@ if test x$use_spotify = xtrue; then
 	AC_SUBST(SPOTIFY_LIBS)
 fi
 
+if test x$use_lastfm = xtrue; then
+	PKG_CHECK_MODULES(LIBCURL, [ libcurl ])
+	AC_CHECK_LIB([mxml], [mxmlGetOpaque],
+		AC_DEFINE(HAVE_MXML_GETOPAQUE, 1, [Define to 1 if your mxml has mxmlGetOpaque.]))
+fi
+
 case "$host" in
   *-*-linux-*)
     if test x$use_oss4 != xtrue; then
diff --git a/forked-daapd.conf b/forked-daapd.conf
index 9260074..9f78371 100644
--- a/forked-daapd.conf
+++ b/forked-daapd.conf
@@ -1,3 +1,11 @@
+# A quick guide to configuring forked-daapd:
+#
+# For regular use, the most important setting to configure is "directories",
+# which should be the location of your media. Whatever user you have set as
+# "uid" must have read access to this location. If the location is a network
+# mount, please see the README.
+#
+# In all likelihood, that's all you need to do!
 
 general {
 	# Username
@@ -11,6 +19,11 @@ general {
 	admin_password = "unused"
 	# Enable/disable IPv6
 	ipv6 = no
+	# Location of DAAP cache
+#	daapcache_path = "/var/cache/forked-daapd/daapcache.db"
+	# DAAP requests that take longer than this threshold (in msec) get their
+	# replies cached for next time. Set to 0 to disable caching.
+#	daapcache_threshold = 1000
 }
 
 # Library configuration
diff --git a/src/Makefile.am b/src/Makefile.am
index e5cc7bd..6701bd2 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -2,27 +2,31 @@
 sbin_PROGRAMS = forked-daapd
 
 if COND_FLAC
-FLACSRC=scan-flac.c
+FLAC_SRC=scan-flac.c
 endif
 
 if COND_MUSEPACK
-MUSEPACKSRC=scan-mpc.c
+MUSEPACK_SRC=scan-mpc.c
 endif
 
 if COND_ITUNES
-ITUNESSRC=filescanner_itunes.c
+ITUNES_SRC=filescanner_itunes.c
 endif
 
 if COND_SPOTIFY
-SPOTIFYSRC=spotify.c spotify.h
+SPOTIFY_SRC=spotify.c spotify.h
+endif
+
+if COND_LASTFM
+LASTFM_SRC=lastfm.c lastfm.h
 endif
 
 if COND_ALSA
-ALSASRC=laudio_alsa.c
+ALSA_SRC=laudio_alsa.c
 endif
 
 if COND_OSS4
-OSS4SRC=laudio_oss4.c
+OSS4_SRC=laudio_oss4.c
 endif
 
 if COND_AVIO
@@ -78,20 +82,23 @@ forked_daapd_CPPFLAGS = -D_GNU_SOURCE \
 forked_daapd_CFLAGS = \
 	@ZLIB_CFLAGS@ @AVAHI_CFLAGS@ @SQLITE3_CFLAGS@ @LIBAV_CFLAGS@ \
 	@CONFUSE_CFLAGS@ @TAGLIB_CFLAGS@ @MINIXML_CFLAGS@ @LIBPLIST_CFLAGS@ \
-	@LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @SPOTIFY_CFLAGS@
+	@LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @SPOTIFY_CFLAGS@ \
+	@LIBCURL_CFLAGS@
 
 forked_daapd_LDADD = -lrt \
 	@ZLIB_LIBS@ @AVAHI_LIBS@ @SQLITE3_LIBS@ @LIBAV_LIBS@ \
 	@CONFUSE_LIBS@ @FLAC_LIBS@ @TAGLIB_LIBS@ @LIBEVENT_LIBS@ \
 	@LIBAVL_LIBS@ @MINIXML_LIBS@ @ANTLR3C_LIBS@ @LIBPLIST_LIBS@ \
-	@LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @SPOTIFY_LIBS@
+	@LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @SPOTIFY_LIBS@ \
+	@LIBCURL_LIBS@
 
 forked_daapd_SOURCES = main.c \
 	db.c db.h \
 	logger.c logger.h \
 	conffile.c conffile.h \
+	daap_cache.h daap_cache.c \
 	filescanner.c filescanner.h \
-	filescanner_ffmpeg.c filescanner_m3u.c filescanner_icy.c $(ITUNESSRC) \
+	filescanner_ffmpeg.c filescanner_playlist.c filescanner_icy.c $(ITUNES_SRC) \
 	mdns_avahi.c mdns.h \
 	remote_pairing.c remote_pairing.h \
 	$(EVHTTP_SRC) \
@@ -109,12 +116,12 @@ forked_daapd_SOURCES = main.c \
 	rsp_query.c rsp_query.h \
 	daap_query.c daap_query.h \
 	player.c player.h \
-	$(ALSASRC) $(OSS4SRC) laudio.h \
+	$(ALSA_SRC) $(OSS4_SRC) laudio.h \
 	raop.c raop.h \
 	$(RTSP_SRC) \
 	scan-wma.c \
-	$(SPOTIFYSRC) \
-	$(FLACSRC) $(MUSEPACKSRC)
+	$(SPOTIFY_SRC) $(LASTFM_SRC) \
+	$(FLAC_SRC) $(MUSEPACK_SRC)
 
 nodist_forked_daapd_SOURCES = \
 	$(ANTLR_SOURCES)
diff --git a/src/artwork.c b/src/artwork.c
index c099981..2446890 100644
--- a/src/artwork.c
+++ b/src/artwork.c
@@ -43,6 +43,10 @@
 #endif
 #include "artwork.h"
 
+#ifndef HAVE_LIBEVENT2
+# define evbuffer_get_length(x) (x)->off
+#endif
+
 #ifdef HAVE_SPOTIFY_H
 # include "spotify.h"
 #endif
@@ -717,8 +721,8 @@ artwork_get(char *filename, int max_w, int max_h, int format, struct evbuffer *e
 
   if (ret < 0)
     {
-      if (EVBUFFER_LENGTH(evbuf) > 0)
-	evbuffer_drain(evbuf, EVBUFFER_LENGTH(evbuf));
+      if (evbuffer_get_length(evbuf) > 0)
+	evbuffer_drain(evbuf, evbuffer_get_length(evbuf));
     }
 
   return ret;
@@ -736,14 +740,6 @@ artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, str
   int format_ok;
   int ret;
 
-  /* If item is an internet stream don't look for artwork */
-  if (strncmp(filename, "http://", strlen("http://")) == 0)
-    return -1;
-
-  /* If Spotify item don't look for artwork */
-  if (strncmp(filename, "spotify:", strlen("spotify:")) == 0)
-    return -1;
-
   DPRINTF(E_SPAM, L_ART, "Trying embedded artwork in %s\n", filename);
 
   src_ctx = NULL;
@@ -838,8 +834,8 @@ artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, str
 
   if (ret < 0)
     {
-      if (EVBUFFER_LENGTH(evbuf) > 0)
-	evbuffer_drain(evbuf, EVBUFFER_LENGTH(evbuf));
+      if (evbuffer_get_length(evbuf) > 0)
+	evbuffer_drain(evbuf, evbuffer_get_length(evbuf));
     }
 
   return ret;
@@ -855,27 +851,6 @@ artwork_get_own_image(char *path, int max_w, int max_h, int format, struct evbuf
   int i;
   int ret;
 
-  /* If item is an internet stream don't look for artwork */
-  if (strncmp(path, "http://", strlen("http://")) == 0)
-    return -1;
-
-  if (strncmp(path, "spotify:", strlen("spotify:")) == 0)
-#ifdef HAVE_SPOTIFY_H
-    {
-      if (!(format & ART_CAN_JPEG))
-	return -1;
-
-      ret = spotify_artwork_get(evbuf, path, max_w, max_h);
-
-      if (ret < 0)
-	return -1;
-      else
-        return ART_FMT_JPEG;
-    }
-#else
-    return -1;
-#endif
-
   ret = snprintf(artwork, sizeof(artwork), "%s", path);
   if ((ret < 0) || (ret >= sizeof(artwork)))
     {
@@ -929,14 +904,6 @@ artwork_get_dir_image(char *path, int isdir, int max_w, int max_h, int format, s
   cfg_t *lib;
   int nbasenames;
 
-  /* If item is an internet stream don't look for artwork */
-  if (strncmp(path, "http://", strlen("http://")) == 0)
-    return -1;
-
-  /* If Spotify item don't look for artwork */
-  if (strncmp(path, "spotify:", strlen("spotify:")) == 0)
-    return -1;
-
   ret = snprintf(artwork, sizeof(artwork), "%s", path);
   if ((ret < 0) || (ret >= sizeof(artwork)))
     {
@@ -1003,14 +970,6 @@ artwork_get_parentdir_image(char *path, int isdir, int max_w, int max_h, int for
   int i;
   int ret;
 
-  /* If item is an internet stream don't look for artwork */
-  if (strncmp(path, "http://", strlen("http://")) == 0)
-    return -1;
-
-  /* If Spotify item don't look for artwork */
-  if (strncmp(path, "spotify:", strlen("spotify:")) == 0)
-    return -1;
-
   ret = snprintf(artwork, sizeof(artwork), "%s", path);
   if ((ret < 0) || (ret >= sizeof(artwork)))
     {
@@ -1060,55 +1019,81 @@ artwork_get_parentdir_image(char *path, int isdir, int max_w, int max_h, int for
   return artwork_get(artwork, max_w, max_h, format, evbuf);
 }
 
-
-
-int
-artwork_get_item_filename(char *filename, int max_w, int max_h, int format, struct evbuffer *evbuf)
+static int
+artwork_get_item_path(char *path, int artwork, uint32_t data_kind, int nodir, int max_w, int max_h, int format, struct evbuffer *evbuf)
 {
   int ret;
 
+  ret = 0;
+  switch (artwork)
+    {
+      case ARTWORK_NONE:
+	break;
+#ifdef HAVE_SPOTIFY_H
+      case ARTWORK_SPOTIFY:
+	if (!(format & ART_CAN_JPEG))
+	  break;
+
+	ret = spotify_artwork_get(evbuf, path, max_w, max_h);
+	(ret < 0) ? (ret = 0) : (ret = ART_FMT_JPEG);
+	break;
+#endif
 #if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6)
-  /* Look for embedded artwork */
-  ret = artwork_get_embedded_image(filename, max_w, max_h, format, evbuf);
-  if (ret > 0)
-    return ret;
+      case ARTWORK_EMBEDDED:
+	ret = artwork_get_embedded_image(path, max_w, max_h, format, evbuf);
+
+	/* Fall through if embedded artwork not found */
+	if (ret > 0)
+	  break;
 #endif
+      default:
+	/* Not a normal file */
+	if (data_kind != 0)
+	  break;
 
-  /* Look for basename(filename).{png,jpg} */
-  ret = artwork_get_own_image(filename, max_w, max_h, format, evbuf);
-  if (ret > 0)
-    return ret;
+	/* Look for basename(filename).{png,jpg} */
+	ret = artwork_get_own_image(path, max_w, max_h, format, evbuf);
+	if (ret > 0)
+	  break;
 
-  /* Look for basedir(filename)/{artwork,cover}.{png,jpg} */
-  ret = artwork_get_dir_image(filename, 0, max_w, max_h, format, evbuf);
-  if (ret > 0)
-    return ret;
+	if (nodir)
+	  break;
+
+	/* Look for basedir(filename)/{artwork,cover}.{png,jpg} */
+	ret = artwork_get_dir_image(path, 0, max_w, max_h, format, evbuf);
+	if (ret > 0)
+	  break;
+
+	/* Look for parentdir(filename).{png,jpg} */
+	ret = artwork_get_parentdir_image(path, 0, max_w, max_h, format, evbuf);
+	if (ret > 0)
+	  break;
+    }
 
-  /* Look for parentdir(filename).{png,jpg} */
-  ret = artwork_get_parentdir_image(filename, 0, max_w, max_h, format, evbuf);
   if (ret > 0)
     return ret;
-
-  return -1;
+  else
+    return -1;
 }
 
+
 int
 artwork_get_item(int id, int max_w, int max_h, int format, struct evbuffer *evbuf)
 {
-  char *filename;
+  struct media_file_info *mfi;
   int ret;
 
   DPRINTF(E_DBG, L_ART, "Artwork request for item %d\n", id);
 
-  filename = db_file_path_byid(id);
-  if (!filename)
+  mfi = db_file_fetch_byid(id);
+  if (!mfi)
     return -1;
 
-  ret = artwork_get_item_filename(filename, max_w, max_h, format, evbuf);
+  ret = artwork_get_item_path(mfi->path, mfi->artwork, mfi->data_kind, 0, max_w, max_h, format, evbuf);
   if (ret < 0)
-    DPRINTF(E_DBG, L_ART, "No artwork found for item id %d\n", id);
+    DPRINTF(E_DBG, L_ART, "No artwork found for item id %d (%s)\n", id, mfi->fname);
 
-  free(filename);
+  free_mfi(mfi, 0);
 
   return ret;
 }
@@ -1121,9 +1106,8 @@ artwork_get_group(int id, int max_w, int max_h, int format, struct evbuffer *evb
   char *dir;
   int got_art;
   int ret;
-#if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6)
-  int artwork_t;
-#endif
+  int artwork;
+  uint32_t data_kind;
 
   DPRINTF(E_DBG, L_ART, "Artwork request for group %d\n", id);
 
@@ -1145,6 +1129,14 @@ artwork_get_group(int id, int max_w, int max_h, int format, struct evbuffer *evb
   got_art = 0;
   while (!got_art && ((ret = db_query_fetch_string(&qp, &dir)) == 0) && (dir))
     {
+      /* If item is an internet stream don't look for artwork */
+      if (strncmp(dir, "http://", strlen("http://")) == 0)
+	continue;
+
+      /* If Spotify item don't look for artwork */
+      if (strncmp(dir, "spotify:", strlen("spotify:")) == 0)
+	continue;
+
       got_art = (artwork_get_dir_image(dir, 1, max_w, max_h, format, evbuf) > 0)
                 || (artwork_get_parentdir_image(dir, 1, max_w, max_h, format, evbuf) > 0);
     }
@@ -1175,14 +1167,10 @@ artwork_get_group(int id, int max_w, int max_h, int format, struct evbuffer *evb
   got_art = 0;
   while (!got_art && ((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id))
     {
-#if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6)
-      if ((safe_atoi32(dbmfi.artwork, &artwork_t) == 0) && (artwork_t == ARTWORK_EMBEDDED))
-	got_art = (artwork_get_embedded_image(dbmfi.path, max_w, max_h, format, evbuf) > 0);
-      else
-	got_art = (artwork_get_own_image(dbmfi.path, max_w, max_h, format, evbuf) > 0);
-#else
-      got_art = (artwork_get_own_image(dbmfi.path, max_w, max_h, format, evbuf) > 0);
-#endif
+      if ((safe_atoi32(dbmfi.artwork, &artwork) != 0) && (safe_atou32(dbmfi.data_kind, &data_kind) != 0))
+	continue;
+
+      got_art = (artwork_get_item_path(dbmfi.path, artwork, data_kind, 1, max_w, max_h, format, evbuf) > 0);
     }
 
   db_query_end(&qp);
diff --git a/src/artwork.h b/src/artwork.h
index bb64161..a230464 100644
--- a/src/artwork.h
+++ b/src/artwork.h
@@ -8,14 +8,17 @@
 #define ART_FMT_PNG     1
 #define ART_FMT_JPEG    2
 
-#include <event.h>
-
-int
-artwork_get_item_filename(char *filename, int max_w, int max_h, int format, struct evbuffer *evbuf);
+#ifdef HAVE_LIBEVENT2
+# include <event2/buffer.h>
+#else
+# include <event.h>
+#endif
 
+/* Get artwork for individual track */
 int
 artwork_get_item(int id, int max_w, int max_h, int format, struct evbuffer *evbuf);
 
+/* Get artwork for album or artist */
 int
 artwork_get_group(int id, int max_w, int max_h, int format, struct evbuffer *evbuf);
 
diff --git a/src/avio_evbuffer.h b/src/avio_evbuffer.h
index a5cba88..88d81eb 100644
--- a/src/avio_evbuffer.h
+++ b/src/avio_evbuffer.h
@@ -2,7 +2,11 @@
 #ifndef __AVIO_EVBUFFER_H__
 #define __AVIO_EVBUFFER_H__
 
-#include <event.h>
+#ifdef HAVE_LIBEVENT2
+# include <event2/buffer.h>
+#else
+# include <event.h>
+#endif
 
 AVIOContext *
 avio_evbuffer_open(struct evbuffer *evbuf);
diff --git a/src/conffile.c b/src/conffile.c
index 7392dd6..88dc741 100644
--- a/src/conffile.c
+++ b/src/conffile.c
@@ -48,8 +48,13 @@ static cfg_opt_t sec_general[] =
     CFG_STR("admin_password", NULL, CFGF_NONE),
     CFG_STR("logfile", STATEDIR "/log/" PACKAGE ".log", CFGF_NONE),
     CFG_STR("db_path", STATEDIR "/cache/" PACKAGE "/songs3.db", CFGF_NONE),
+    CFG_INT("db_pragma_cache_size", -1, CFGF_NONE),
+    CFG_STR("db_pragma_journal_mode", NULL, CFGF_NONE),
+    CFG_INT("db_pragma_synchronous", -1, CFGF_NONE),
     CFG_INT_CB("loglevel", E_LOG, CFGF_NONE, &cb_loglevel),
     CFG_BOOL("ipv6", cfg_false, CFGF_NONE),
+    CFG_STR("daapcache_path", STATEDIR "/cache/" PACKAGE "/daapcache.db", CFGF_NONE),
+    CFG_INT("daapcache_threshold", 1000, CFGF_NONE),
     CFG_END()
   };
 
diff --git a/src/daap_cache.c b/src/daap_cache.c
new file mode 100644
index 0000000..4751244
--- /dev/null
+++ b/src/daap_cache.c
@@ -0,0 +1,895 @@
+/*
+ * Copyright (C) 2014 Espen Jürgensen <espenjurgensen at gmail.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 <inttypes.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include <time.h>
+#include <string.h>
+#include <pthread.h>
+
+#include "conffile.h"
+#include "logger.h"
+#include "httpd_daap.h"
+#include "db.h"
+#include "daap_cache.h"
+
+/* The DAAP cache will cache raw daap replies for queries added with
+ * daapcache_add(). Only some query types are supported.
+ * You can't add queries where the canonical reply is not HTTP_OK, because
+ * daap_request will use that as default for cache replies.
+ *
+ */
+
+struct daapcache_command;
+
+typedef int (*cmd_func)(struct daapcache_command *cmd);
+
+struct daapcache_command
+{
+  pthread_mutex_t lck;
+  pthread_cond_t cond;
+
+  cmd_func func;
+
+  int nonblock;
+
+  struct {
+    char *query;
+    char *ua;
+    int msec;
+    struct evbuffer *evbuf;
+  } arg;
+
+  int ret;
+};
+
+/* --- Globals --- */
+// daapcache thread
+static pthread_t tid_daapcache;
+
+// Event base, pipes and events
+struct event_base *evbase_daapcache;
+static int g_exit_pipe[2];
+static int g_cmd_pipe[2];
+static struct event *g_exitev;
+static struct event *g_cmdev;
+static struct event *g_cacheev;
+
+static int g_initialized;
+
+// Global cache database handle
+static sqlite3 *g_db_hdl;
+static char *g_db_path;
+
+// After being triggered wait 60 seconds before rebuilding daapcache
+static struct timeval g_wait = { 60, 0 };
+
+// The user may configure a threshold (in msec), and queries slower than
+// that will have their reply cached
+static int g_cfg_threshold;
+
+/* --------------------------------- HELPERS ------------------------------- */
+
+/* The purpose of this function is to remove transient tags from a request 
+ * url (query), eg remove session-id=xxx
+ */
+static void
+remove_tag(char *in, const char *tag)
+{
+  char *s;
+  char *e;
+
+  s = strstr(in, tag);
+  if (!s)
+    return;
+
+  e = strchr(s, '&');
+  if (e)
+    memmove(s, (e + 1), strlen(e + 1) + 1);
+  else if (s > in)
+    *(s - 1) = '\0';
+}
+
+/* ---------------------------- COMMAND EXECUTION -------------------------- */
+
+static void
+command_init(struct daapcache_command *cmd)
+{
+  memset(cmd, 0, sizeof(struct daapcache_command));
+
+  pthread_mutex_init(&cmd->lck, NULL);
+  pthread_cond_init(&cmd->cond, NULL);
+}
+
+static void
+command_deinit(struct daapcache_command *cmd)
+{
+  pthread_cond_destroy(&cmd->cond);
+  pthread_mutex_destroy(&cmd->lck);
+}
+
+static int
+send_command(struct daapcache_command *cmd)
+{
+  int ret;
+
+  if (!cmd->func)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "BUG: cmd->func is NULL!\n");
+      return -1;
+    }
+
+  ret = write(g_cmd_pipe[1], &cmd, sizeof(cmd));
+  if (ret != sizeof(cmd))
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not send command: %s\n", strerror(errno));
+      return -1;
+    }
+
+  return 0;
+}
+
+static int
+sync_command(struct daapcache_command *cmd)
+{
+  int ret;
+
+  pthread_mutex_lock(&cmd->lck);
+
+  ret = send_command(cmd);
+  if (ret < 0)
+    {
+      pthread_mutex_unlock(&cmd->lck);
+      return -1;
+    }
+
+  pthread_cond_wait(&cmd->cond, &cmd->lck);
+  pthread_mutex_unlock(&cmd->lck);
+
+  ret = cmd->ret;
+
+  return ret;
+}
+
+static int
+nonblock_command(struct daapcache_command *cmd)
+{
+  int ret;
+
+  ret = send_command(cmd);
+  if (ret < 0)
+    return -1;
+
+  return 0;
+}
+
+static void
+thread_exit(void)
+{
+  int dummy = 42;
+
+  DPRINTF(E_DBG, L_DCACHE, "Killing daapcache thread\n");
+
+  if (write(g_exit_pipe[1], &dummy, sizeof(dummy)) != sizeof(dummy))
+    DPRINTF(E_LOG, L_DCACHE, "Could not write to exit fd: %s\n", strerror(errno));
+}
+
+
+/* --------------------------------- MAIN --------------------------------- */
+/*                              Thread: daapcache                              */
+
+static int
+daapcache_create(void)
+{
+#define T_REPLIES						\
+  "CREATE TABLE IF NOT EXISTS replies ("			\
+  "   id                 INTEGER PRIMARY KEY NOT NULL,"		\
+  "   query              VARCHAR(4096) NOT NULL,"		\
+  "   reply              BLOB"					\
+  ");"
+#define T_QUERIES						\
+  "CREATE TABLE IF NOT EXISTS queries ("			\
+  "   id                 INTEGER PRIMARY KEY NOT NULL,"		\
+  "   query              VARCHAR(4096) UNIQUE NOT NULL,"	\
+  "   user_agent         VARCHAR(1024),"			\
+  "   msec               INTEGER DEFAULT 0,"			\
+  "   timestamp          INTEGER DEFAULT 0"			\
+  ");"
+#define I_QUERY							\
+  "CREATE INDEX IF NOT EXISTS idx_query ON replies (query);"
+  char *errmsg;
+  int ret;
+
+  // A fresh start
+  unlink(g_db_path);
+
+  // Create db
+  ret = sqlite3_open(g_db_path, &g_db_hdl);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_DCACHE, "Could not create database: %s\n", sqlite3_errmsg(g_db_hdl));
+
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Create reply cache table
+  ret = sqlite3_exec(g_db_hdl, T_REPLIES, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_DCACHE, "Error creating reply cache table: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Create query table (the queries for which we will generate and cache replies)
+  ret = sqlite3_exec(g_db_hdl, T_QUERIES, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_DCACHE, "Error creating query table: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Create index
+  ret = sqlite3_exec(g_db_hdl, I_QUERY, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_DCACHE, "Error creating query index: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  DPRINTF(E_DBG, L_DCACHE, "Cache created\n");
+
+  return 0;
+#undef T_CACHE
+#undef I_QUERY
+}
+
+static void
+daapcache_destroy(void)
+{
+  sqlite3_stmt *stmt;
+
+  if (!g_db_hdl)
+    return;
+
+  /* Tear down anything that's in flight */
+  while ((stmt = sqlite3_next_stmt(g_db_hdl, 0)))
+    sqlite3_finalize(stmt);
+
+  sqlite3_close(g_db_hdl);
+
+  unlink(g_db_path);
+
+  DPRINTF(E_DBG, L_DCACHE, "Cache destroyed\n");
+}
+
+/* Adds the reply (stored in evbuf) to the cache */
+static int
+daapcache_reply_add(const char *query, struct evbuffer *evbuf)
+{
+#define Q_TMPL "INSERT INTO replies (query, reply) VALUES (?, ?);"
+  sqlite3_stmt *stmt;
+  unsigned char *data;
+  size_t datlen;
+  int ret;
+
+#ifdef HAVE_LIBEVENT2
+  datlen = evbuffer_get_length(evbuf);
+  data = evbuffer_pullup(evbuf, -1);
+#else
+  datlen = EVBUFFER_LENGTH(evbuf);
+  data = EVBUFFER_DATA(evbuf);
+#endif
+
+  ret = sqlite3_prepare_v2(g_db_hdl, Q_TMPL, -1, &stmt, 0);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error preparing query for cache update: %s\n", sqlite3_errmsg(g_db_hdl));
+      return -1;
+    }
+
+  sqlite3_bind_text(stmt, 1, query, -1, SQLITE_STATIC);
+  sqlite3_bind_blob(stmt, 2, data, datlen, SQLITE_STATIC);
+
+  ret = sqlite3_step(stmt);
+  if (ret != SQLITE_DONE)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error stepping query for cache update: %s\n", sqlite3_errmsg(g_db_hdl));
+      sqlite3_finalize(stmt);
+      return -1;
+    }
+
+  ret = sqlite3_finalize(stmt);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error finalizing query for cache update: %s\n", sqlite3_errmsg(g_db_hdl));
+      return -1;
+    }
+
+  DPRINTF(E_DBG, L_DCACHE, "Wrote cache reply, size %d\n", datlen);
+
+  return 0;
+#undef Q_TMPL
+}
+
+/* Adds the query to the list of queries for which we will build and cache a reply */
+static int
+daapcache_query_add(struct daapcache_command *cmd)
+{
+#define Q_TMPL "INSERT OR REPLACE INTO queries (user_agent, query, msec, timestamp) VALUES ('%q', '%q', %d, %" PRIi64 ");"
+#define Q_CLEANUP "DELETE FROM queries WHERE id NOT IN (SELECT id FROM queries ORDER BY timestamp DESC LIMIT 20);"
+  char *query;
+  char *errmsg;
+  int ret;
+
+  if (!cmd->arg.ua)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Couldn't add slow query to cache, unknown user-agent\n");
+
+      goto error_add;
+    }
+
+  // Currently we are only able to pre-build and cache these reply types
+  if ( (strncmp(cmd->arg.query, "/databases/1/containers/", strlen("/databases/1/containers/")) != 0) &&
+       (strncmp(cmd->arg.query, "/databases/1/groups?", strlen("/databases/1/groups?")) != 0) &&
+       (strncmp(cmd->arg.query, "/databases/1/items?", strlen("/databases/1/items?")) != 0) &&
+       (strncmp(cmd->arg.query, "/databases/1/browse/", strlen("/databases/1/browse/")) != 0) )
+    goto error_add;
+
+  remove_tag(cmd->arg.query, "session-id");
+  remove_tag(cmd->arg.query, "revision-number");
+
+  query = sqlite3_mprintf(Q_TMPL, cmd->arg.ua, cmd->arg.query, cmd->arg.msec, (int64_t)time(NULL));
+  if (!query)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Out of memory making query string.\n");
+
+      goto error_add;
+    }
+
+  ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error adding query to query list: %s\n", errmsg);
+
+      sqlite3_free(query);
+      sqlite3_free(errmsg);
+      goto error_add;
+    }
+
+  sqlite3_free(query);
+
+  DPRINTF(E_INFO, L_DCACHE, "Slow query (%d ms) added to cache: '%s' (user-agent: '%s')\n", cmd->arg.msec, cmd->arg.query, cmd->arg.ua);
+
+  free(cmd->arg.ua);
+  free(cmd->arg.query);
+
+  // Limits the size of the cache to only contain replies for 20 most recent queries
+  ret = sqlite3_exec(g_db_hdl, Q_CLEANUP, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error cleaning up query list before update: %s\n", errmsg);
+      sqlite3_free(errmsg);
+      return -1;
+    }
+
+  daapcache_trigger();
+
+  return 0;
+
+ error_add:
+  if (cmd->arg.ua)
+    free(cmd->arg.ua);
+
+  if (cmd->arg.query)
+    free(cmd->arg.query);
+
+  return -1;
+#undef Q_CLEANUP
+#undef Q_TMPL
+}
+
+/* Gets a reply from the cache */
+static int
+daapcache_query_get(struct daapcache_command *cmd)
+{
+#define Q_TMPL "SELECT reply FROM replies WHERE query = ?;"
+  sqlite3_stmt *stmt;
+  char *query;
+  int datlen;
+  int ret;
+
+  cmd->arg.evbuf = NULL;
+
+  query = cmd->arg.query;
+  remove_tag(query, "session-id");
+  remove_tag(query, "revision-number");
+
+  // Look in the DB
+  ret = sqlite3_prepare_v2(g_db_hdl, Q_TMPL, -1, &stmt, 0);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error preparing query for cache update: %s\n", sqlite3_errmsg(g_db_hdl));
+      free(query);
+      return -1;
+    }
+
+  sqlite3_bind_text(stmt, 1, query, -1, SQLITE_STATIC);
+
+  ret = sqlite3_step(stmt);
+  if (ret != SQLITE_ROW)  
+    {
+      if (ret != SQLITE_DONE)
+	DPRINTF(E_LOG, L_DCACHE, "Error stepping query for cache update: %s\n", sqlite3_errmsg(g_db_hdl));
+      goto error_get;
+    }
+
+  datlen = sqlite3_column_bytes(stmt, 0);
+
+  cmd->arg.evbuf = evbuffer_new();
+  if (!cmd->arg.evbuf)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create reply evbuffer\n");
+      goto error_get;
+    }
+
+  ret = evbuffer_add(cmd->arg.evbuf, sqlite3_column_blob(stmt, 0), datlen);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Out of memory for reply evbuffer\n");
+      evbuffer_free(cmd->arg.evbuf);
+      cmd->arg.evbuf = NULL;
+      goto error_get;
+    }
+
+  ret = sqlite3_finalize(stmt);
+  if (ret != SQLITE_OK)
+    DPRINTF(E_LOG, L_DCACHE, "Error finalizing query for getting cache: %s\n", sqlite3_errmsg(g_db_hdl));
+
+  DPRINTF(E_INFO, L_DCACHE, "Cache hit: %s\n", query);
+
+  free(query);
+
+  return 0;
+
+ error_get:
+  sqlite3_finalize(stmt);
+  free(query);
+  return -1;  
+#undef Q_TMPL
+}
+
+/* Here we actually update the cache by asking httpd_daap for responses
+ * to the queries set for caching
+ */
+static void
+daapcache_update_cb(int fd, short what, void *arg)
+{
+  sqlite3_stmt *stmt;
+  struct evbuffer *evbuf;
+  char *errmsg;
+  char *query;
+  int ret;
+
+  DPRINTF(E_INFO, L_DCACHE, "Timeout reached, time to update DAAP cache\n");
+
+  ret = sqlite3_exec(g_db_hdl, "DELETE FROM replies;", NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error clearing reply cache before update: %s\n", errmsg);
+      sqlite3_free(errmsg);
+      return;
+    }
+
+  ret = sqlite3_prepare_v2(g_db_hdl, "SELECT user_agent, query FROM queries;", -1, &stmt, 0);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error preparing for cache update: %s\n", sqlite3_errmsg(g_db_hdl));
+      return;
+    }
+
+  while ((ret = sqlite3_step(stmt)) == SQLITE_ROW)
+    {
+      query = strdup((char *)sqlite3_column_text(stmt, 1));
+
+      evbuf = daap_reply_build(query, (char *)sqlite3_column_text(stmt, 0));
+      if (!evbuf)
+	{
+	  DPRINTF(E_LOG, L_DCACHE, "Error building DAAP reply for query: %s\n", query);
+	  free(query);
+	  continue;
+	}
+
+      daapcache_reply_add(query, evbuf);
+
+      free(query);
+      evbuffer_free(evbuf);
+    }
+
+  if (ret != SQLITE_DONE)
+    DPRINTF(E_LOG, L_DCACHE, "Could not step: %s\n", sqlite3_errmsg(g_db_hdl));
+
+  sqlite3_finalize(stmt);
+
+  DPRINTF(E_INFO, L_DCACHE, "DAAP cache updated\n");
+}
+
+/* This function will just set a timer, which when it times out will trigger
+ * the actual daapcache update. The purpose is to avoid avoid daapcache updates when
+ * the database is busy, eg during a library scan.
+ */
+static int
+daapcache_update_timer(struct daapcache_command *cmd)
+{
+  if (!g_cacheev)
+    return -1;
+
+  evtimer_add(g_cacheev, &g_wait);
+
+  return 0;
+}
+
+static void *
+daapcache(void *arg)
+{
+  int ret;
+
+  ret = daapcache_create();
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error: Cache create failed\n");
+      pthread_exit(NULL);
+    }
+
+  ret = db_perthread_init();
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Error: DB init failed\n");
+      daapcache_destroy();
+
+      pthread_exit(NULL);
+    }
+
+  g_initialized = 1;
+
+  event_base_dispatch(evbase_daapcache);
+
+  if (g_initialized)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "daapcache event loop terminated ahead of time!\n");
+      g_initialized = 0;
+    }
+
+  db_perthread_deinit();
+
+  daapcache_destroy();
+
+  pthread_exit(NULL);
+}
+
+static void
+exit_cb(int fd, short what, void *arg)
+{
+  int dummy;
+  int ret;
+
+  ret = read(g_exit_pipe[0], &dummy, sizeof(dummy));
+  if (ret != sizeof(dummy))
+    DPRINTF(E_LOG, L_DCACHE, "Error reading from exit pipe\n");
+
+  event_base_loopbreak(evbase_daapcache);
+
+  g_initialized = 0;
+
+  event_add(g_exitev, NULL);
+}
+
+static void
+command_cb(int fd, short what, void *arg)
+{
+  struct daapcache_command *cmd;
+  int ret;
+
+  ret = read(g_cmd_pipe[0], &cmd, sizeof(cmd));
+  if (ret != sizeof(cmd))
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not read command! (read %d): %s\n", ret, (ret < 0) ? strerror(errno) : "-no error-");
+      goto readd;
+    }
+
+  if (cmd->nonblock)
+    {
+      cmd->func(cmd);
+
+      free(cmd);
+      goto readd;
+    }
+
+  pthread_mutex_lock(&cmd->lck);
+
+  ret = cmd->func(cmd);
+  cmd->ret = ret;
+
+  pthread_cond_signal(&cmd->cond);
+  pthread_mutex_unlock(&cmd->lck);
+
+ readd:
+  event_add(g_cmdev, NULL);
+}
+
+
+
+/* ---------------------------- Our daapcache API  --------------------------- */
+
+void
+daapcache_trigger(void)
+{
+  struct daapcache_command *cmd; 
+
+  if (!g_initialized)
+    return;
+
+  cmd = (struct daapcache_command *)malloc(sizeof(struct daapcache_command));
+  if (!cmd)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not allocate daapcache_command\n");
+      return;
+    }
+
+  memset(cmd, 0, sizeof(struct daapcache_command));
+
+  cmd->nonblock = 1;
+
+  cmd->func = daapcache_update_timer;
+
+  nonblock_command(cmd);
+}
+
+struct evbuffer *
+daapcache_get(const char *query)
+{
+  struct daapcache_command cmd;
+  struct evbuffer *evbuf;
+  int ret;
+
+  if (!g_initialized)
+    return NULL;
+
+  command_init(&cmd);
+
+  cmd.func = daapcache_query_get;
+  cmd.arg.query = strdup(query);
+
+  ret = sync_command(&cmd);
+
+  evbuf = cmd.arg.evbuf;
+
+  command_deinit(&cmd);
+
+  return ((ret < 0) ? NULL : evbuf);
+}
+
+void
+daapcache_add(const char *query, const char *ua, int msec)
+{
+  struct daapcache_command *cmd; 
+
+  if (!g_initialized)
+    return;
+
+  cmd = (struct daapcache_command *)malloc(sizeof(struct daapcache_command));
+  if (!cmd)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not allocate daapcache_command\n");
+      return;
+    }
+
+  memset(cmd, 0, sizeof(struct daapcache_command));
+
+  cmd->nonblock = 1;
+
+  cmd->func = daapcache_query_add;
+  cmd->arg.query = strdup(query);
+  cmd->arg.ua = strdup(ua);
+  cmd->arg.msec = msec;
+
+  nonblock_command(cmd);
+}
+
+int
+daapcache_threshold(void)
+{
+  return g_cfg_threshold;
+}
+
+int
+daapcache_init(void)
+{
+  int ret;
+
+  g_db_path = cfg_getstr(cfg_getsec(cfg, "general"), "daapcache_path");
+  if (!g_db_path || (strlen(g_db_path) == 0))
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Cache path invalid, disabling cache\n");
+      g_initialized = 0;
+      return 0;
+    }
+
+  g_cfg_threshold = cfg_getint(cfg_getsec(cfg, "general"), "daapcache_threshold");
+  if (g_cfg_threshold == 0)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Cache threshold set to 0, disabling cache\n");
+      g_initialized = 0;
+      return 0;
+    }
+
+# if defined(__linux__)
+  ret = pipe2(g_exit_pipe, O_CLOEXEC);
+# else
+  ret = pipe(g_exit_pipe);
+# endif
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create pipe: %s\n", strerror(errno));
+      goto exit_fail;
+    }
+
+# if defined(__linux__)
+  ret = pipe2(g_cmd_pipe, O_CLOEXEC);
+# else
+  ret = pipe(g_cmd_pipe);
+# endif
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create command pipe: %s\n", strerror(errno));
+      goto cmd_fail;
+    }
+
+  evbase_daapcache = event_base_new();
+  if (!evbase_daapcache)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create an event base\n");
+      goto evbase_fail;
+    }
+
+#ifdef HAVE_LIBEVENT2
+  g_exitev = event_new(evbase_daapcache, g_exit_pipe[0], EV_READ, exit_cb, NULL);
+  if (!g_exitev)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create exit event\n");
+      goto evnew_fail;
+    }
+
+  g_cmdev = event_new(evbase_daapcache, g_cmd_pipe[0], EV_READ, command_cb, NULL);
+  if (!g_cmdev)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create cmd event\n");
+      goto evnew_fail;
+    }
+
+  g_cacheev = evtimer_new(evbase_daapcache, daapcache_update_cb, NULL);
+  if (!g_cmdev)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create daapcache event\n");
+      goto evnew_fail;
+    }
+#else
+  g_exitev = (struct event *)malloc(sizeof(struct event));
+  if (!g_exitev)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create exit event\n");
+      goto evnew_fail;
+    }
+  event_set(g_exitev, g_exit_pipe[0], EV_READ, exit_cb, NULL);
+  event_base_set(evbase_daapcache, g_exitev);
+
+  g_cmdev = (struct event *)malloc(sizeof(struct event));
+  if (!g_cmdev)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create cmd event\n");
+      goto evnew_fail;
+    }
+  event_set(g_cmdev, g_cmd_pipe[0], EV_READ, command_cb, NULL);
+  event_base_set(evbase_daapcache, g_cmdev);
+
+  g_cacheev = (struct event *)malloc(sizeof(struct event));
+  if (!g_cacheev)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not create daapcache event\n");
+      goto evnew_fail;
+    }
+  event_set(g_cacheev, -1, EV_TIMEOUT, daapcache_update_cb, NULL);
+  event_base_set(evbase_daapcache, g_cacheev);
+#endif
+
+  event_add(g_exitev, NULL);
+  event_add(g_cmdev, NULL);
+
+  DPRINTF(E_INFO, L_DCACHE, "daapcache thread init\n");
+
+  ret = pthread_create(&tid_daapcache, NULL, daapcache, NULL);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_DCACHE, "Could not spawn daapcache thread: %s\n", strerror(errno));
+
+      goto thread_fail;
+    }
+
+  return 0;
+  
+ thread_fail:
+ evnew_fail:
+  event_base_free(evbase_daapcache);
+  evbase_daapcache = NULL;
+
+ evbase_fail:
+  close(g_cmd_pipe[0]);
+  close(g_cmd_pipe[1]);
+
+ cmd_fail:
+  close(g_exit_pipe[0]);
+  close(g_exit_pipe[1]);
+
+ exit_fail:
+  return -1;
+}
+
+void
+daapcache_deinit(void)
+{
+  int ret;
+
+  if (!g_initialized)
+    return;
+
+  thread_exit();
+
+  ret = pthread_join(tid_daapcache, NULL);
+  if (ret != 0)
+    {
+      DPRINTF(E_FATAL, L_DCACHE, "Could not join daapcache thread: %s\n", strerror(errno));
+      return;
+    }
+
+  // Free event base (should free events too)
+  event_base_free(evbase_daapcache);
+
+  // Close pipes
+  close(g_cmd_pipe[0]);
+  close(g_cmd_pipe[1]);
+  close(g_exit_pipe[0]);
+  close(g_exit_pipe[1]);
+}
diff --git a/src/daap_cache.h b/src/daap_cache.h
new file mode 100644
index 0000000..15d0fa7
--- /dev/null
+++ b/src/daap_cache.h
@@ -0,0 +1,30 @@
+
+#ifndef __DAAP_CACHE_H__
+#define __DAAP_CACHE_H__
+
+#ifdef HAVE_LIBEVENT2
+# include <event2/event.h>
+# include <event2/buffer.h>
+#else
+# include <event.h>
+#endif
+
+void
+daapcache_trigger(void);
+
+struct evbuffer *
+daapcache_get(const char *query);
+
+void
+daapcache_add(const char *query, const char *ua, int msec);
+
+int
+daapcache_threshold(void);
+
+int
+daapcache_init(void);
+
+void
+daapcache_deinit(void);
+
+#endif /* !__DAAP_CACHE_H__ */
diff --git a/src/db.c b/src/db.c
index 12ed79e..0782359 100644
--- a/src/db.c
+++ b/src/db.c
@@ -40,6 +40,7 @@
 
 #include "conffile.h"
 #include "logger.h"
+#include "daap_cache.h"
 #include "misc.h"
 #include "db.h"
 
@@ -1555,6 +1556,36 @@ db_query_end(struct query_params *qp)
   qp->stmt = NULL;
 }
 
+static int
+db_query_run(char *query, int free, int cache_update)
+{
+  char *errmsg;
+  int ret;
+
+  if (!query)
+    {
+      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
+
+      return -1;
+    }
+
+  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
+
+  ret = db_exec(query, &errmsg);
+  if (ret != SQLITE_OK)
+    DPRINTF(E_LOG, L_DB, "Error '%s' while runnning '%s'\n", errmsg, query);
+
+  sqlite3_free(errmsg);
+
+  if (free)
+    sqlite3_free(query);
+
+  if (cache_update)
+    daapcache_trigger();
+
+  return ((ret != SQLITE_OK) ? -1 : 0);
+}
+
 int
 db_query_fetch_file(struct query_params *qp, struct db_media_file_info *dbmfi)
 {
@@ -1851,37 +1882,13 @@ db_files_get_count_bymatch(char *path)
 void
 db_files_update_songartistid(void)
 {
-#define Q_SONGARTISTID "UPDATE files SET songartistid = daap_songalbumid(LOWER(album_artist), '');"
-  char *errmsg;
-  int ret;
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", Q_SONGARTISTID);
-
-  ret = db_exec(Q_SONGARTISTID, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error updating songartistid: %s\n", errmsg);
-
-  sqlite3_free(errmsg);
-
-#undef Q_SONGARTISTID
+  db_query_run("UPDATE files SET songartistid = daap_songalbumid(LOWER(album_artist), '');", 0, 1);
 }
 
 void
 db_files_update_songalbumid(void)
 {
-#define Q_SONGALBUMID "UPDATE files SET songalbumid = daap_songalbumid(LOWER(album_artist), LOWER(album));"
-  char *errmsg;
-  int ret;
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", Q_SONGALBUMID);
-
-  ret = db_exec(Q_SONGALBUMID, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error updating songalbumid: %s\n", errmsg);
-
-  sqlite3_free(errmsg);
-
-#undef Q_SONGALBUMID
+  db_query_run("UPDATE files SET songalbumid = daap_songalbumid(LOWER(album_artist), LOWER(album));", 0, 1);
 }
 
 void
@@ -1908,26 +1915,10 @@ db_file_ping(int id)
 {
 #define Q_TMPL "UPDATE files SET db_timestamp = %" PRIi64 ", disabled = 0 WHERE id = %d;"
   char *query;
-  char *errmsg;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error pinging file ID %d: %s\n", id, errmsg);
-
-  sqlite3_free(errmsg);
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 1);
 #undef Q_TMPL
 }
 
@@ -1937,30 +1928,13 @@ db_file_ping_bymatch(char *path, int isdir)
 #define Q_TMPL_DIR "UPDATE files SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q/%%';"
 #define Q_TMPL_NODIR "UPDATE files SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q%%';"
   char *query;
-  char *errmsg;
-  int ret;
 
   if (isdir)
     query = sqlite3_mprintf(Q_TMPL_DIR, (int64_t)time(NULL), path);
   else
     query = sqlite3_mprintf(Q_TMPL_NODIR, (int64_t)time(NULL), path);
 
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error pinging files matching %s: %s\n", path, errmsg);
-
-  sqlite3_free(errmsg);
-  sqlite3_free(query);
-
+  db_query_run(query, 1, 1);
 #undef Q_TMPL_DIR
 #undef Q_TMPL_NODIR
 }
@@ -2456,6 +2430,8 @@ db_file_add(struct media_file_info *mfi)
 
   sqlite3_free(query);
 
+  daapcache_trigger();
+
   return 0;
 
 #undef Q_TMPL
@@ -2530,6 +2506,8 @@ db_file_update(struct media_file_info *mfi)
 
   sqlite3_free(query);
 
+  daapcache_trigger();
+
   return 0;
 
 #undef Q_TMPL
@@ -2540,44 +2518,13 @@ db_file_delete_bypath(char *path)
 {
 #define Q_TMPL "DELETE FROM files WHERE path = '%q';"
   char *query;
-  char *errmsg;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error deleting file: %s\n", errmsg);
-
-  sqlite3_free(errmsg);
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 1);
 #undef Q_TMPL
 }
 
-static void
-db_file_disable_byquery(char *query)
-{
-  char *errmsg;
-  int ret;
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error disabling file: %s\n", errmsg);
-
-  sqlite3_free(errmsg);
-}
-
 void
 db_file_disable_bypath(char *path, char *strip, uint32_t cookie)
 {
@@ -2590,17 +2537,8 @@ db_file_disable_bypath(char *path, char *strip, uint32_t cookie)
   striplen = strlen(strip) + 1;
 
   query = sqlite3_mprintf(Q_TMPL, striplen, disabled, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  db_file_disable_byquery(query);
-
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 1);
 #undef Q_TMPL
 }
 
@@ -2616,17 +2554,8 @@ db_file_disable_bymatch(char *path, char *strip, uint32_t cookie)
   striplen = strlen(strip) + 1;
 
   query = sqlite3_mprintf(Q_TMPL, striplen, disabled, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  db_file_disable_byquery(query);
-
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 1);
 #undef Q_TMPL
 }
 
@@ -2635,33 +2564,13 @@ db_file_enable_bycookie(uint32_t cookie, char *path)
 {
 #define Q_TMPL "UPDATE files SET path = '%q' || path, disabled = 0 WHERE disabled = %" PRIi64 ";"
   char *query;
-  char *errmsg;
   int ret;
 
   query = sqlite3_mprintf(Q_TMPL, path, (int64_t)cookie);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return -1;
-    }
 
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Error enabling files: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      sqlite3_free(query);
-      return -1;
-    }
-
-  sqlite3_free(query);
-
-  return sqlite3_changes(hdl);
+  ret = db_query_run(query, 1, 1);
 
+  return ((ret < 0) ? -1 : sqlite3_changes(hdl));
 #undef Q_TMPL
 }
 
@@ -2727,26 +2636,10 @@ db_pl_ping(int id)
 {
 #define Q_TMPL "UPDATE playlists SET db_timestamp = %" PRIi64 ", disabled = 0 WHERE id = %d;"
   char *query;
-  char *errmsg;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error pinging playlist %d: %s\n", id, errmsg);
-
-  sqlite3_free(errmsg);
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -2756,30 +2649,13 @@ db_pl_ping_bymatch(char *path, int isdir)
 #define Q_TMPL_DIR "UPDATE playlists SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q/%%';"
 #define Q_TMPL_NODIR "UPDATE playlists SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q%%';"
   char *query;
-  char *errmsg;
-  int ret;
 
   if (isdir)
     query = sqlite3_mprintf(Q_TMPL_DIR, (int64_t)time(NULL), path);
   else
     query = sqlite3_mprintf(Q_TMPL_NODIR, (int64_t)time(NULL), path);
 
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error pinging playlists matching %s: %s\n", path, errmsg);
-
-  sqlite3_free(errmsg);
-  sqlite3_free(query);
-
+  db_query_run(query, 1, 0);
 #undef Q_TMPL_DIR
 #undef Q_TMPL_NODIR
 }
@@ -3104,32 +2980,10 @@ db_pl_add_item_bypath(int plid, char *path)
 {
 #define Q_TMPL "INSERT INTO playlistitems (playlistid, filepath) VALUES (%d, '%q');"
   char *query;
-  char *errmsg;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, plid, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-      return -1;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      sqlite3_free(query);
-      return -1;
-    }
-
-  sqlite3_free(query);
-
-  return 0;
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3138,32 +2992,10 @@ db_pl_add_item_byid(int plid, int fileid)
 {
 #define Q_TMPL "INSERT INTO playlistitems (playlistid, filepath) VALUES (%d, (SELECT f.path FROM files f WHERE f.id = %d));"
   char *query;
-  char *errmsg;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, plid, fileid);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-      return -1;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      sqlite3_free(query);
-      return -1;
-    }
-
-  sqlite3_free(query);
-
-  return 0;
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3172,33 +3004,10 @@ db_pl_update(char *title, char *path, int id)
 {
 #define Q_TMPL "UPDATE playlists SET title = '%q', db_timestamp = %" PRIi64 ", disabled = 0, path = '%q' WHERE id = %d;"
   char *query;
-  char *errmsg;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, title, (int64_t)time(NULL), path, id);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return -1;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      sqlite3_free(query);
-      return -1;
-    }
 
-  sqlite3_free(errmsg);
-  sqlite3_free(query);
-
-  return 0;
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3207,59 +3016,29 @@ db_pl_clear_items(int id)
 {
 #define Q_TMPL "DELETE FROM playlistitems WHERE playlistid = %d;"
   char *query;
-  char *errmsg;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, id);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
 
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error clearing playlist %d items: %s\n", id, errmsg);
-
-  sqlite3_free(errmsg);
-  sqlite3_free(query);
-
-#undef Q_TMPL
-}
+  db_query_run(query, 1, 0);
+#undef Q_TMPL
+}
 
 void
 db_pl_delete(int id)
 {
 #define Q_TMPL "DELETE FROM playlists WHERE id = %d;"
   char *query;
-  char *errmsg;
   int ret;
 
   if (id == 1)
     return;
 
   query = sqlite3_mprintf(Q_TMPL, id);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
 
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error deleting playlist %d: %s\n", id, errmsg);
-
-  sqlite3_free(errmsg);
-  sqlite3_free(query);
-
-  db_pl_clear_items(id);
+  ret = db_query_run(query, 1, 0);
 
+  if (ret == 0)
+    db_pl_clear_items(id);
 #undef Q_TMPL
 }
 
@@ -3276,21 +3055,6 @@ db_pl_delete_bypath(char *path)
   db_pl_delete(id);
 }
 
-static void
-db_pl_disable_byquery(char *query)
-{
-  char *errmsg;
-  int ret;
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error disabling playlist: %s\n", errmsg);
-
-  sqlite3_free(errmsg);
-}
-
 void
 db_pl_disable_bypath(char *path, char *strip, uint32_t cookie)
 {
@@ -3303,17 +3067,8 @@ db_pl_disable_bypath(char *path, char *strip, uint32_t cookie)
   striplen = strlen(strip) + 1;
 
   query = sqlite3_mprintf(Q_TMPL, striplen, disabled, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  db_pl_disable_byquery(query);
-
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3329,17 +3084,8 @@ db_pl_disable_bymatch(char *path, char *strip, uint32_t cookie)
   striplen = strlen(strip) + 1;
 
   query = sqlite3_mprintf(Q_TMPL, striplen, disabled, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  db_pl_disable_byquery(query);
-
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3348,33 +3094,13 @@ db_pl_enable_bycookie(uint32_t cookie, char *path)
 {
 #define Q_TMPL "UPDATE playlists SET path = '%q' || path, disabled = 0 WHERE disabled = %" PRIi64 ";"
   char *query;
-  char *errmsg;
   int ret;
 
   query = sqlite3_mprintf(Q_TMPL, path, (int64_t)cookie);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
 
-      return -1;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Error enabling playlists: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      sqlite3_free(query);
-      return -1;
-    }
-
-  sqlite3_free(query);
-
-  return sqlite3_changes(hdl);
+  ret = db_query_run(query, 1, 0);
 
+  return ((ret < 0) ? -1 : sqlite3_changes(hdl));
 #undef Q_TMPL
 }
 
@@ -3383,22 +3109,7 @@ db_pl_enable_bycookie(uint32_t cookie, char *path)
 int
 db_groups_clear(void)
 {
-  char *query = "DELETE FROM groups;";
-  char *errmsg;
-  int ret;
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      return -1;
-    }
-
-  return 0;
+  return db_query_run("DELETE FROM groups;", 0, 1);
 }
 
 enum group_type
@@ -3462,33 +3173,10 @@ db_pairing_delete_byremote(char *remote_id)
 {
 #define Q_TMPL "DELETE FROM pairings WHERE remote = '%q';"
   char *query;
-  char *errmsg;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, remote_id);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return -1;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Error deleting pairing: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      sqlite3_free(query);
-      return -1;
-    }
-
-  sqlite3_free(query);
-
-  return 0;
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3497,7 +3185,6 @@ db_pairing_add(struct pairing_info *pi)
 {
 #define Q_TMPL "INSERT INTO pairings (remote, name, guid) VALUES ('%q', '%q', '%q');"
   char *query;
-  char *errmsg;
   int ret;
 
   ret = db_pairing_delete_byremote(pi->remote_id);
@@ -3505,29 +3192,8 @@ db_pairing_add(struct pairing_info *pi)
     return ret;
 
   query = sqlite3_mprintf(Q_TMPL, pi->remote_id, pi->name, pi->guid);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return -1;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Error adding pairing: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      sqlite3_free(query);
-      return -1;
-    }
-
-  sqlite3_free(query);
-
-  return 0;
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3595,22 +3261,14 @@ db_spotify_purge(void)
       "DELETE FROM playlistitems WHERE filepath LIKE 'spotify:%%';",
       "DELETE FROM playlists WHERE path LIKE 'spotify:%%';",
     };
-  char *errmsg;
   int i;
   int ret;
 
   for (i = 0; i < (sizeof(queries) / sizeof(queries[0])); i++)
     {
-      DPRINTF(E_DBG, L_DB, "Running spotify purge query '%s'\n", queries[i]);
+      ret = db_query_run(queries[i], 0, 1);
 
-      ret = db_exec(queries[i], &errmsg);
-      if (ret != SQLITE_OK)
-	{
-	  DPRINTF(E_LOG, L_DB, "Purge query %d error: %s\n", i, errmsg);
-
-	  sqlite3_free(errmsg);
-	}
-      else
+      if (ret == 0)
 	DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
     }
 }
@@ -3619,79 +3277,133 @@ db_spotify_purge(void)
 void
 db_spotify_pl_delete(int id)
 {
-  char *queries[3] = { NULL, NULL, NULL };
   char *queries_tmpl[3] =
     {
       "DELETE FROM playlists WHERE id = %d;",
       "DELETE FROM playlistitems WHERE playlistid = %d;",
       "DELETE FROM files WHERE path LIKE 'spotify:%%' AND NOT path IN (SELECT filepath FROM playlistitems WHERE id <> %d);",
     };
-  char *errmsg;
+  char *query;
   int i;
   int ret;
 
   for (i = 0; i < (sizeof(queries_tmpl) / sizeof(queries_tmpl[0])); i++)
     {
-      queries[i] = sqlite3_mprintf(queries_tmpl[i], id);
-      if (!queries[i])
-	{
-	  DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-	  return;
-	}
-    }
-
-  for (i = 0; i < (sizeof(queries) / sizeof(queries[0])); i++)
-    {
-      DPRINTF(E_DBG, L_DB, "Running spotify playlist delete query '%s'\n", queries[i]);
+      query = sqlite3_mprintf(queries_tmpl[i], id);
 
-      ret = db_exec(queries[i], &errmsg);
-      if (ret != SQLITE_OK)
-	{
-	  DPRINTF(E_LOG, L_DB, "Spotify playlist delete %d error: %s\n", i, errmsg);
+      ret = db_query_run(query, 1, 1);
 
-	  sqlite3_free(errmsg);
-	}
-      else
+      if (ret == 0)
 	DPRINTF(E_DBG, L_DB, "Deleted %d rows\n", sqlite3_changes(hdl));
     }
 }
 #endif
 
-
-/* Speakers */
+/* Admin */
 int
-db_speaker_save(uint64_t id, int selected, int volume)
+db_admin_add(const char *key, const char *value)
 {
-#define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume) VALUES (%" PRIi64 ", %d, %d);"
+#define Q_TMPL "INSERT OR REPLACE INTO admin (key, value) VALUES ('%q', '%q');"
   char *query;
-  char *errmsg;
+
+  query = sqlite3_mprintf(Q_TMPL, key, value);
+
+  return db_query_run(query, 1, 0);
+#undef Q_TMPL
+}
+
+char *
+db_admin_get(const char *key)
+{
+#define Q_TMPL "SELECT value FROM admin a WHERE a.key = '%q';"
+  char *query;
+  sqlite3_stmt *stmt;
+  char *res;
   int ret;
 
-  query = sqlite3_mprintf(Q_TMPL, id, selected, volume);
+  query = sqlite3_mprintf(Q_TMPL, key);
   if (!query)
     {
       DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
 
-      return -1;
+      return NULL;
     }
 
   DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
 
-  errmsg = NULL;
-  ret = db_exec(query, &errmsg);
+  ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL);
   if (ret != SQLITE_OK)
     {
-      DPRINTF(E_LOG, L_DB, "Error saving speaker state: %s\n", errmsg);
+      DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
 
-      sqlite3_free(errmsg);
       sqlite3_free(query);
-      return -1;
+      return NULL;
     }
 
+  ret = db_blocking_step(stmt);
+  if (ret != SQLITE_ROW)
+    {
+      if (ret == SQLITE_DONE)
+	DPRINTF(E_DBG, L_DB, "No results\n");
+      else
+	DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));	
+
+      sqlite3_finalize(stmt);
+      sqlite3_free(query);
+      return NULL;
+    }
+
+  res = (char *)sqlite3_column_text(stmt, 0);
+  if (res)
+    res = strdup(res);
+
+#ifdef DB_PROFILE
+  while (db_blocking_step(stmt) == SQLITE_ROW)
+    ; /* EMPTY */
+#endif
+
+  sqlite3_finalize(stmt);
   sqlite3_free(query);
 
-  return 0;
+  return res;
+
+#undef Q_TMPL
+}
+
+int
+db_admin_update(const char *key, const char *value)
+{
+#define Q_TMPL "UPDATE admin SET value='%q' WHERE key='%q';"
+  char *query;
+
+  query = sqlite3_mprintf(Q_TMPL, key, value);
+
+  return db_query_run(query, 1, 0);
+#undef Q_TMPL
+}
+
+int
+db_admin_delete(const char *key)
+{
+#define Q_TMPL "DELETE FROM admin where key='%q';"
+  char *query;
+
+  query = sqlite3_mprintf(Q_TMPL, key);
+
+  return db_query_run(query, 1, 0);
+#undef Q_TMPL
+}
+
+/* Speakers */
+int
+db_speaker_save(uint64_t id, int selected, int volume)
+{
+#define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume) VALUES (%" PRIi64 ", %d, %d);"
+  char *query;
+
+  query = sqlite3_mprintf(Q_TMPL, id, selected, volume);
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3756,19 +3468,7 @@ db_speaker_get(uint64_t id, int *selected, int *volume)
 void
 db_speaker_clear_all(void)
 {
-  char *query = "UPDATE speakers SET selected = 0;";
-  char *errmsg;
-  int ret;
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-    }
+  db_query_run("UPDATE speakers SET selected = 0;", 0, 0);
 }
 
 
@@ -3776,22 +3476,7 @@ db_speaker_clear_all(void)
 int
 db_watch_clear(void)
 {
-  char *query = "DELETE FROM inotify;";
-  char *errmsg;
-  int ret;
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Query error: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      return -1;
-    }
-
-  return 0;
+  return db_query_run("DELETE FROM inotify;", 0, 0);
 }
 
 int
@@ -3799,78 +3484,22 @@ db_watch_add(struct watch_info *wi)
 {
 #define Q_TMPL "INSERT INTO inotify (wd, cookie, path) VALUES (%d, 0, '%q');"
   char *query;
-  char *errmsg;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, wi->wd, wi->path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return -1;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Error adding watch: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      sqlite3_free(query);
-      return -1;
-    }
-
-  sqlite3_free(query);
-
-  return 0;
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
-static int
-db_watch_delete_byquery(char *query)
-{
-  char *errmsg;
-  int ret;
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    {
-      DPRINTF(E_LOG, L_DB, "Error deleting watch: %s\n", errmsg);
-
-      sqlite3_free(errmsg);
-      sqlite3_free(query);
-      return -1;
-    }
-
-  return 0;
-}
-
 int
 db_watch_delete_bywd(uint32_t wd)
 {
 #define Q_TMPL "DELETE FROM inotify WHERE wd = %d;"
   char *query;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, wd);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return -1;
-    }
-
-  ret = db_watch_delete_byquery(query);
-
-  sqlite3_free(query);
-
-  return ret;
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3879,22 +3508,10 @@ db_watch_delete_bypath(char *path)
 {
 #define Q_TMPL "DELETE FROM inotify WHERE path = '%q';"
   char *query;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return -1;
-    }
-
-  ret = db_watch_delete_byquery(query);
-
-  sqlite3_free(query);
-
-  return ret;
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3903,22 +3520,10 @@ db_watch_delete_bymatch(char *path)
 {
 #define Q_TMPL "DELETE FROM inotify WHERE path LIKE '%q/%%';"
   char *query;
-  int ret;
 
   query = sqlite3_mprintf(Q_TMPL, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return -1;
-    }
-
-  ret = db_watch_delete_byquery(query);
-
-  sqlite3_free(query);
-
-  return ret;
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -3927,25 +3532,13 @@ db_watch_delete_bycookie(uint32_t cookie)
 {
 #define Q_TMPL "DELETE FROM inotify WHERE cookie = %" PRIi64 ";"
   char *query;
-  int ret;
 
   if (cookie == 0)
     return -1;
 
   query = sqlite3_mprintf(Q_TMPL, (int64_t)cookie);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return -1;
-    }
-
-  ret = db_watch_delete_byquery(query);
-
-  sqlite3_free(query);
-
-  return ret;
 
+  return db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -4068,21 +3661,6 @@ db_watch_get_bypath(struct watch_info *wi)
 #undef Q_TMPL
 }
 
-static void
-db_watch_mark_byquery(char *query)
-{
-  char *errmsg;
-  int ret;
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error marking watch: %s\n", errmsg);
-
-  sqlite3_free(errmsg);
-}
-
 void
 db_watch_mark_bypath(char *path, char *strip, uint32_t cookie)
 {
@@ -4095,17 +3673,8 @@ db_watch_mark_bypath(char *path, char *strip, uint32_t cookie)
   striplen = strlen(strip) + 1;
 
   query = sqlite3_mprintf(Q_TMPL, striplen, disabled, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  db_watch_mark_byquery(query);
-
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -4121,17 +3690,8 @@ db_watch_mark_bymatch(char *path, char *strip, uint32_t cookie)
   striplen = strlen(strip) + 1;
 
   query = sqlite3_mprintf(Q_TMPL, striplen, disabled, path);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  db_watch_mark_byquery(query);
-
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -4140,29 +3700,13 @@ db_watch_move_bycookie(uint32_t cookie, char *path)
 {
 #define Q_TMPL "UPDATE inotify SET path = '%q' || path, cookie = 0 WHERE cookie = %" PRIi64 ";"
   char *query;
-  char *errmsg;
-  int ret;
 
   if (cookie == 0)
     return;
 
   query = sqlite3_mprintf(Q_TMPL, path, (int64_t)cookie);
-  if (!query)
-    {
-      DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
-
-      return;
-    }
-
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
-
-  ret = db_exec(query, &errmsg);
-  if (ret != SQLITE_OK)
-    DPRINTF(E_LOG, L_DB, "Error moving watch: %s\n", errmsg);
-
-  sqlite3_free(errmsg);
-  sqlite3_free(query);
 
+  db_query_run(query, 1, 0);
 #undef Q_TMPL
 }
 
@@ -4338,12 +3882,184 @@ db_xprofile(void *notused, const char *pquery, sqlite3_uint64 ptime)
 }
 #endif
 
+static int
+db_pragma_get_cache_size()
+{
+  sqlite3_stmt *stmt;
+  char *query = "PRAGMA cache_size;";
+  int ret;
+
+  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
+
+  ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
+
+      sqlite3_free(query);
+      return 0;
+    }
+
+  ret = db_blocking_step(stmt);
+  if (ret == SQLITE_DONE)
+    {
+      DPRINTF(E_DBG, L_DB, "End of query results\n");
+      sqlite3_free(query);
+      return 0;
+    }
+  else if (ret != SQLITE_ROW)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
+      sqlite3_free(query);
+      return -1;
+    }
+
+  ret = sqlite3_column_int(stmt, 0);
+
+  sqlite3_finalize(stmt);
+  return ret;
+}
+
+static int
+db_pragma_set_cache_size(int pages)
+{
+#define Q_TMPL "PRAGMA cache_size=%d;"
+  sqlite3_stmt *stmt;
+  char *query;
+  int ret;
+
+  query = sqlite3_mprintf(Q_TMPL, pages);
+  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
+
+  ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
+
+      sqlite3_free(query);
+      return 0;
+    }
+
+  sqlite3_finalize(stmt);
+  sqlite3_free(query);
+  return 0;
+#undef Q_TMPL
+}
+
+static char *
+db_pragma_set_journal_mode(char *mode)
+{
+#define Q_TMPL "PRAGMA journal_mode=%s;"
+  sqlite3_stmt *stmt;
+  char *query;
+  int ret;
+  char *new_mode;
+
+  query = sqlite3_mprintf(Q_TMPL, mode);
+  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
+
+  ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
+
+      sqlite3_free(query);
+      return NULL;
+    }
+
+  ret = db_blocking_step(stmt);
+  if (ret == SQLITE_DONE)
+    {
+      DPRINTF(E_DBG, L_DB, "End of query results\n");
+      sqlite3_free(query);
+      return NULL;
+    }
+  else if (ret != SQLITE_ROW)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
+      sqlite3_free(query);
+      return NULL;
+    }
+
+  new_mode = (char *) sqlite3_column_text(stmt, 0);
+  sqlite3_finalize(stmt);
+  sqlite3_free(query);
+  return new_mode;
+#undef Q_TMPL
+}
+
+static int
+db_pragma_get_synchronous()
+{
+  sqlite3_stmt *stmt;
+  char *query = "PRAGMA synchronous;";
+  int ret;
+
+  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
+
+  ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
+
+      sqlite3_free(query);
+      return 0;
+    }
+
+  ret = db_blocking_step(stmt);
+  if (ret == SQLITE_DONE)
+    {
+      DPRINTF(E_DBG, L_DB, "End of query results\n");
+      sqlite3_free(query);
+      return 0;
+    }
+  else if (ret != SQLITE_ROW)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
+      sqlite3_free(query);
+      return -1;
+    }
+
+  ret = sqlite3_column_int(stmt, 0);
+
+  sqlite3_finalize(stmt);
+  return ret;
+}
+
+static int
+db_pragma_set_synchronous(int synchronous)
+{
+#define Q_TMPL "PRAGMA synchronous=%d;"
+  sqlite3_stmt *stmt;
+  char *query;
+  int ret;
+
+  query = sqlite3_mprintf(Q_TMPL, synchronous);
+  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
+
+  ret = db_blocking_prepare_v2(query, -1, &stmt, NULL);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
+
+      sqlite3_free(query);
+      return 0;
+    }
+
+  sqlite3_finalize(stmt);
+  sqlite3_free(query);
+  return 0;
+#undef Q_TMPL
+}
 
 int
 db_perthread_init(void)
 {
   char *errmsg;
   int ret;
+  int cache_size;
+  char *journal_mode;
+  int synchronous;
 
   ret = sqlite3_open(db_path, &hdl);
   if (ret != SQLITE_OK)
@@ -4392,6 +4108,29 @@ db_perthread_init(void)
   sqlite3_profile(hdl, db_xprofile, NULL);
 #endif
 
+  cache_size = cfg_getint(cfg_getsec(cfg, "general"), "db_pragma_cache_size");
+  if (cache_size > -1)
+    {
+      db_pragma_set_cache_size(cache_size);
+      cache_size = db_pragma_get_cache_size();
+      DPRINTF(E_DBG, L_DB, "Database cache size in pages: %d\n", cache_size);
+    }
+
+  journal_mode = cfg_getstr(cfg_getsec(cfg, "general"), "db_pragma_journal_mode");
+  if (journal_mode)
+    {
+      journal_mode = db_pragma_set_journal_mode(journal_mode);
+      DPRINTF(E_DBG, L_DB, "Database journal mode: %s\n", journal_mode);
+    }
+
+  synchronous = cfg_getint(cfg_getsec(cfg, "general"), "db_pragma_synchronous");
+  if (synchronous > -1)
+    {
+      db_pragma_set_synchronous(synchronous);
+      synchronous = db_pragma_get_synchronous();
+      DPRINTF(E_DBG, L_DB, "Database synchronous: %d\n", synchronous);
+    }
+
   return 0;
 }
 
diff --git a/src/db.h b/src/db.h
index 14e2462..78be9f8 100644
--- a/src/db.h
+++ b/src/db.h
@@ -480,6 +480,19 @@ void
 db_spotify_pl_delete(int id);
 #endif
 
+/* Admin */
+int
+db_admin_add(const char *key, const char *value);
+
+char *
+db_admin_get(const char *key);
+
+int
+db_admin_update(const char *key, const char *value);
+
+int
+db_admin_delete(const char *key);
+
 /* Speakers */
 int
 db_speaker_save(uint64_t id, int selected, int volume);
diff --git a/src/dmap_common.c b/src/dmap_common.c
index d4b5b5b..985475a 100644
--- a/src/dmap_common.c
+++ b/src/dmap_common.c
@@ -23,6 +23,10 @@
 #include <string.h>
 #include <stdint.h>
 
+#ifndef HAVE_LIBEVENT2
+# define evbuffer_get_length(x) (x)->off
+#endif
+
 #include "db.h"
 #include "misc.h"
 #include "logger.h"
@@ -43,7 +47,7 @@ dmap_get_fields_table(int *nfields)
 
 
 void
-dmap_add_container(struct evbuffer *evbuf, char *tag, int len)
+dmap_add_container(struct evbuffer *evbuf, const char *tag, int len)
 {
   unsigned char buf[4];
 
@@ -59,7 +63,7 @@ dmap_add_container(struct evbuffer *evbuf, char *tag, int len)
 }
 
 void
-dmap_add_long(struct evbuffer *evbuf, char *tag, int64_t val)
+dmap_add_long(struct evbuffer *evbuf, const char *tag, int64_t val)
 {
   unsigned char buf[12];
 
@@ -85,7 +89,7 @@ dmap_add_long(struct evbuffer *evbuf, char *tag, int64_t val)
 }
 
 void
-dmap_add_int(struct evbuffer *evbuf, char *tag, int val)
+dmap_add_int(struct evbuffer *evbuf, const char *tag, int val)
 {
   unsigned char buf[8];
 
@@ -107,7 +111,7 @@ dmap_add_int(struct evbuffer *evbuf, char *tag, int val)
 }
 
 void
-dmap_add_short(struct evbuffer *evbuf, char *tag, short val)
+dmap_add_short(struct evbuffer *evbuf, const char *tag, short val)
 {
   unsigned char buf[6];
 
@@ -127,7 +131,7 @@ dmap_add_short(struct evbuffer *evbuf, char *tag, short val)
 }
 
 void
-dmap_add_char(struct evbuffer *evbuf, char *tag, char val)
+dmap_add_char(struct evbuffer *evbuf, const char *tag, char val)
 {
   unsigned char buf[5];
 
@@ -146,7 +150,7 @@ dmap_add_char(struct evbuffer *evbuf, char *tag, char val)
 }
 
 void
-dmap_add_literal(struct evbuffer *evbuf, char *tag, char *str, int len)
+dmap_add_literal(struct evbuffer *evbuf, const char *tag, char *str, int len)
 {
   char buf[4];
 
@@ -179,7 +183,7 @@ dmap_add_raw_uint32(struct evbuffer *evbuf, uint32_t val)
 }
 
 void
-dmap_add_string(struct evbuffer *evbuf, char *tag, const char *str)
+dmap_add_string(struct evbuffer *evbuf, const char *tag, const char *str)
 {
   unsigned char buf[4];
   int len;
@@ -341,12 +345,15 @@ dmap_add_field(struct evbuffer *evbuf, const struct dmap_field *df, char *strval
 
 
 void
-dmap_send_error(struct evhttp_request *req, char *container, char *errmsg)
+dmap_send_error(struct evhttp_request *req, const char *container, const char *errmsg)
 {
   struct evbuffer *evbuf;
   int len;
   int ret;
 
+  if (!req)
+    return;
+
   evbuf = evbuffer_new();
   if (!evbuf)
     {
@@ -526,7 +533,7 @@ dmap_encode_file_metadata(struct evbuffer *songlist, struct evbuffer *song, stru
   if (want_asdk)
     val += 9;
 
-  dmap_add_container(songlist, "mlit", EVBUFFER_LENGTH(song) + val);
+  dmap_add_container(songlist, "mlit", evbuffer_get_length(song) + val);
 
   /* Prepend mikd & asdk if needed */
   if (want_mikd)
diff --git a/src/dmap_common.h b/src/dmap_common.h
index 181b2d8..b3c0617 100644
--- a/src/dmap_common.h
+++ b/src/dmap_common.h
@@ -2,10 +2,11 @@
 #ifndef __DMAP_HELPERS_H__
 #define __DMAP_HELPERS_H__
 
-#include <event.h>
 #ifdef HAVE_LIBEVENT2
+# include <event2/buffer.h>
 # include <event2/http.h>
 #else
+# include <event.h>
 # include "evhttp/evhttp.h"
 #endif
 
@@ -54,35 +55,35 @@ dmap_find_field (register const char *str, register unsigned int len);
 
 
 void
-dmap_add_container(struct evbuffer *evbuf, char *tag, int len);
+dmap_add_container(struct evbuffer *evbuf, const char *tag, int len);
 
 void
-dmap_add_long(struct evbuffer *evbuf, char *tag, int64_t val);
+dmap_add_long(struct evbuffer *evbuf, const char *tag, int64_t val);
 
 void
-dmap_add_int(struct evbuffer *evbuf, char *tag, int val);
+dmap_add_int(struct evbuffer *evbuf, const char *tag, int val);
 
 void
-dmap_add_short(struct evbuffer *evbuf, char *tag, short val);
+dmap_add_short(struct evbuffer *evbuf, const char *tag, short val);
 
 void
-dmap_add_char(struct evbuffer *evbuf, char *tag, char val);
+dmap_add_char(struct evbuffer *evbuf, const char *tag, char val);
 
 void
-dmap_add_literal(struct evbuffer *evbuf, char *tag, char *str, int len);
+dmap_add_literal(struct evbuffer *evbuf, const char *tag, char *str, int len);
 
 void
 dmap_add_raw_uint32(struct evbuffer *evbuf, uint32_t val);
 
 void
-dmap_add_string(struct evbuffer *evbuf, char *tag, const char *str);
+dmap_add_string(struct evbuffer *evbuf, const char *tag, const char *str);
 
 void
 dmap_add_field(struct evbuffer *evbuf, const struct dmap_field *df, char *strval, int32_t intval);
 
 
 void
-dmap_send_error(struct evhttp_request *req, char *container, char *errmsg);
+dmap_send_error(struct evhttp_request *req, const char *container, const char *errmsg);
 
 
 int
diff --git a/src/evhttp/evhttp_compat.h b/src/evhttp/evhttp_compat.h
index 657bdd2..6c1af0d 100644
--- a/src/evhttp/evhttp_compat.h
+++ b/src/evhttp/evhttp_compat.h
@@ -1,3 +1,6 @@
+#ifndef _EVHTTP_COMPAT_H_
+#define _EVHTTP_COMPAT_H_
+
 #include "evhttp.h"
 
 /* This file should only be included if using libevent 1
@@ -24,4 +27,4 @@ evhttp_connection_base_new(struct event_base *base, void *ignore, const char *ad
 void
 evhttp_request_set_header_cb(struct evhttp_request *req, int (*cb)(struct evhttp_request *, void *));
 
-
+#endif /* _EVHTTP_COMPAT_H_ */
diff --git a/src/filescanner.c b/src/filescanner.c
index aa88752..97d4365 100644
--- a/src/filescanner.c
+++ b/src/filescanner.c
@@ -63,6 +63,9 @@
 #include "remote_pairing.h"
 #include "player.h"
 
+#ifdef LASTFM
+# include "lastfm.h"
+#endif
 #ifdef HAVE_SPOTIFY_H
 # include "spotify.h"
 #endif
@@ -73,6 +76,20 @@
 #define F_SCAN_FAST    (1 << 2)
 #define F_SCAN_MOVED   (1 << 3)
 
+enum file_type {
+  FILE_UNKNOWN = 0,
+  FILE_IGNORE,
+  FILE_REGULAR,
+  FILE_PLAYLIST,
+  FILE_ITUNES,
+  FILE_ARTWORK,
+  FILE_CTRL_REMOTE,
+  FILE_CTRL_LASTFM,
+  FILE_CTRL_SPOTIFY,
+  FILE_CTRL_INITSCAN,
+  FILE_CTRL_FULLSCAN,
+};
+
 struct deferred_pl {
   char *path;
   time_t mtime;
@@ -153,8 +170,9 @@ pop_dir(struct stacked_dir **s)
   return ret;
 }
 
+/* Checks if the file extension is in the ignore list */
 static int
-ignore_filetype(char *ext)
+file_type_ignore(const char *ext)
 {
   cfg_t *lib;
   int n;
@@ -172,6 +190,66 @@ ignore_filetype(char *ext)
   return 0;
 }
 
+static enum file_type
+file_type_get(const char *path) {
+  const char *filename;
+  const char *ext;
+
+  filename = strrchr(path, '/');
+  if ((!filename) || (strlen(filename) == 1))
+    filename = path;
+  else
+    filename++;
+
+  ext = strrchr(path, '.');
+  if (!ext || (strlen(ext) == 1))
+    return FILE_REGULAR;
+
+  if ((strcasecmp(ext, ".m3u") == 0) || (strcasecmp(ext, ".pls") == 0))
+    return FILE_PLAYLIST;
+
+  if ((strcasecmp(ext, ".png") == 0) || (strcasecmp(ext, ".jpg") == 0))
+    return FILE_ARTWORK;
+
+#ifdef ITUNES
+  if (strcasecmp(ext, ".xml") == 0)
+    return FILE_ITUNES;
+#endif
+
+  if (strcasecmp(ext, ".remote") == 0)
+    return FILE_CTRL_REMOTE;
+
+#ifdef LASTFM
+  if (strcasecmp(ext, ".lastfm") == 0)
+    return FILE_CTRL_LASTFM;
+#endif
+
+#ifdef HAVE_SPOTIFY_H
+  if (strcasecmp(ext, ".spotify") == 0)
+    return FILE_CTRL_SPOTIFY;
+#endif
+
+  if (strcasecmp(ext, ".init-rescan") == 0)
+    return FILE_CTRL_INITSCAN;
+
+  if (strcasecmp(ext, ".full-rescan") == 0)
+    return FILE_CTRL_FULLSCAN;
+
+  if (strcasecmp(ext, ".url") == 0)
+    {
+      DPRINTF(E_INFO, L_SCAN, "No support for .url, use .m3u or .pls\n");
+      return FILE_IGNORE;
+    }
+
+  if (file_type_ignore(ext))
+    return FILE_IGNORE;
+
+  if ((filename[0] == '_') || (filename[0] == '.'))
+    return FILE_IGNORE;
+
+  return FILE_REGULAR;
+}
+
 static void
 sort_tag_create(char **sort_tag, char *src_tag)
 {
@@ -184,6 +262,7 @@ sort_tag_create(char **sort_tag, char *src_tag)
   ucs4_t puc;
   int numlen;
   size_t len;
+  int charlen;
 
   /* Note: include terminating NUL in string length for u8_normalize */
 
@@ -236,7 +315,11 @@ sort_tag_create(char **sort_tag, char *src_tag)
 	  if (number)
 	    append_number = 1; // A number has ended so time to append it
 	  else
-	    o_ptr = u8_stpncpy(o_ptr, i_ptr, u8_strmblen(i_ptr)); // No numbers in sight, just append char
+	    {
+              charlen = u8_strmblen(i_ptr);
+              if (charlen >= 0)
+	    	o_ptr = u8_stpncpy(o_ptr, i_ptr, charlen); // No numbers in sight, just append char
+	    }
 	}
 
       // Break if less than 100 bytes remain (prevent buffer overflow)
@@ -437,7 +520,6 @@ filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct
 {
   struct media_file_info *mfi;
   char *filename;
-  char *ext;
   time_t stamp;
   int id;
   int ret;
@@ -448,33 +530,6 @@ filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct
   else
     filename++;
 
-  /* File types which should never be processed */
-  ext = strrchr(path, '.');
-  if (ext)
-    {
-      if ((strcasecmp(ext, ".pls") == 0) || (strcasecmp(ext, ".url") == 0))
-	{
-	  DPRINTF(E_INFO, L_SCAN, "No support for .url and .pls in this version, use .m3u\n");
-
-	  return;
-	}
-      else if ((strcasecmp(ext, ".png") == 0) || (strcasecmp(ext, ".jpg") == 0))
-	{
-	  /* Artwork files - don't scan */
-	  return;
-	}
-      else if ((filename[0] == '_') || (filename[0] == '.'))
-	{
-	  /* Hidden files - don't scan */
-	  return;
-	}
-      else if (ignore_filetype(ext))
-	{
-	  /* File extension is in ignore list - don't scan */
-	  return;
-	}
-    }
-
   db_file_stamp_bypath(path, &stamp, &id);
 
   if (stamp && (stamp >= mtime))
@@ -585,18 +640,15 @@ filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct
 static void
 process_playlist(char *file, time_t mtime)
 {
-  char *ext;
+  enum file_type ft;
 
-  ext = strrchr(file, '.');
-  if (ext)
-    {
-      if (strcasecmp(ext, ".m3u") == 0)
-	scan_m3u_playlist(file, mtime);
+  ft = file_type_get(file);
+  if (ft == FILE_PLAYLIST)
+    scan_playlist(file, mtime);
 #ifdef ITUNES
-      else if (strcasecmp(ext, ".xml") == 0)
-	scan_itunes_itml(file);
+  else if (ft == FILE_ITUNES)
+    scan_itunes_itml(file);
 #endif
-    }
 }
 
 /* Thread: scan */
@@ -646,9 +698,6 @@ process_deferred_playlists(void)
       free(pl->path);
       free(pl);
 
-      /* Run the event loop */
-      event_base_loop(evbase_scan, EVLOOP_ONCE | EVLOOP_NONBLOCK);
-
       if (scan_exit)
 	return;
     }
@@ -658,85 +707,76 @@ process_deferred_playlists(void)
 static void
 process_file(char *file, time_t mtime, off_t size, int type, int flags)
 {
-  char *ext;
-
-  ext = strrchr(file, '.');
-  if (ext)
+  switch (file_type_get(file))
     {
-      if ((strcasecmp(ext, ".m3u") == 0)
-#ifdef ITUNES
-	  || (strcasecmp(ext, ".xml") == 0)
-#endif
-	  )
-	{
-	  if (flags & F_SCAN_BULK)
-	    defer_playlist(file, mtime);
-	  else
-	    process_playlist(file, mtime);
+      case FILE_REGULAR:
+	filescanner_process_media(file, mtime, size, type, NULL);
 
-	  return;
-	}
-      else if (strcmp(ext, ".remote") == 0)
-	{
-	  remote_pairing_read_pin(file);
+	counter++;
 
-	  return;
-	}
-#ifdef HAVE_SPOTIFY_H
-      else if (strcmp(ext, ".spotify") == 0)
-	{
-	  spotify_login(file);
+	/* When in bulk mode, split transaction in pieces of 200 */
+	if ((flags & F_SCAN_BULK) && (counter % 200 == 0))
+	  {
+	    DPRINTF(E_LOG, L_SCAN, "Scanned %d files...\n", counter);
+	    db_transaction_end();
+	    db_transaction_begin();
+	  }
+	break;
 
-	  return;
-	}
+      case FILE_PLAYLIST:
+      case FILE_ITUNES:
+	if (flags & F_SCAN_BULK)
+	  defer_playlist(file, mtime);
+	else
+	  process_playlist(file, mtime);
+	break;
+
+      case FILE_CTRL_REMOTE:
+	remote_pairing_read_pin(file);
+	break;
+
+#ifdef LASTFM
+      case FILE_CTRL_LASTFM:
+	lastfm_login(file);
+	break;
 #endif
-      else if (strcmp(ext, ".full-rescan") == 0)
-	{
-	  if (flags & F_SCAN_BULK)
-	    return;
-	  else
-	    {
-	      DPRINTF(E_LOG, L_SCAN, "Forcing full rescan, found full-rescan file: %s\n", file);
-	      player_playback_stop();
-	      player_queue_clear();
-	      inofd_event_unset(); // Clears all inotify watches
-	      db_purge_all(); // Clears files, playlists, playlistitems, inotify and groups
 
-	      inofd_event_set();
-	      bulk_scan(F_SCAN_BULK);
+#ifdef HAVE_SPOTIFY_H
+      case FILE_CTRL_SPOTIFY:
+	spotify_login(file);
+	break;
+#endif
 
-	      return;
-	    }
-	}
-      else if (strcmp(ext, ".init-rescan") == 0)
-	{
-	  if (flags & F_SCAN_BULK)
-	    return;
-	  else
-	    {
-	      DPRINTF(E_LOG, L_SCAN, "Forcing startup rescan, found init-rescan file: %s\n", file);
-	      inofd_event_unset(); // Clears all inotify watches
-	      db_watch_clear();
+      case FILE_CTRL_INITSCAN:
+	if (flags & F_SCAN_BULK)
+	  break;
 
-	      inofd_event_set();
-	      bulk_scan(F_SCAN_BULK | F_SCAN_RESCAN);
+	DPRINTF(E_LOG, L_SCAN, "Startup rescan triggered, found init-rescan file: %s\n", file);
 
-	      return;
-	    }
-	}
-    }
+	inofd_event_unset(); // Clears all inotify watches
+	db_watch_clear();
 
-  /* Not any kind of special file, so let's see if it's a media file */
-  filescanner_process_media(file, mtime, size, type, NULL);
+	inofd_event_set();
+	bulk_scan(F_SCAN_BULK | F_SCAN_RESCAN);
+	break;
 
-  counter++;
+      case FILE_CTRL_FULLSCAN:
+	if (flags & F_SCAN_BULK)
+	  break;
 
-  /* When in bulk mode, split transaction in pieces of 200 */
-  if ((flags & F_SCAN_BULK) && (counter % 200 == 0))
-    {
-      DPRINTF(E_LOG, L_SCAN, "Scanned %d files...\n", counter);
-      db_transaction_end();
-      db_transaction_begin();
+	DPRINTF(E_LOG, L_SCAN, "Full rescan triggered, found full-rescan file: %s\n", file);
+
+	player_playback_stop();
+	player_queue_clear();
+	inofd_event_unset(); // Clears all inotify watches
+	db_purge_all(); // Clears files, playlists, playlistitems, inotify and groups
+
+	inofd_event_set();
+	bulk_scan(F_SCAN_BULK);
+	break;
+
+      default:
+	DPRINTF(E_WARN, L_SCAN, "Ignoring file: %s\n", file);
     }
 }
 
@@ -763,7 +803,6 @@ check_speciallib(char *path, const char *libtype)
 static void
 process_directory(char *path, int flags)
 {
-  struct stacked_dir *bulkstack;
   DIR *dirp;
   struct dirent buf;
   struct dirent *de;
@@ -777,24 +816,6 @@ process_directory(char *path, int flags)
   int type;
   int ret;
 
-  if (flags & F_SCAN_BULK)
-    {
-      /* Save our directory stack so it won't get handled inside
-       * the event loop - not its business, we're in bulk mode here.
-       */
-      bulkstack = dirstack;
-      dirstack = NULL;
-
-      /* Run the event loop */
-      event_base_loop(evbase_scan, EVLOOP_ONCE | EVLOOP_NONBLOCK);
-
-      /* Restore our directory stack */
-      dirstack = bulkstack;
-
-      if (scan_exit)
-	return;
-    }
-
   DPRINTF(E_DBG, L_SCAN, "Processing directory %s (flags = 0x%x)\n", path, flags);
 
   dirp = opendir(path);
@@ -816,6 +837,9 @@ process_directory(char *path, int flags)
 
   for (;;)
     {
+      if (scan_exit)
+	break;
+
       ret = readdir_r(dirp, &buf, &de);
       if (ret != 0)
 	{
@@ -1056,6 +1080,20 @@ static void *
 filescanner(void *arg)
 {
   int ret;
+#if defined(__linux__)
+  struct sched_param param;
+
+  /* Lower the priority of the thread so forked-daapd may still respond
+   * during file scan on low power devices. Param must be 0 for the SCHED_BATCH
+   * policy.
+   */
+  memset(&param, 0, sizeof(struct sched_param));
+  ret = pthread_setschedparam(pthread_self(), SCHED_BATCH, &param);
+  if (ret != 0)
+    {
+      DPRINTF(E_LOG, L_SCAN, "Warning: Could not set thread priority to SCHED_BATCH\n");
+    }
+#endif
 
   ret = db_perthread_init();
   if (ret < 0)
@@ -1093,12 +1131,12 @@ filescanner(void *arg)
   else
     bulk_scan(F_SCAN_BULK);
 
+  if (!scan_exit)
+    {
 #ifdef HAVE_SPOTIFY_H
-  spotify_login(NULL);
+      spotify_login(NULL);
 #endif
 
-  if (!scan_exit)
-    {
       /* Enable inotify */
       event_add(&inoev, NULL);
 
@@ -1244,7 +1282,11 @@ process_inotify_dir(struct watch_info *wi, char *path, struct inotify_event *ie)
 	free(wi->path);
       wi->path = s;
 
+#ifdef HAVE_EUIDACCESS
       if (euidaccess(path, (R_OK | X_OK)) < 0)
+#else
+      if (access(path, (R_OK | X_OK)) < 0)
+#endif
 	{
 	  DPRINTF(E_LOG, L_SCAN, "Directory access to '%s' failed: %s\n", path, strerror(errno));
 
@@ -1307,13 +1349,17 @@ process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie
     {
       DPRINTF(E_DBG, L_SCAN, "File permissions changed: %s\n", path);
 
+#ifdef HAVE_EUIDACCESS
       if (euidaccess(path, R_OK) < 0)
+#else
+      if (access(path, R_OK) < 0)
+#endif
 	{
 	  DPRINTF(E_LOG, L_SCAN, "File access to '%s' failed: %s\n", path, strerror(errno));
 
 	  db_file_delete_bypath(path);;
 	}
-      else if (db_file_id_bypath(path) <= 0)
+      else if ((file_type_get(path) == FILE_REGULAR) && (db_file_id_bypath(path) <= 0)) // TODO Playlists
 	{
 	  DPRINTF(E_LOG, L_SCAN, "File access to '%s' achieved\n", path);
 
@@ -1760,13 +1806,6 @@ exit_cb(int fd, short event, void *arg)
   scan_exit = 1;
 }
 
-
-int
-filescanner_status(void)
-{
-  return scan_exit;
-}
-
 /* Thread: main */
 int
 filescanner_init(void)
@@ -1870,6 +1909,8 @@ filescanner_deinit(void)
     }
 #endif
 
+  scan_exit = 1;
+
   ret = pthread_join(tid_scan, NULL);
   if (ret != 0)
     {
diff --git a/src/filescanner.h b/src/filescanner.h
index 5600b15..97c628e 100644
--- a/src/filescanner.h
+++ b/src/filescanner.h
@@ -18,9 +18,6 @@ filescanner_init(void);
 void
 filescanner_deinit(void);
 
-int
-filescanner_status(void);
-
 void
 filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct media_file_info *external_mfi);
 
@@ -32,7 +29,7 @@ int
 scan_metadata_icy(char *url, struct media_file_info *mfi);
 
 void
-scan_m3u_playlist(char *file, time_t mtime);
+scan_playlist(char *file, time_t mtime);
 
 #ifdef ITUNES
 void
diff --git a/src/filescanner_ffmpeg.c b/src/filescanner_ffmpeg.c
index f38f729..cec6b1a 100644
--- a/src/filescanner_ffmpeg.c
+++ b/src/filescanner_ffmpeg.c
@@ -314,7 +314,8 @@ extract_metadata(struct media_file_info *mfi, AVFormatContext *ctx, AVStream *au
   return mdcount;
 }
 
-/* Extracts ICY metadata (requires libav 10: libavformat 55.13) */
+#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13)
+/* Extracts ICY metadata (requires libav 10) */
 static void
 extract_metadata_icy(struct media_file_info *mfi, AVFormatContext *ctx)
 {
@@ -387,6 +388,7 @@ extract_metadata_icy(struct media_file_info *mfi, AVFormatContext *ctx)
   av_free(icy_meta);
   free(icy_str);
 }
+#endif
 
 int
 scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
@@ -542,9 +544,11 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
 
   DPRINTF(E_DBG, L_SCAN, "Duration %d ms, bitrate %d kbps\n", mfi->song_length, mfi->bitrate);
 
+#if LIBAVFORMAT_VERSION_MAJOR >= 56 || (LIBAVFORMAT_VERSION_MAJOR == 55 && LIBAVFORMAT_VERSION_MINOR >= 13)
   /* Try to extract ICY metadata if url/stream */
   if (mfi->data_kind == 1)
     extract_metadata_icy(mfi, ctx);
+#endif
 
   /* Get some more information on the audio stream */
   if (audio_stream)
diff --git a/src/filescanner_itunes.c b/src/filescanner_itunes.c
index da57720..174dfd6 100644
--- a/src/filescanner_itunes.c
+++ b/src/filescanner_itunes.c
@@ -765,12 +765,12 @@ scan_itunes_itml(char *file)
   int fd;
   int ret;
 
-  DPRINTF(E_INFO, L_SCAN, "Processing iTunes library: %s\n", file);
+  DPRINTF(E_LOG, L_SCAN, "Processing iTunes library: %s\n", file);
 
   fd = open(file, O_RDONLY);
   if (fd < 0)
     {
-      DPRINTF(E_WARN, L_SCAN, "Could not open iTunes library '%s': %s\n", file, strerror(errno));
+      DPRINTF(E_LOG, L_SCAN, "Could not open iTunes library '%s': %s\n", file, strerror(errno));
 
       return;
     }
@@ -778,7 +778,7 @@ scan_itunes_itml(char *file)
   ret = fstat(fd, &sb);
   if (ret < 0)
     {
-      DPRINTF(E_WARN, L_SCAN, "Could not stat iTunes library '%s': %s\n", file, strerror(errno));
+      DPRINTF(E_LOG, L_SCAN, "Could not stat iTunes library '%s': %s\n", file, strerror(errno));
 
       close(fd);
       return;
@@ -787,7 +787,7 @@ scan_itunes_itml(char *file)
   itml_xml = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
   if (itml_xml == MAP_FAILED)
     {
-      DPRINTF(E_WARN, L_SCAN, "Could not map iTunes library: %s\n", strerror(errno));
+      DPRINTF(E_LOG, L_SCAN, "Could not map iTunes library: %s\n", strerror(errno));
 
       close(fd);
       return;
@@ -804,14 +804,14 @@ scan_itunes_itml(char *file)
 
   if (!itml)
     {
-      DPRINTF(E_WARN, L_SCAN, "iTunes XML playlist '%s' failed to parse\n", file);
+      DPRINTF(E_LOG, L_SCAN, "iTunes XML playlist '%s' failed to parse\n", file);
 
       return;
     }
 
   if (plist_get_node_type(itml) != PLIST_DICT)
     {
-      DPRINTF(E_WARN, L_SCAN, "Malformed iTunes XML playlist '%s'\n", file);
+      DPRINTF(E_LOG, L_SCAN, "Malformed iTunes XML playlist '%s'\n", file);
 
       plist_free(itml);
       return;
@@ -829,7 +829,7 @@ scan_itunes_itml(char *file)
   ret = get_dictval_dict_from_key(itml, "Tracks", &node);
   if (ret < 0)
     {
-      DPRINTF(E_WARN, L_SCAN, "Could not find Tracks dict\n");
+      DPRINTF(E_LOG, L_SCAN, "Could not find Tracks dict\n");
 
       plist_free(itml);
       return;
@@ -859,7 +859,7 @@ scan_itunes_itml(char *file)
   ret = process_tracks(node);
   if (ret <= 0)
     {
-      DPRINTF(E_WARN, L_SCAN, "No tracks loaded\n");
+      DPRINTF(E_LOG, L_SCAN, "No tracks loaded\n");
 
       avl_free_tree(itml_to_db);
       plist_free(itml);
@@ -874,7 +874,7 @@ scan_itunes_itml(char *file)
   ret = get_dictval_array_from_key(itml, "Playlists", &node);
   if (ret < 0)
     {
-      DPRINTF(E_WARN, L_SCAN, "Could not find Playlists dict\n");
+      DPRINTF(E_LOG, L_SCAN, "Could not find Playlists dict\n");
 
       avl_free_tree(itml_to_db);
       plist_free(itml);
diff --git a/src/filescanner_m3u.c b/src/filescanner_playlist.c
similarity index 80%
rename from src/filescanner_m3u.c
rename to src/filescanner_playlist.c
index 775096f..b3b5e7e 100644
--- a/src/filescanner_m3u.c
+++ b/src/filescanner_playlist.c
@@ -39,6 +39,9 @@
 #include "filescanner.h"
 #include "misc.h"
 
+/* Formats we can read so far */
+#define PLAYLIST_PLS 1
+#define PLAYLIST_M3U 2
 
 /* Get metadata from the EXTINF tag */
 static int
@@ -71,24 +74,26 @@ extinf_get(char *string, struct media_file_info *mfi, int *extinf)
 }
 
 void
-scan_m3u_playlist(char *file, time_t mtime)
+scan_playlist(char *file, time_t mtime)
 {
   FILE *fp;
   struct media_file_info mfi;
   struct playlist_info *pli;
   struct stat sb;
   char buf[PATH_MAX];
+  char *path;
   char *entry;
   char *filename;
   char *ptr;
   size_t len;
   int extinf;
   int pl_id;
+  int pl_format;
   int mfi_id;
   int ret;
   int i;
 
-  DPRINTF(E_INFO, L_SCAN, "Processing static playlist: %s\n", file);
+  DPRINTF(E_LOG, L_SCAN, "Processing static playlist: %s\n", file);
 
   ret = stat(file, &sb);
   if (ret < 0)
@@ -98,6 +103,17 @@ scan_m3u_playlist(char *file, time_t mtime)
       return;
     }
 
+  ptr = strrchr(file, '.');
+  if (!ptr)
+    return;
+
+  if (strcasecmp(ptr, ".m3u") == 0)
+    pl_format = PLAYLIST_M3U;
+  else if (strcasecmp(ptr, ".pls") == 0)
+    pl_format = PLAYLIST_PLS;
+  else
+    return;
+
   filename = strrchr(file, '/');
   if (!filename)
     filename = file;
@@ -123,7 +139,7 @@ scan_m3u_playlist(char *file, time_t mtime)
   fp = fopen(file, "r");
   if (!fp)
     {
-      DPRINTF(E_WARN, L_SCAN, "Could not open playlist '%s': %s\n", file, strerror(errno));
+      DPRINTF(E_LOG, L_SCAN, "Could not open playlist '%s': %s\n", file, strerror(errno));
 
       return;
     }
@@ -145,7 +161,7 @@ scan_m3u_playlist(char *file, time_t mtime)
       ret = db_pl_add(buf, file, &pl_id);
       if (ret < 0)
 	{
-	  DPRINTF(E_LOG, L_SCAN, "Error adding m3u playlist '%s'\n", file);
+	  DPRINTF(E_LOG, L_SCAN, "Error adding playlist '%s'\n", file);
 
 	  return;
 	}
@@ -159,12 +175,6 @@ scan_m3u_playlist(char *file, time_t mtime)
   while (fgets(buf, sizeof(buf), fp) != NULL)
     {
       len = strlen(buf);
-      if (len >= (sizeof(buf) - 1))
-	{
-	  DPRINTF(E_LOG, L_SCAN, "Playlist entry exceeds PATH_MAX, discarding\n");
-
-	  continue;
-	}
 
       /* rtrim and check that length is sane (ignore blank lines) */
       while ((len > 0) && isspace(buf[len - 1]))
@@ -176,19 +186,29 @@ scan_m3u_playlist(char *file, time_t mtime)
 	continue;
 
       /* Saves metadata in mfi if EXTINF metadata line */
-      if (extinf_get(buf, &mfi, &extinf))
+      if ((pl_format == PLAYLIST_M3U) && extinf_get(buf, &mfi, &extinf))
+	continue;
+
+      /* For pls files we are only interested in the part after the FileX= entry */
+      path = NULL;
+      if ((pl_format == PLAYLIST_PLS) && (strncasecmp(buf, "file", strlen("file")) == 0))
+	path = strchr(buf, '=') + 1;
+      else if (pl_format == PLAYLIST_M3U)
+	path = buf;
+
+      if (!path)
 	continue;
 
       /* Check that first char is sane for a path */
-      if ((!isalnum(buf[0])) && (buf[0] != '/') && (buf[0] != '.'))
+      if ((!isalnum(path[0])) && (path[0] != '/') && (path[0] != '.'))
 	continue;
 
       /* Check if line is an URL, will be added to library */
-      if (strcmp(buf, "http://") > 0)
+      if (strncasecmp(path, "http://", strlen("http://")) == 0)
 	{
 	  DPRINTF(E_DBG, L_SCAN, "Playlist contains URL entry\n");
 
-	  filename = strdup(buf);
+	  filename = strdup(path);
 	  if (!filename)
 	    {
 	      DPRINTF(E_LOG, L_SCAN, "Out of memory for playlist filename\n");
@@ -204,11 +224,11 @@ scan_m3u_playlist(char *file, time_t mtime)
       /* Regular file, should already be in library */
       else
 	{
-	  /* m3u might be from Windows so we change backslash to forward slash */
-	  for (i = 0; i < strlen(buf); i++)
+	  /* Playlist might be from Windows so we change backslash to forward slash */
+	  for (i = 0; i < strlen(path); i++)
 	    {
-	      if (buf[i] == '\\')
-	        buf[i] = '/';
+	      if (path[i] == '\\')
+	        path[i] = '/';
 	    }
 
           /* Now search for the library item where the path has closest match to playlist item */
@@ -217,7 +237,7 @@ scan_m3u_playlist(char *file, time_t mtime)
 	  entry = NULL;
 	  do
 	    {
-	      ptr = strrchr(buf, '/');
+	      ptr = strrchr(path, '/');
 	      if (entry)
 		*(entry - 1) = '/';
 	      if (ptr)
@@ -226,7 +246,7 @@ scan_m3u_playlist(char *file, time_t mtime)
 		  entry = ptr + 1;
 		}
 	      else
-		entry = buf;
+		entry = path;
 
 	      DPRINTF(E_SPAM, L_SCAN, "Playlist entry is now %s\n", entry);
 	      ret = db_files_get_count_bymatch(entry);
diff --git a/src/httpd.c b/src/httpd.c
index ed33974..1652983 100644
--- a/src/httpd.c
+++ b/src/httpd.c
@@ -357,6 +357,8 @@ httpd_stream_file(struct evhttp_request *req, int id)
   struct evkeyvalq *output_headers;
   const char *param;
   const char *param_end;
+  const char *ua;
+  const char *client_codecs;
   char buf[64];
   int64_t offset;
   int64_t end_offset;
@@ -431,7 +433,10 @@ httpd_stream_file(struct evhttp_request *req, int id)
   memset(st, 0, sizeof(struct stream_ctx));
   st->fd = -1;
 
-  transcode = transcode_needed(input_headers, mfi->codectype);
+  ua = evhttp_find_header(input_headers, "User-Agent");
+  client_codecs = evhttp_find_header(input_headers, "Accept-Codecs");
+
+  transcode = transcode_needed(ua, client_codecs, mfi->codectype);
 
   output_headers = evhttp_request_get_output_headers(req);
 
@@ -665,6 +670,9 @@ httpd_send_reply(struct evhttp_request *req, int code, const char *reason, struc
   int zret;
   int ret;
 
+  if (!req)
+    return;
+
   if (!evbuf || (EVBUFFER_LENGTH(evbuf) == 0))
     {
       DPRINTF(E_DBG, L_HTTPD, "Not gzipping body-less reply\n");
diff --git a/src/httpd_daap.c b/src/httpd_daap.c
index a472031..9506b6f 100644
--- a/src/httpd_daap.c
+++ b/src/httpd_daap.c
@@ -34,9 +34,11 @@
 #include <limits.h>
 #include <stdint.h>
 #include <inttypes.h>
+#include <time.h>
 #include <ctype.h>
 
 #include <uninorm.h>
+#include <unistd.h>
 
 #include <avl.h>
 
@@ -50,6 +52,7 @@
 #include "httpd_daap.h"
 #include "daap_query.h"
 #include "dmap_common.h"
+#include "daap_cache.h"
 
 #ifdef HAVE_LIBEVENT2
 # include <event2/http_struct.h>
@@ -71,7 +74,7 @@ extern struct event_base *evbase_httpd;
 struct uri_map {
   regex_t preg;
   char *regexp;
-  void (*handler)(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query);
+  int (*handler)(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua);
 };
 
 struct daap_session {
@@ -241,6 +244,9 @@ daap_session_find(struct evhttp_request *req, struct evkeyvalq *query, struct ev
   const char *param;
   int ret;
 
+  if (!req)
+    return NULL;
+
   param = evhttp_find_header(query, "session-id");
   if (!param)
     {
@@ -545,6 +551,32 @@ user_agent_filter(const char *user_agent, struct query_params *qp)
   free(filter);
 }
 
+/* Returns eg /databases/1/containers from /databases/1/containers?meta=dmap.item... */
+static char *
+extract_uri(char *full_uri)
+{
+  char *uri;
+  char *ptr;
+
+  ptr = strchr(full_uri, '?');
+  if (ptr)
+    *ptr = '\0';
+
+  uri = strdup(full_uri);
+
+  if (ptr)
+    *ptr = '?';
+
+  if (!uri)
+    return NULL;
+
+  ptr = uri;
+  uri = evhttp_decode_uri(uri);
+  free(ptr);
+
+  return uri;
+}
+
 static void
 get_query_params(struct evkeyvalq *query, int *sort_headers, struct query_params *qp)
 {
@@ -729,8 +761,8 @@ parse_meta(struct evhttp_request *req, char *tag, const char *param, const struc
 }
 
 
-static void
-daap_reply_server_info(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_server_info(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   struct evbuffer *content;
   struct evkeyvalq *headers;
@@ -751,7 +783,7 @@ daap_reply_server_info(struct evhttp_request *req, struct evbuffer *evbuf, char
       DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP server-info reply\n");
 
       dmap_send_error(req, "msrv", "Out of memory");
-      return;
+      return -1;
     }
 
   mpro = 2 << 16 | 10;
@@ -822,10 +854,12 @@ daap_reply_server_info(struct evhttp_request *req, struct evbuffer *evbuf, char
   evbuffer_free(content);
 
   httpd_send_reply(req, HTTP_OK, "OK", evbuf);
+
+  return 0;
 }
 
-static void
-daap_reply_content_codes(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_content_codes(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   const struct dmap_field *dmap_fields;
   int nfields;
@@ -845,7 +879,7 @@ daap_reply_content_codes(struct evhttp_request *req, struct evbuffer *evbuf, cha
       DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP content-codes reply\n");
 
       dmap_send_error(req, "mccr", "Out of memory");
-      return;
+      return -1;
     } 
 
   dmap_add_container(evbuf, "mccr", len);
@@ -862,15 +896,15 @@ daap_reply_content_codes(struct evhttp_request *req, struct evbuffer *evbuf, cha
     }
 
   httpd_send_reply(req, HTTP_OK, "OK", evbuf);
+
+  return 0;
 }
 
-static void
-daap_reply_login(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_login(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   struct pairing_info pi;
   struct daap_session *s;
-  struct evkeyvalq *headers;
-  const char *ua;
   const char *param;
   int request_session_id;
   int ret;
@@ -881,11 +915,9 @@ daap_reply_login(struct evhttp_request *req, struct evbuffer *evbuf, char **uri,
       DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP login reply\n");
 
       dmap_send_error(req, "mlog", "Out of memory");
-      return;
+      return -1;
     }
 
-  headers = evhttp_request_get_input_headers(req);
-  ua = evhttp_find_header(headers, "User-Agent");
   if (ua && (strncmp(ua, "Remote", strlen("Remote")) == 0))
     {
       param = evhttp_find_header(query, "pairing-guid");
@@ -894,7 +926,7 @@ daap_reply_login(struct evhttp_request *req, struct evbuffer *evbuf, char **uri,
 	  DPRINTF(E_LOG, L_DAAP, "Login attempt with U-A: Remote and no pairing-guid\n");
 
 	  evhttp_send_error(req, 403, "Forbidden");
-	  return;
+	  return -1;
 	}
 
       memset(&pi, 0, sizeof(struct pairing_info));
@@ -907,7 +939,7 @@ daap_reply_login(struct evhttp_request *req, struct evbuffer *evbuf, char **uri,
 
 	  free_pi(&pi, 1);
 	  evhttp_send_error(req, 403, "Forbidden");
-	  return;
+	  return -1;
 	}
 
       DPRINTF(E_INFO, L_DAAP, "Remote '%s' logging in with GUID %s\n", pi.name, pi.guid);
@@ -931,7 +963,7 @@ daap_reply_login(struct evhttp_request *req, struct evbuffer *evbuf, char **uri,
   if (!s)
     {
       dmap_send_error(req, "mlog", "Could not start session");
-      return;
+      return -1;
     }
 
   dmap_add_container(evbuf, "mlog", 24);
@@ -939,24 +971,28 @@ daap_reply_login(struct evhttp_request *req, struct evbuffer *evbuf, char **uri,
   dmap_add_int(evbuf, "mlid", s->id); /* 12 */
 
   httpd_send_reply(req, HTTP_OK, "OK", evbuf);
+
+  return 0;
 }
 
-static void
-daap_reply_logout(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_logout(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   struct daap_session *s;
 
   s = daap_session_find(req, query, evbuf);
   if (!s)
-    return;
+    return -1;
 
   daap_session_kill(s);
 
   httpd_send_reply(req, 204, "Logout Successful", evbuf);
+
+  return 0;
 }
 
-static void
-daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   struct timeval tv;
   struct daap_session *s;
@@ -968,7 +1004,7 @@ daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
 
   s = daap_session_find(req, query, evbuf);
   if (!s)
-    return;
+    return -1;
 
   param = evhttp_find_header(query, "revision-number");
   if (!param)
@@ -985,7 +1021,7 @@ daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
       DPRINTF(E_LOG, L_DAAP, "Parameter revision-number not an integer\n");
 
       dmap_send_error(req, "mupd", "Invalid request");
-      return;
+      return -1;
     }
 
   if (reqd_rev == 1) /* Or revision is not valid */
@@ -996,7 +1032,7 @@ daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
 	  DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP update reply\n");
 
 	  dmap_send_error(req, "mupd", "Out of memory");
-	  return;
+	  return -1;
 	}
 
       /* Send back current revision */
@@ -1006,7 +1042,7 @@ daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
 
       httpd_send_reply(req, HTTP_OK, "OK", evbuf);
 
-      return;
+      return 0;
     }
 
   /* Else, just let the request hang until we have changes to push back */
@@ -1016,7 +1052,7 @@ daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
       DPRINTF(E_LOG, L_DAAP, "Out of memory for update request\n");
 
       dmap_send_error(req, "mupd", "Out of memory");
-      return;
+      return -1;
     }
   memset(ur, 0, sizeof(struct daap_update_request));
 
@@ -1035,7 +1071,7 @@ daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
 	dmap_send_error(req, "mupd", "Could not register timer");
 	
 	update_free(ur);
-	return;
+	return -1;
       }
   }
 
@@ -1051,17 +1087,21 @@ daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
   evcon = evhttp_request_get_connection(req);
   if (evcon)
     evhttp_connection_set_closecb(evcon, update_fail_cb, ur);
+
+  return 0;
 }
 
-static void
-daap_reply_activity(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_activity(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   /* That's so nice, thanks for letting us know */
   evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf);
+
+  return 0;
 }
 
-static void
-daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   struct evbuffer *content;
   struct daap_session *s;
@@ -1071,7 +1111,7 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
 
   s = daap_session_find(req, query, evbuf);
   if (!s)
-    return;
+    return -1;
 
   lib = cfg_getsec(cfg, "library");
   name = cfg_getstr(lib, "name");
@@ -1082,7 +1122,7 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
       DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DAAP dblist reply\n");
 
       dmap_send_error(req, "avdb", "Out of memory");
-      return;
+      return -1;
     }
 
   dmap_add_int(content, "miid", 1);
@@ -1112,10 +1152,12 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
   evbuffer_free(content);
 
   httpd_send_reply(req, HTTP_OK, "OK", evbuf);
+
+  return 0;
 }
 
-static void
-daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf, int playlist, struct evkeyvalq *query)
+static int
+daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf, int playlist, struct evkeyvalq *query, const char *ua)
 {
   struct daap_session *s;
   struct query_params qp;
@@ -1126,6 +1168,7 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
   const struct dmap_field **meta;
   struct sort_ctx *sctx;
   const char *param;
+  const char *client_codecs;
   char *tag;
   int nmeta;
   int sort_headers;
@@ -1134,8 +1177,8 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
   int ret;
 
   s = daap_session_find(req, query, evbuf);
-  if (!s)
-    return;
+  if (!s && req)
+    return -1;
 
   DPRINTF(E_DBG, L_DAAP, "Fetching song list for playlist %d\n", playlist);
 
@@ -1150,7 +1193,7 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
       DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP song list reply\n");
 
       dmap_send_error(req, tag, "Out of memory");
-      return;
+      return -1;
     }
 
   songlist = evbuffer_new();
@@ -1159,7 +1202,7 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
       DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP song list\n");
 
       dmap_send_error(req, tag, "Out of memory");
-      return;
+      return -1;
     }
 
   /* Start with a big enough evbuffer - it'll expand as needed */
@@ -1218,8 +1261,9 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
 
   memset(&qp, 0, sizeof(struct query_params));
   get_query_params(query, &sort_headers, &qp);
+
   if (playlist == -1)
-    user_agent_filter(s->user_agent, &qp);
+    user_agent_filter(ua, &qp);
 
   sctx = NULL;
   if (sort_headers)
@@ -1260,8 +1304,14 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
     {
       nsongs++;
 
-      headers = evhttp_request_get_input_headers(req);
-      transcode = transcode_needed(headers, dbmfi.codectype);
+      client_codecs = NULL;
+      if (req)
+	{
+	  headers = evhttp_request_get_input_headers(req);
+	  client_codecs = evhttp_find_header(headers, "Accept-Codecs");
+	}
+
+      transcode = transcode_needed(ua, client_codecs, dbmfi.codectype);
 
       ret = dmap_encode_file_metadata(songlist, song, &dbmfi, meta, nmeta, sort_headers, transcode);
       if (ret < 0)
@@ -1284,7 +1334,7 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
 	    }
    	}
 
-      DPRINTF(E_DBG, L_DAAP, "Done with song\n");
+      DPRINTF(E_SPAM, L_DAAP, "Done with song\n");
     }
 
   DPRINTF(E_DBG, L_DAAP, "Done with song list, %d songs\n", nsongs);
@@ -1342,7 +1392,7 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
       if (sort_headers)
 	daap_sort_context_free(sctx);
 
-      return;
+      return -1;
     }
 
   if (sort_headers)
@@ -1356,13 +1406,13 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
 	  DPRINTF(E_LOG, L_DAAP, "Could not add sort headers to DAAP song list reply\n");
 
 	  dmap_send_error(req, tag, "Out of memory");
-	  return;
+	  return -1;
 	}
     }
 
   httpd_send_reply(req, HTTP_OK, "OK", evbuf);
 
-  return;
+  return 0;
 
  out_query_free:
   if (nmeta > 0)
@@ -1376,16 +1426,18 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
 
  out_list_free:
   evbuffer_free(songlist);
+
+  return -1;
 }
 
-static void
-daap_reply_dbsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_dbsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
-  daap_reply_songlist_generic(req, evbuf, -1, query);
+  return daap_reply_songlist_generic(req, evbuf, -1, query, ua);
 }
 
-static void
-daap_reply_plsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_plsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   int playlist;
   int ret;
@@ -1395,14 +1447,14 @@ daap_reply_plsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char *
     {
       dmap_send_error(req, "apso", "Invalid playlist ID");
 
-      return;
+      return -1;
     }
 
-  daap_reply_songlist_generic(req, evbuf, playlist, query);
+  return daap_reply_songlist_generic(req, evbuf, playlist, query, ua);
 }
 
-static void
-daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   struct query_params qp;
   struct db_playlist_info dbpli;
@@ -1424,7 +1476,7 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
 
   s = daap_session_find(req, query, evbuf);
   if (!s)
-    return;
+    return -1;
 
   ret = evbuffer_expand(evbuf, 61);
   if (ret < 0)
@@ -1432,7 +1484,7 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
       DPRINTF(E_LOG, L_DAAP, "Could not expand evbuffer for DAAP playlists reply\n");
 
       dmap_send_error(req, "aply", "Out of memory");
-      return;
+      return -1;
     }
 
   playlistlist = evbuffer_new();
@@ -1441,7 +1493,7 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
       DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP playlist list\n");
 
       dmap_send_error(req, "aply", "Out of memory");
-      return;
+      return -1;
     }
 
   /* Start with a big enough evbuffer - it'll expand as needed */
@@ -1626,12 +1678,12 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
       DPRINTF(E_LOG, L_DAAP, "Could not add playlist list to DAAP playlists reply\n");
 
       dmap_send_error(req, "aply", "Out of memory");
-      return;
+      return -1;
     }
 
   httpd_send_reply(req, HTTP_OK, "OK", evbuf);
 
-  return;
+  return 0;
 
  out_query_free:
   free(meta);
@@ -1643,10 +1695,12 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
 
  out_list_free:
   evbuffer_free(playlistlist);
+
+  return -1;
 }
 
-static void
-daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   struct query_params qp;
   struct db_group_info dbgri;
@@ -1668,13 +1722,14 @@ daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
   char *tag;
 
   s = daap_session_find(req, query, evbuf);
-  if (!s)
-    return;
+  if (!s && req)
+    return -1;
 
   memset(&qp, 0, sizeof(struct query_params));
 
   get_query_params(query, &sort_headers, &qp);
-  user_agent_filter(s->user_agent, &qp);
+
+  user_agent_filter(ua, &qp);
 
   param = evhttp_find_header(query, "group-type");
   if (strcmp(param, "artists") == 0)
@@ -1906,7 +1961,7 @@ daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
       if (sort_headers)
 	daap_sort_context_free(sctx);
 
-      return;
+      return -1;
     }
 
   if (sort_headers)
@@ -1920,13 +1975,13 @@ daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
 	  DPRINTF(E_LOG, L_DAAP, "Could not add sort headers to DAAP groups reply\n");
 
 	  dmap_send_error(req, tag, "Out of memory");
-	  return;
+	  return -1;
 	}
     }
 
   httpd_send_reply(req, HTTP_OK, "OK", evbuf);
 
-  return;
+  return 0;
 
  out_query_free:
   free(meta);
@@ -1940,10 +1995,12 @@ daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
  out_qfilter_free:
   if (qp.filter)
     free(qp.filter);
+
+  return -1;
 }
 
-static void
-daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   struct query_params qp;
   struct daap_session *s;
@@ -1957,13 +2014,14 @@ daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
   int ret;
 
   s = daap_session_find(req, query, evbuf);
-  if (!s)
-    return;
+  if (!s && req)
+    return -1;
 
   memset(&qp, 0, sizeof(struct query_params));
 
   get_query_params(query, &sort_headers, &qp);
-  user_agent_filter(s->user_agent, &qp);
+
+  user_agent_filter(ua, &qp);
 
   if (strcmp(uri[3], "artists") == 0)
     {
@@ -1992,6 +2050,8 @@ daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
       DPRINTF(E_LOG, L_DAAP, "Invalid DAAP browse request type '%s'\n", uri[3]);
 
       dmap_send_error(req, "abro", "Invalid browse type");
+      ret = -1;
+
       goto out_qfilter_free;
     }
 
@@ -2010,6 +2070,8 @@ daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
       DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP browse item list\n");
 
       dmap_send_error(req, "abro", "Out of memory");
+      ret = -1;
+
       goto out_qfilter_free;
     }
 
@@ -2033,6 +2095,7 @@ daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
 	  DPRINTF(E_LOG, L_DAAP, "Could not create sort context\n");
 
 	  dmap_send_error(req, "abro", "Out of memory");
+	  ret = -1;
 
           goto out_itemlist_free;
 	}
@@ -2120,6 +2183,8 @@ daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
 
   httpd_send_reply(req, HTTP_OK, "OK", evbuf);
 
+  ret = 0;
+
  out_sort_headers_free:
   if (sort_headers)
     daap_sort_context_free(sctx);
@@ -2130,11 +2195,13 @@ daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
  out_qfilter_free:
   if (qp.filter)
     free(qp.filter);
+
+  return ret;
 }
 
 /* NOTE: We only handle artwork at the moment */
-static void
-daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   char clen[32];
   struct daap_session *s;
@@ -2148,13 +2215,13 @@ daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char *
 
   s = daap_session_find(req, query, evbuf);
   if (!s)
-    return;
+    return -1;
 
   ret = safe_atoi32(uri[3], &id);
   if (ret < 0)
     {
       evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
-      return;
+      return -1;
     }
 
   if (evhttp_find_header(query, "mw") && evhttp_find_header(query, "mh"))
@@ -2166,7 +2233,7 @@ daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char *
 	  DPRINTF(E_LOG, L_DAAP, "Could not convert mw parameter to integer\n");
 
 	  evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
-	  return;
+	  return -1;
 	}
 
       param = evhttp_find_header(query, "mh");
@@ -2176,7 +2243,7 @@ daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char *
 	  DPRINTF(E_LOG, L_DAAP, "Could not convert mh parameter to integer\n");
 
 	  evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
-	  return;
+	  return -1;
 	}
     }
   else
@@ -2217,14 +2284,15 @@ daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char *
 
   /* No gzip compression for artwork */
   evhttp_send_reply(req, HTTP_OK, "OK", evbuf);
-  return;
+  return 0;
 
  no_artwork:
   evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf);
+  return -1;
 }
 
-static void
-daap_stream(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_stream(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   struct daap_session *s;
   int id;
@@ -2232,13 +2300,15 @@ daap_stream(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, stru
 
   s = daap_session_find(req, query, evbuf);
   if (!s)
-    return;
+    return -1;
 
   ret = safe_atoi32(uri[3], &id);
   if (ret < 0)
     evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
   else
     httpd_stream_file(req, id);
+
+  return ret;
 }
 
 
@@ -2283,8 +2353,8 @@ static const struct dmap_field dmap_TST7 = { "test.ulong",     "TST7", NULL, DMA
 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 void
-daap_reply_dmap_test(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
+static int
+daap_reply_dmap_test(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query, const char *ua)
 {
   char buf[64];
   struct evbuffer *test;
@@ -2296,7 +2366,7 @@ daap_reply_dmap_test(struct evhttp_request *req, struct evbuffer *evbuf, char **
       DPRINTF(E_LOG, L_DAAP, "Could not create evbuffer for DMAP test\n");
 
       dmap_send_error(req, dmap_TEST.tag, "Out of memory");
-      return;
+      return -1;
     }
 
   /* UBYTE */
@@ -2361,10 +2431,12 @@ daap_reply_dmap_test(struct evhttp_request *req, struct evbuffer *evbuf, char **
       DPRINTF(E_LOG, L_DAAP, "Could not add test results to DMAP test reply\n");
 
       dmap_send_error(req, dmap_TEST.tag, "Out of memory");
-      return;      
+      return -1;      
     }
 
   evhttp_send_reply(req, HTTP_OK, "OK", evbuf);
+
+  return 0;
 }
 #endif /* DMAP_TEST */
 
@@ -2455,10 +2527,13 @@ daap_request(struct evhttp_request *req)
   struct evbuffer *evbuf;
   struct evkeyvalq query;
   struct evkeyvalq *headers;
+  struct timespec start;
+  struct timespec end;
   const char *ua;
   cfg_t *lib;
   char *libname;
   char *passwd;
+  int msec;
   int handler;
   int ret;
   int i;
@@ -2494,11 +2569,7 @@ daap_request(struct evhttp_request *req)
       full_uri = uri;
     }
 
-  ptr = strchr(full_uri, '?');
-  if (ptr)
-    *ptr = '\0';
-
-  uri = strdup(full_uri);
+  uri = extract_uri(full_uri);
   if (!uri)
     {
       free(full_uri);
@@ -2506,13 +2577,6 @@ daap_request(struct evhttp_request *req)
       return;
     }
 
-  if (ptr)
-    *ptr = '?';
-
-  ptr = uri;
-  uri = evhttp_decode_uri(uri);
-  free(ptr);
-
   DPRINTF(E_DBG, L_DAAP, "DAAP request: %s\n", full_uri);
 
   handler = -1;
@@ -2594,6 +2658,29 @@ daap_request(struct evhttp_request *req)
       return;
     }
 
+  // Set reply headers
+  headers = evhttp_request_get_output_headers(req);
+  evhttp_add_header(headers, "Accept-Ranges", "bytes");
+  evhttp_add_header(headers, "DAAP-Server", "forked-daapd/" 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.
+   */
+  evhttp_add_header(headers, "Content-Type", "application/x-dmap-tagged");
+
+  // Try the cache
+  evbuf = daapcache_get(full_uri);
+  if (evbuf)
+    {
+      httpd_send_reply(req, HTTP_OK, "OK", evbuf); // TODO not all want this reply
+
+      evbuffer_free(evbuf);
+      free(uri);
+      free(full_uri);
+      return;
+    }
+
+  // No cache, so prepare handler arguments and send to the handler
   evbuf = evbuffer_new();
   if (!evbuf)
     {
@@ -2608,19 +2695,21 @@ daap_request(struct evhttp_request *req)
 
   evhttp_parse_query(full_uri, &query);
 
-  headers = evhttp_request_get_output_headers(req);
-  evhttp_add_header(headers, "Accept-Ranges", "bytes");
-  evhttp_add_header(headers, "DAAP-Server", "forked-daapd/" 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.
-   */
-  evhttp_add_header(headers, "Content-Type", "application/x-dmap-tagged");
+  clock_gettime(CLOCK_MONOTONIC, &start);
 
-  daap_handlers[handler].handler(req, evbuf, uri_parts, &query);
+  daap_handlers[handler].handler(req, evbuf, uri_parts, &query, ua);
+
+  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_DB, "DAAP request handled in %d milliseconds\n", msec);
+
+  if (msec > daapcache_threshold())
+    daapcache_add(full_uri, ua, msec);
 
-  evbuffer_free(evbuf);
   evhttp_clear_headers(&query);
+  evbuffer_free(evbuf);
   free(uri);
   free(full_uri);
 }
@@ -2657,6 +2746,87 @@ daap_is_request(struct evhttp_request *req, char *uri)
   return 0;
 }
 
+struct evbuffer *
+daap_reply_build(char *full_uri, const char *ua)
+{
+  char *uri;
+  char *ptr;
+  char *uri_parts[7];
+  struct evbuffer *evbuf;
+  struct evkeyvalq query;
+  int handler;
+  int ret;
+  int i;
+
+  DPRINTF(E_DBG, L_DAAP, "Building reply for DAAP request: %s\n", full_uri);
+
+  uri = extract_uri(full_uri);
+  if (!uri)
+    {
+      DPRINTF(E_LOG, L_DAAP, "Error extracting DAAP request: %s\n", full_uri);
+
+      return NULL;
+    }
+
+  handler = -1;
+  for (i = 0; daap_handlers[i].handler; i++)
+    {
+      ret = regexec(&daap_handlers[i].preg, uri, 0, NULL, 0);
+      if (ret == 0)
+        {
+          handler = i;
+          break;
+        }
+    }
+
+  if (handler < 0)
+    {
+      DPRINTF(E_LOG, L_DAAP, "Unrecognized DAAP request: %s\n", full_uri);
+
+      free(uri);
+      return NULL;
+    }
+
+  memset(uri_parts, 0, sizeof(uri_parts));
+
+  uri_parts[0] = strtok_r(uri, "/", &ptr);
+  for (i = 1; (i < sizeof(uri_parts) / sizeof(uri_parts[0])) && uri_parts[i - 1]; i++)
+    {
+      uri_parts[i] = strtok_r(NULL, "/", &ptr);
+    }
+
+  if (!uri_parts[0] || uri_parts[i - 1] || (i < 2))
+    {
+      DPRINTF(E_LOG, L_DAAP, "DAAP URI has too many/few components (%d)\n", (uri_parts[0]) ? i : 0);
+
+      free(uri);
+      return NULL;
+    }
+
+  evbuf = evbuffer_new();
+  if (!evbuf)
+    {
+      DPRINTF(E_LOG, L_DAAP, "Could not allocate evbuffer for building DAAP reply\n");
+
+      free(uri);
+      return NULL;
+    }
+
+  evhttp_parse_query(full_uri, &query);
+
+  ret = daap_handlers[handler].handler(NULL, evbuf, uri_parts, &query, ua);
+  if (ret < 0)
+    {
+      evbuffer_free(evbuf);
+      evbuf = NULL;
+    }
+
+  evhttp_clear_headers(&query);
+  free(uri);
+
+  return evbuf;
+}
+
 int
 daap_init(void)
 {
diff --git a/src/httpd_daap.h b/src/httpd_daap.h
index f04c339..bf18c9a 100644
--- a/src/httpd_daap.h
+++ b/src/httpd_daap.h
@@ -21,4 +21,7 @@ daap_request(struct evhttp_request *req);
 int
 daap_is_request(struct evhttp_request *req, char *uri);
 
+struct evbuffer *
+daap_reply_build(char *full_uri, const char *ua);
+
 #endif /* !__HTTPD_DAAP_H__ */
diff --git a/src/httpd_dacp.c b/src/httpd_dacp.c
index 707f712..e24f61b 100644
--- a/src/httpd_dacp.c
+++ b/src/httpd_dacp.c
@@ -1417,6 +1417,8 @@ dacp_reply_playqueueedit_add(struct evhttp_request *req, struct evbuffer *evbuf,
   int ret;
   int quirkyquery;
 
+  mode = 1;
+
   param = evhttp_find_header(query, "mode");
   if (param)
     {
@@ -1447,8 +1449,8 @@ dacp_reply_playqueueedit_add(struct evhttp_request *req, struct evbuffer *evbuf,
 	sort = "album";
       }
 
-      // only use queryfilter if mode is not equal 3 (play next) or 5 (add to up next)
-      queuefilter = (mode == 3 || mode == 5) ? NULL : evhttp_find_header(query, "queuefilter");
+      // only use queryfilter if mode is not equal 0 (add to up next), 3 (play next) or 5 (add to up next)
+      queuefilter = (mode == 0 || mode == 3 || mode == 5) ? NULL : evhttp_find_header(query, "queuefilter");
 
       querymodifier = evhttp_find_header(query, "query-modifier");
       if (!querymodifier || (strcmp(querymodifier, "containers") != 0))
@@ -2104,7 +2106,7 @@ dacp_reply_setspeakers(struct evhttp_request *req, struct evbuffer *evbuf, char
 	}
       else
 	{
-	  DPRINTF(E_DBG, L_DACP, "Speaker id converted with ret %d, param %s, dec val %llu.\n", ret, param, ids[i]);
+	  DPRINTF(E_DBG, L_DACP, "Speaker id converted with ret %d, param %s, dec val %" PRIu64 ".\n", ret, param, ids[i]);
 	}
       i++;
     }
diff --git a/src/httpd_rsp.c b/src/httpd_rsp.c
index e16438e..633f1a6 100644
--- a/src/httpd_rsp.c
+++ b/src/httpd_rsp.c
@@ -439,6 +439,8 @@ rsp_reply_playlist(struct evhttp_request *req, char **uri, struct evkeyvalq *que
   struct db_media_file_info dbmfi;
   struct evkeyvalq *headers;
   const char *param;
+  const char *ua;
+  const char *client_codecs;
   char **strval;
   mxml_node_t *reply;
   mxml_node_t *status;
@@ -531,7 +533,11 @@ rsp_reply_playlist(struct evhttp_request *req, char **uri, struct evkeyvalq *que
   while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id))
     {
       headers = evhttp_request_get_input_headers(req);
-      transcode = transcode_needed(headers, dbmfi.codectype);
+
+      ua = evhttp_find_header(headers, "User-Agent");
+      client_codecs = evhttp_find_header(headers, "Accept-Codecs");
+
+      transcode = transcode_needed(ua, client_codecs, dbmfi.codectype);
 
       /* Item block (one item) */
       item = mxmlNewElement(items, "item");
diff --git a/src/lastfm.c b/src/lastfm.c
new file mode 100644
index 0000000..4782a91
--- /dev/null
+++ b/src/lastfm.c
@@ -0,0 +1,989 @@
+/*
+ * Copyright (C) 2014 Espen Jürgensen <espenjurgensen at gmail.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 <inttypes.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <time.h>
+#include <string.h>
+#include <pthread.h>
+
+#include <gcrypt.h>
+#include <mxml.h>
+#include <event2/event.h>
+#include <event2/buffer.h>
+#include <event2/http.h>
+#include <curl/curl.h>
+
+#include "db.h"
+#include "lastfm.h"
+#include "logger.h"
+#include "misc.h"
+
+
+struct lastfm_command;
+
+typedef int (*cmd_func)(struct lastfm_command *cmd);
+
+struct lastfm_command
+{
+  pthread_mutex_t lck;
+  pthread_cond_t cond;
+
+  cmd_func func;
+
+  int nonblock;
+
+  union {
+    void *noarg;
+    int id;
+    struct keyval *kv;
+  } arg;
+
+  int ret;
+};
+
+struct https_client_ctx
+{
+  const char *url;
+  const char *body;
+  struct evbuffer *data;
+};
+
+
+/* --- Globals --- */
+// lastfm thread
+static pthread_t tid_lastfm;
+
+// Event base, pipes and events
+struct event_base *evbase_lastfm;
+static int g_exit_pipe[2];
+static int g_cmd_pipe[2];
+static struct event *g_exitev;
+static struct event *g_cmdev;
+
+// Tells us if the LastFM thread has been set up
+static int g_initialized = 0;
+
+// LastFM becomes disabled if we get a scrobble, try initialising session,
+// but can't (probably no session key in db because user does not use LastFM)
+static int g_disabled = 0;
+
+/**
+ * The API key and secret (not so secret being open source) is specific to 
+ * forked-daapd, and is used to identify forked-daapd and to sign requests
+ */
+const char *g_api_key = "579593f2ed3f49673c7364fd1c9c829b";
+const char *g_secret = "ce45a1d275c10b3edf0ecfa27791cb2b";
+
+const char *api_url = "http://ws.audioscrobbler.com/2.0/";
+const char *auth_url = "https://ws.audioscrobbler.com/2.0/";
+
+// Session key
+char *g_session_key = NULL;
+
+
+
+/* --------------------------------- HELPERS ------------------------------- */
+
+/* Reads a LastFM credentials file (1st line username, 2nd line password) */
+static int
+credentials_read(char *path, char **username, char **password)
+{
+  FILE *fp;
+  char *u;
+  char *p;
+  char buf[256];
+  int len;
+
+  fp = fopen(path, "rb");
+  if (!fp)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not open lastfm credentials file %s: %s\n", path, strerror(errno));
+      return -1;
+    }
+
+  u = fgets(buf, sizeof(buf), fp);
+  if (!u)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Empty lastfm credentials file %s\n", path);
+
+      fclose(fp);
+      return -1;
+    }
+
+  len = strlen(u);
+  if (buf[len - 1] != '\n')
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Invalid lastfm credentials file %s: username name too long or missing password\n", path);
+
+      fclose(fp);
+      return -1;
+    }
+
+  while (len)
+    {
+      if ((buf[len - 1] == '\r') || (buf[len - 1] == '\n'))
+	{
+	  buf[len - 1] = '\0';
+	  len--;
+	}
+      else
+	break;
+    }
+
+  if (!len)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Invalid lastfm credentials file %s: empty line where username expected\n", path);
+
+      fclose(fp);
+      return -1;
+    }
+
+  u = strdup(buf);
+  if (!u)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Out of memory for username while reading %s\n", path);
+
+      fclose(fp);
+      return -1;
+    }
+
+  p = fgets(buf, sizeof(buf), fp);
+  fclose(fp);
+  if (!p)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Invalid lastfm credentials file %s: no password\n", path);
+
+      free(u);
+      return -1;
+    }
+
+  len = strlen(p);
+
+  while (len)
+    {
+      if ((buf[len - 1] == '\r') || (buf[len - 1] == '\n'))
+	{
+	  buf[len - 1] = '\0';
+	  len--;
+	}
+      else
+	break;
+    }
+
+  p = strdup(buf);
+  if (!p)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Out of memory for password while reading %s\n", path);
+
+      free(u);
+      return -1;
+    }
+
+  DPRINTF(E_LOG, L_LASTFM, "lastfm credentials file OK, logging in with username %s\n", u);
+
+  *username = u;
+  *password = p;
+
+  return 0;
+}
+
+/* Converts parameters to a string in application/x-www-form-urlencoded format */
+static int
+body_print(char **body, struct keyval *kv)
+{
+  struct evbuffer *evbuf;
+  struct onekeyval *okv;
+  char *k;
+  char *v;
+
+  evbuf = evbuffer_new();
+
+  for (okv = kv->head; okv; okv = okv->next)
+    {
+      k = evhttp_encode_uri(okv->name);
+      if (!k)
+        continue;
+
+      v = evhttp_encode_uri(okv->value);
+      if (!v)
+	{
+	  free(k);
+	  continue;
+	}
+
+      evbuffer_add(evbuf, k, strlen(k));
+      evbuffer_add(evbuf, "=", 1);
+      evbuffer_add(evbuf, v, strlen(v));
+      if (okv->next)
+	evbuffer_add(evbuf, "&", 1);
+
+      free(k);
+      free(v);
+    }
+
+  evbuffer_add(evbuf, "\n", 1);
+
+  *body = evbuffer_readln(evbuf, NULL, EVBUFFER_EOL_ANY);
+
+  evbuffer_free(evbuf);
+
+  DPRINTF(E_DBG, L_LASTFM, "Parameters in request are: %s\n", *body);
+
+  return 0;
+}
+
+/* Creates an md5 signature of the concatenated parameters and adds it to keyval */
+static int
+param_sign(struct keyval *kv)
+{
+  struct onekeyval *okv;
+
+  char hash[33];
+  char ebuf[64];
+  uint8_t *hash_bytes;
+  size_t hash_len;
+  gcry_md_hd_t md_hdl;
+  gpg_error_t gc_err;
+  int ret;
+  int i;
+
+  gc_err = gcry_md_open(&md_hdl, GCRY_MD_MD5, 0);
+  if (gc_err != GPG_ERR_NO_ERROR)
+    {
+      gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
+      DPRINTF(E_LOG, L_LASTFM, "Could not open MD5: %s\n", ebuf);
+      return -1;
+    }
+
+  for (okv = kv->head; okv; okv = okv->next)
+    {
+      gcry_md_write(md_hdl, okv->name, strlen(okv->name));
+      gcry_md_write(md_hdl, okv->value, strlen(okv->value));
+    }  
+
+  gcry_md_write(md_hdl, g_secret, strlen(g_secret));
+
+  hash_bytes = gcry_md_read(md_hdl, GCRY_MD_MD5);
+  if (!hash_bytes)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not read MD5 hash\n");
+      return -1;
+    }
+
+  hash_len = gcry_md_get_algo_dlen(GCRY_MD_MD5);
+
+  for (i = 0; i < hash_len; i++)
+    sprintf(hash + (2 * i), "%02x", hash_bytes[i]);
+
+  ret = keyval_add(kv, "api_sig", hash);
+
+  gcry_md_close(md_hdl);
+
+  return ret;
+}
+
+/* For compability with mxml 2.6 */
+#ifndef HAVE_MXML_GETOPAQUE
+const char *				/* O - Opaque string or NULL */
+mxmlGetOpaque(mxml_node_t *node)	/* I - Node to get */
+{
+  if (!node)
+    return (NULL);
+
+  if (node->type == MXML_OPAQUE)
+    return (node->value.opaque);
+  else if (node->type == MXML_ELEMENT &&
+           node->child &&
+	   node->child->type == MXML_OPAQUE)
+    return (node->child->value.opaque);
+  else
+    return (NULL);
+}
+#endif
+
+/* ---------------------------- COMMAND EXECUTION -------------------------- */
+
+static int
+send_command(struct lastfm_command *cmd)
+{
+  int ret;
+
+  if (!cmd->func)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "BUG: cmd->func is NULL!\n");
+      return -1;
+    }
+
+  ret = write(g_cmd_pipe[1], &cmd, sizeof(cmd));
+  if (ret != sizeof(cmd))
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not send command: %s\n", strerror(errno));
+      return -1;
+    }
+
+  return 0;
+}
+
+static int
+nonblock_command(struct lastfm_command *cmd)
+{
+  int ret;
+
+  ret = send_command(cmd);
+  if (ret < 0)
+    return -1;
+
+  return 0;
+}
+
+/* Thread: main */
+static void
+thread_exit(void)
+{
+  int dummy = 42;
+
+  DPRINTF(E_DBG, L_LASTFM, "Killing lastfm thread\n");
+
+  if (write(g_exit_pipe[1], &dummy, sizeof(dummy)) != sizeof(dummy))
+    DPRINTF(E_LOG, L_LASTFM, "Could not write to exit fd: %s\n", strerror(errno));
+}
+
+
+
+/* --------------------------------- MAIN --------------------------------- */
+/*                              Thread: lastfm                              */
+
+static size_t
+request_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
+{
+  size_t realsize;
+  struct https_client_ctx *ctx;
+  int ret;
+
+  realsize = size * nmemb;
+  ctx = (struct https_client_ctx *)userdata;
+
+  ret = evbuffer_add(ctx->data, ptr, realsize);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Error adding reply from LastFM to data buffer\n");
+      return 0;
+    }
+
+  return realsize;
+}
+
+static void
+response_proces(struct https_client_ctx *ctx)
+{
+  mxml_node_t *tree;
+  mxml_node_t *s_node;
+  mxml_node_t *e_node;
+  char *body;
+  char *errmsg;
+  char *sk;
+
+  // NULL-terminate the buffer
+  evbuffer_add(ctx->data, "", 1);
+
+  body = (char *)evbuffer_pullup(ctx->data, -1);
+  if (!body || (strlen(body) == 0))
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Empty response\n");
+      return;
+    }
+
+  DPRINTF(E_SPAM, L_LASTFM, "LastFM response:\n%s\n", body);
+
+  tree = mxmlLoadString(NULL, body, MXML_OPAQUE_CALLBACK);
+  if (!tree)
+    return;
+
+  // Look for errors
+  e_node = mxmlFindElement(tree, tree, "error", NULL, NULL, MXML_DESCEND);
+  if (e_node)
+    {
+      errmsg = trimwhitespace(mxmlGetOpaque(e_node));
+      DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", errmsg);
+
+      if (errmsg)
+	free(errmsg);
+      mxmlDelete(tree);
+      return;
+    }
+
+  // Was it a scrobble request? Then do nothing. TODO: Check for error messages
+  s_node = mxmlFindElement(tree, tree, "scrobbles", NULL, NULL, MXML_DESCEND);
+  if (s_node)
+    {
+      DPRINTF(E_DBG, L_LASTFM, "Scrobble callback\n");
+      mxmlDelete(tree);
+      return;
+    }
+
+  // Otherwise an auth request, so get the session key
+  s_node = mxmlFindElement(tree, tree, "key", NULL, NULL, MXML_DESCEND);
+  if (!s_node)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Session key not found\n");
+      mxmlDelete(tree);
+      return;
+    }
+
+  sk = trimwhitespace(mxmlGetOpaque(s_node));
+  if (sk)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Got session key from LastFM: %s\n", sk);
+      db_admin_add("lastfm_sk", sk);
+
+      if (g_session_key)
+	free(g_session_key);
+
+      g_session_key = sk;
+    }
+
+  mxmlDelete(tree);
+}
+
+// We use libcurl to make the request. We could use libevent and avoid the
+// dependency, but for SSL, libevent needs to be v2.1 or better, which is still
+// a bit too new to be in the major distros
+static int
+https_client_request(struct https_client_ctx *ctx)
+{
+  CURL *curl;
+  CURLcode res;
+ 
+  curl = curl_easy_init();
+  if (!curl)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Error: Could not get curl handle\n");
+      return -1;
+    }
+
+  curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcurl-agent/1.0");
+  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ctx->body);
+  curl_easy_setopt(curl, CURLOPT_URL, ctx->url);
+
+  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, request_cb);
+  curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx);
+
+  ctx->data = evbuffer_new();
+  if (!ctx->data)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not create evbuffer for LastFM response\n");
+      curl_easy_cleanup(curl);
+      return -1;
+    }
+
+  res = curl_easy_perform(curl);
+  if (res != CURLE_OK)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res));
+      curl_easy_cleanup(curl);
+      return -1;
+    }
+
+  response_proces(ctx);
+
+  evbuffer_free(ctx->data);
+ 
+  curl_easy_cleanup(curl);
+
+  return 0;
+}
+
+static int
+request_post(char *method, struct keyval *kv, int auth)
+{
+  struct https_client_ctx ctx;
+  char *body;
+  int ret;
+
+  ret = keyval_add(kv, "method", method);
+  if (ret < 0)
+    return -1;
+
+  if (!auth)
+    ret = keyval_add(kv, "sk", g_session_key);
+  if (ret < 0)
+    return -1;
+
+  // API requires that we MD5 sign sorted param (without "format" param)
+  keyval_sort(kv);
+  ret = param_sign(kv);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Aborting request, param_sign failed\n");
+      return -1;
+    }
+
+  ret = body_print(&body, kv);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Aborting request, body_print failed\n");
+      return -1;
+    }
+
+  memset(&ctx, 0, sizeof(struct https_client_ctx));
+  ctx.url = auth ? auth_url : api_url;
+  ctx.body = body;
+
+  ret = https_client_request(&ctx);
+
+  return ret;
+}
+
+static int
+login(struct lastfm_command *cmd)
+{
+  request_post("auth.getMobileSession", cmd->arg.kv, 1);
+
+  keyval_clear(cmd->arg.kv);
+
+  return 0;
+}
+
+static int
+scrobble(struct lastfm_command *cmd)
+{
+  struct media_file_info *mfi;
+  struct keyval *kv;
+  char duration[4];
+  char trackNumber[4];
+  char timestamp[16];
+  int ret;
+
+  mfi = db_file_fetch_byid(cmd->arg.id);
+  if (!mfi)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Scrobble failed, track id %d is unknown\n", cmd->arg.id);
+      return -1;
+    }
+
+  // Don't scrobble songs which are shorter than 30 sec
+  if (mfi->song_length < 30000)
+    goto noscrobble;
+
+  // Don't scrobble non-music and radio stations
+  if ((mfi->media_kind != 1) || (mfi->data_kind == 1))
+    goto noscrobble;
+
+  // Don't scrobble songs with unknown artist
+  if (strcmp(mfi->artist, "Unknown artist") == 0)
+    goto noscrobble;
+
+  kv = keyval_alloc();
+  if (!kv)
+    goto noscrobble;
+
+  snprintf(duration, sizeof(duration), "%" PRIu32, mfi->song_length);
+  snprintf(trackNumber, sizeof(trackNumber), "%" PRIu32, mfi->track);
+  snprintf(timestamp, sizeof(timestamp), "%" PRIi64, (int64_t)time(NULL));
+
+  ret = ( (keyval_add(kv, "api_key", g_api_key) == 0) &&
+          (keyval_add(kv, "sk", g_session_key) == 0) &&
+          (keyval_add(kv, "artist", mfi->artist) == 0) &&
+          (keyval_add(kv, "track", mfi->title) == 0) &&
+          (keyval_add(kv, "album", mfi->album) == 0) &&
+          (keyval_add(kv, "albumArtist", mfi->album_artist) == 0) &&
+          (keyval_add(kv, "duration", duration) == 0) &&
+          (keyval_add(kv, "trackNumber", trackNumber) == 0) &&
+          (keyval_add(kv, "timestamp", timestamp) == 0)
+        );
+
+  free_mfi(mfi, 0);
+
+  if (!ret)
+    {
+      keyval_clear(kv);
+      return -1;
+    }
+
+  DPRINTF(E_INFO, L_LASTFM, "Scrobbling '%s' by '%s'\n", keyval_get(kv, "track"), keyval_get(kv, "artist"));
+
+  request_post("track.scrobble", kv, 0);
+
+  keyval_clear(kv);
+
+  return 0;
+
+ noscrobble:
+  free_mfi(mfi, 0);
+
+  return -1;
+}
+
+
+
+static void *
+lastfm(void *arg)
+{
+  int ret;
+
+  DPRINTF(E_DBG, L_LASTFM, "Main loop initiating\n");
+
+  ret = db_perthread_init();
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Error: DB init failed\n");
+      pthread_exit(NULL);
+    }
+
+  event_base_dispatch(evbase_lastfm);
+
+  if (g_initialized)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "LastFM event loop terminated ahead of time!\n");
+      g_initialized = 0;
+    }
+
+  db_perthread_deinit();
+
+  DPRINTF(E_DBG, L_LASTFM, "Main loop terminating\n");
+
+  pthread_exit(NULL);
+}
+
+static void
+exit_cb(int fd, short what, void *arg)
+{
+  int dummy;
+  int ret;
+
+  ret = read(g_exit_pipe[0], &dummy, sizeof(dummy));
+  if (ret != sizeof(dummy))
+    DPRINTF(E_LOG, L_LASTFM, "Error reading from exit pipe\n");
+
+  event_base_loopbreak(evbase_lastfm);
+
+  g_initialized = 0;
+
+  event_add(g_exitev, NULL);
+}
+
+static void
+command_cb(int fd, short what, void *arg)
+{
+  struct lastfm_command *cmd;
+  int ret;
+
+  ret = read(g_cmd_pipe[0], &cmd, sizeof(cmd));
+  if (ret != sizeof(cmd))
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not read command! (read %d): %s\n", ret, (ret < 0) ? strerror(errno) : "-no error-");
+      goto readd;
+    }
+
+  if (cmd->nonblock)
+    {
+      cmd->func(cmd);
+
+      free(cmd);
+      goto readd;
+    }
+
+  pthread_mutex_lock(&cmd->lck);
+
+  ret = cmd->func(cmd);
+  cmd->ret = ret;
+
+  pthread_cond_signal(&cmd->cond);
+  pthread_mutex_unlock(&cmd->lck);
+
+ readd:
+  event_add(g_cmdev, NULL);
+}
+
+
+/* ---------------------------- Our lastfm API  --------------------------- */
+
+static int
+lastfm_init(void);
+
+/* Thread: filescanner */
+void
+lastfm_login(char *path)
+{
+  struct lastfm_command *cmd;
+  struct keyval *kv;
+  char *username;
+  char *password;
+  int ret;
+
+  DPRINTF(E_DBG, L_LASTFM, "Got LastFM login request\n");
+
+  // Delete any existing session key
+  if (g_session_key)
+    free(g_session_key);
+
+  g_session_key = NULL;
+
+  db_admin_delete("lastfm_sk");
+
+  // Read the credentials file
+  ret = credentials_read(path, &username, &password);
+  if (ret < 0)
+    return;
+
+  // Enable LastFM now that we got a login attempt
+  g_disabled = 0;
+
+  kv = keyval_alloc();
+  if (!kv)
+    {
+      free(username);
+      free(password);
+      return;
+    }
+
+  ret = ( (keyval_add(kv, "api_key", g_api_key) == 0) &&
+          (keyval_add(kv, "username", username) == 0) &&
+          (keyval_add(kv, "password", password) == 0) );
+
+  free(username);
+  free(password);
+
+  if (!ret)
+    {
+      keyval_clear(kv);
+      return;
+    }
+
+  // Spawn thread
+  ret = lastfm_init();
+  if (ret < 0)
+    {
+      g_disabled = 1;
+      return;
+    }
+  g_initialized = 1;
+
+  // Send login command to the thread
+  cmd = (struct lastfm_command *)malloc(sizeof(struct lastfm_command));
+  if (!cmd)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not allocate lastfm_command\n");
+      return;
+    }
+
+  memset(cmd, 0, sizeof(struct lastfm_command));
+
+  cmd->nonblock = 1;
+  cmd->func = login;
+  cmd->arg.kv = kv;
+
+  nonblock_command(cmd);
+
+  return;
+}
+
+/* Thread: http and player */
+int
+lastfm_scrobble(int id)
+{
+  struct lastfm_command *cmd;
+  int ret;
+
+  DPRINTF(E_DBG, L_LASTFM, "Got LastFM scrobble request\n");
+
+  // LastFM is disabled because we already tried looking for a session key, but failed
+  if (g_disabled)
+    return -1;
+
+  // No session key in mem or in db
+  if (!g_session_key)
+    g_session_key = db_admin_get("lastfm_sk");
+
+  if (!g_session_key)
+    {
+      DPRINTF(E_INFO, L_LASTFM, "No valid LastFM session key\n");
+      g_disabled = 1;
+      return -1;
+    }
+
+  // Spawn LastFM thread
+  ret = lastfm_init();
+  if (ret < 0)
+    {
+      g_disabled = 1;
+      return -1;
+    }
+  g_initialized = 1;
+
+  // Send scrobble command to the thread
+  cmd = (struct lastfm_command *)malloc(sizeof(struct lastfm_command));
+  if (!cmd)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not allocate lastfm_command\n");
+      return -1;
+    }
+
+  memset(cmd, 0, sizeof(struct lastfm_command));
+
+  cmd->nonblock = 1;
+  cmd->func = scrobble;
+  cmd->arg.id = id;
+
+  nonblock_command(cmd);
+
+  return 0;
+}
+
+static int
+lastfm_init(void)
+{
+  int ret;
+
+  if (g_initialized)
+    return 0;
+
+  curl_global_init(CURL_GLOBAL_DEFAULT);
+
+# if defined(__linux__)
+  ret = pipe2(g_exit_pipe, O_CLOEXEC);
+# else
+  ret = pipe(g_exit_pipe);
+# endif
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not create pipe: %s\n", strerror(errno));
+      goto exit_fail;
+    }
+
+# if defined(__linux__)
+  ret = pipe2(g_cmd_pipe, O_CLOEXEC);
+# else
+  ret = pipe(g_cmd_pipe);
+# endif
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not create command pipe: %s\n", strerror(errno));
+      goto cmd_fail;
+    }
+
+  evbase_lastfm = event_base_new();
+  if (!evbase_lastfm)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not create an event base\n");
+      goto evbase_fail;
+    }
+
+#ifdef HAVE_LIBEVENT2
+  g_exitev = event_new(evbase_lastfm, g_exit_pipe[0], EV_READ, exit_cb, NULL);
+  if (!g_exitev)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not create exit event\n");
+      goto evnew_fail;
+    }
+
+  g_cmdev = event_new(evbase_lastfm, g_cmd_pipe[0], EV_READ, command_cb, NULL);
+  if (!g_cmdev)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not create cmd event\n");
+      goto evnew_fail;
+    }
+#else
+  g_exitev = (struct event *)malloc(sizeof(struct event));
+  if (!g_exitev)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not create exit event\n");
+      goto evnew_fail;
+    }
+  event_set(g_exitev, g_exit_pipe[0], EV_READ, exit_cb, NULL);
+  event_base_set(evbase_lastfm, g_exitev);
+
+  g_cmdev = (struct event *)malloc(sizeof(struct event));
+  if (!g_cmdev)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not create cmd event\n");
+      goto evnew_fail;
+    }
+  event_set(g_cmdev, g_cmd_pipe[0], EV_READ, command_cb, NULL);
+  event_base_set(evbase_lastfm, g_cmdev);
+#endif
+
+  event_add(g_exitev, NULL);
+  event_add(g_cmdev, NULL);
+
+  DPRINTF(E_INFO, L_LASTFM, "LastFM thread init\n");
+
+  ret = pthread_create(&tid_lastfm, NULL, lastfm, NULL);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_LASTFM, "Could not spawn LastFM thread: %s\n", strerror(errno));
+
+      goto thread_fail;
+    }
+
+  return 0;
+  
+ thread_fail:
+ evnew_fail:
+  event_base_free(evbase_lastfm);
+  evbase_lastfm = NULL;
+
+ evbase_fail:
+  close(g_cmd_pipe[0]);
+  close(g_cmd_pipe[1]);
+
+ cmd_fail:
+  close(g_exit_pipe[0]);
+  close(g_exit_pipe[1]);
+
+ exit_fail:
+  return -1;
+}
+
+void
+lastfm_deinit(void)
+{
+  int ret;
+
+  if (!g_initialized)
+    return;
+
+  curl_global_cleanup();
+
+  thread_exit();
+
+  ret = pthread_join(tid_lastfm, NULL);
+  if (ret != 0)
+    {
+      DPRINTF(E_FATAL, L_LASTFM, "Could not join lastfm thread: %s\n", strerror(errno));
+      return;
+    }
+
+  // Free event base (should free events too)
+  event_base_free(evbase_lastfm);
+
+  // Close pipes
+  close(g_cmd_pipe[0]);
+  close(g_cmd_pipe[1]);
+  close(g_exit_pipe[0]);
+  close(g_exit_pipe[1]);
+}
diff --git a/src/lastfm.h b/src/lastfm.h
new file mode 100644
index 0000000..2ba6383
--- /dev/null
+++ b/src/lastfm.h
@@ -0,0 +1,14 @@
+
+#ifndef __LASTFM_H__
+#define __LASTFM_H__
+
+void
+lastfm_login(char *path);
+
+int
+lastfm_scrobble(int id);
+
+void
+lastfm_deinit(void);
+
+#endif /* !__LASTFM_H__ */
diff --git a/src/laudio_alsa.c b/src/laudio_alsa.c
index e401898..c56d7b2 100644
--- a/src/laudio_alsa.c
+++ b/src/laudio_alsa.c
@@ -211,7 +211,7 @@ laudio_write(uint8_t *buf, uint64_t rtptime)
 
       return;
     }
-  else if ((pcm_status != LAUDIO_RUNNING) && (pcm_pos >= pcm_start_pos))
+  else if ((pcm_status != LAUDIO_RUNNING) && (pcm_pos + pcm_buf_threshold >= pcm_start_pos))
     {
       /* Kill threshold */
       ret = laudio_set_start_threshold(0);
@@ -362,7 +362,7 @@ laudio_start(uint64_t cur_pos, uint64_t next_pkt)
   pcm_start_pos = next_pkt + pcm_buf_threshold;
 
   /* Compensate threshold, as it's taken into account by snd_pcm_delay() */
-  pcm_pos += pcm_buf_threshold;
+  //pcm_pos += pcm_buf_threshold;
 
   DPRINTF(E_DBG, L_LAUDIO, "PCM pos %" PRIu64 ", start pos %" PRIu64 "\n", pcm_pos, pcm_start_pos);
 
diff --git a/src/logger.c b/src/logger.c
index 7feba73..00242ed 100644
--- a/src/logger.c
+++ b/src/logger.c
@@ -43,7 +43,7 @@ static int threshold;
 static int console;
 static char *logfilename;
 static FILE *logfile;
-static char *labels[] = { "config", "daap", "db", "httpd", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify" };
+static char *labels[] = { "config", "daap", "db", "httpd", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "dcache" };
 
 
 static int
diff --git a/src/logger.h b/src/logger.h
index 0498be2..180f57d 100644
--- a/src/logger.h
+++ b/src/logger.h
@@ -27,8 +27,10 @@
 #define L_DMAP    18
 #define L_DBPERF  19
 #define L_SPOTIFY 20
+#define L_LASTFM  21
+#define L_DCACHE  22
 
-#define N_LOGDOMAINS  21
+#define N_LOGDOMAINS  23
 
 /* Severities */
 #define E_FATAL   0
diff --git a/src/main.c b/src/main.c
index c7fcf01..d7e5c63 100644
--- a/src/main.c
+++ b/src/main.c
@@ -58,6 +58,7 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL;
 #include "db.h"
 #include "logger.h"
 #include "misc.h"
+#include "daap_cache.h"
 #include "filescanner.h"
 #include "httpd.h"
 #include "mdns.h"
@@ -67,6 +68,9 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL;
 # include "ffmpeg_url_evbuffer.h"
 #endif
 
+#ifdef LASTFM
+# include "lastfm.h"
+#endif
 #ifdef HAVE_SPOTIFY_H
 # include "spotify.h"
 #endif
@@ -671,6 +675,16 @@ main(int argc, char **argv)
       goto db_fail;
     }
 
+  /* Spawn DAAP cache thread */
+  ret = daapcache_init();
+  if (ret != 0)
+    {
+      DPRINTF(E_FATAL, L_MAIN, "DAAP cache thread failed to start\n");
+
+      ret = EXIT_FAILURE;
+      goto cache_fail;
+    }
+
   /* Spawn file scanner thread */
   ret = filescanner_init();
   if (ret != 0)
@@ -798,6 +812,10 @@ main(int argc, char **argv)
   player_deinit();
 
  player_fail:
+#ifdef LASTFM
+  DPRINTF(E_LOG, L_MAIN, "LastFM deinit\n");
+  lastfm_deinit();
+#endif
 #ifdef HAVE_SPOTIFY_H
   DPRINTF(E_LOG, L_MAIN, "Spotify deinit\n");
   spotify_deinit();
@@ -806,9 +824,14 @@ main(int argc, char **argv)
   filescanner_deinit();
 
  filescanner_fail:
+  DPRINTF(E_LOG, L_MAIN, "DAAP cache deinit\n");
+  daapcache_deinit();
+
+ cache_fail:
   DPRINTF(E_LOG, L_MAIN, "Database deinit\n");
   db_perthread_deinit();
   db_deinit();
+
  db_fail:
   if (ret == EXIT_FAILURE)
     {
diff --git a/src/misc.c b/src/misc.c
index 6251d2d..24762e9 100644
--- a/src/misc.c
+++ b/src/misc.c
@@ -28,6 +28,7 @@
 
 #include <stdlib.h>
 #include <string.h>
+#include <ctype.h>
 #include <errno.h>
 #include <stdint.h>
 #include <limits.h>
@@ -258,12 +259,33 @@ safe_hextou64(const char *str, uint64_t *val)
 
 
 /* Key/value functions */
+struct keyval *
+keyval_alloc(void)
+{
+  struct keyval *kv;
+
+  kv = (struct keyval *)malloc(sizeof(struct keyval));
+  if (!kv)
+    {
+      DPRINTF(E_LOG, L_MISC, "Out of memory for keyval alloc\n");
+
+      return NULL;
+    }
+
+  memset(kv, 0, sizeof(struct keyval));
+
+  return kv;
+}
+
 int
 keyval_add_size(struct keyval *kv, const char *name, const char *value, size_t size)
 {
   struct onekeyval *okv;
   const char *val;
 
+  if (!kv)
+    return -1;
+
   /* Check for duplicate key names */
   val = keyval_get(kv, name);
   if (val)
@@ -330,6 +352,9 @@ keyval_remove(struct keyval *kv, const char *name)
   struct onekeyval *okv;
   struct onekeyval *pokv;
 
+  if (!kv)
+    return;
+
   for (pokv = NULL, okv = kv->head; okv; pokv = okv, okv = okv->next)
     {
       if (strcasecmp(okv->name, name) == 0)
@@ -358,6 +383,9 @@ keyval_get(struct keyval *kv, const char *name)
 {
   struct onekeyval *okv;
 
+  if (!kv)
+    return NULL;
+
   for (okv = kv->head; okv; okv = okv->next)
     {
       if (strcasecmp(okv->name, name) == 0)
@@ -373,6 +401,9 @@ keyval_clear(struct keyval *kv)
   struct onekeyval *hokv;
   struct onekeyval *okv;
 
+  if (!kv)
+    return;
+
   hokv = kv->head;
 
   for (okv = hokv; hokv; okv = hokv)
@@ -388,6 +419,47 @@ keyval_clear(struct keyval *kv)
   kv->tail = NULL;
 }
 
+void
+keyval_sort(struct keyval *kv)
+{
+  struct onekeyval *head;
+  struct onekeyval *okv;
+  struct onekeyval *sokv;
+
+  if (!kv)
+    return;
+
+  head = kv->head;
+  for (okv = kv->head; okv; okv = okv->next)
+    {
+      okv->sort = NULL;
+      for (sokv = kv->head; sokv; sokv = sokv->next)
+	{
+	  // We try to find a name which is greater than okv->name
+	  // but less than our current candidate (okv->sort->name)
+	  if ( (strcmp(sokv->name, okv->name) > 0) &&
+	       ((okv->sort == NULL) || (strcmp(sokv->name, okv->sort->name) < 0)) )
+	    okv->sort = sokv;
+	}
+
+      // Find smallest name, which will be the new head
+      if (strcmp(okv->name, head->name) < 0)
+	head = okv;
+    }
+
+  while ((okv = kv->head))
+    {
+      kv->head  = okv->next;
+      okv->next = okv->sort;
+    }
+
+  kv->head = head;
+  for (okv = kv->head; okv; okv = okv->next)
+    kv->tail = okv;
+
+  DPRINTF(E_DBG, L_MISC, "Keyval sorted. New head: %s. New tail: %s.\n", kv->head->name, kv->tail->name);
+}
+
 
 char *
 m_realpath(const char *pathname)
@@ -445,6 +517,43 @@ unicode_fixup_string(char *str)
   return (char *)ret;
 }
 
+char *
+trimwhitespace(const char *str)
+{
+  char *ptr;
+  char *start;
+  char *out;
+
+  if (!str)
+    return NULL;
+
+  // Find the beginning
+  while (isspace(*str))
+    str++;
+
+  if (*str == 0) // All spaces?
+    return strdup("");
+
+  // Make copy, because we will need to insert a null terminator
+  start = strdup(str);
+  if (!start)
+    return NULL;
+
+  // Find the end
+  ptr = start + strlen(start) - 1;
+  while (ptr > start && isspace(*ptr))
+    ptr--;
+
+  // Insert null terminator
+  *(ptr+1) = 0;
+
+  out = strdup(start);
+
+  free(start);
+
+  return out;
+}
+
 uint32_t
 djb_hash(void *data, size_t len)
 {
@@ -754,21 +863,20 @@ murmur_hash64(const void *key, int len, uint32_t seed)
 # error Platform not supported
 #endif
 
-#if defined(__linux__)
 int
 clock_gettime_with_res(clockid_t clock_id, struct timespec *tp, struct timespec *res)
 {
-  int r_val = -1;
-  if(res && tp)
-  {
-   r_val = clock_gettime(clock_id, tp);
-   /* this will only work for sub-second resolutions. */
-   if(r_val == 0 && res->tv_nsec > 1)
-    {
-      tp->tv_nsec = (tp->tv_nsec/res->tv_nsec)*res->tv_nsec;    
-    }
-  }
-  return r_val;
+  int ret;
+
+  if ((!tp) || (!res))
+    return -1;
+
+  ret = clock_gettime(clock_id, tp);
+  /* this will only work for sub-second resolutions. */
+  if (ret == 0 && res->tv_nsec > 1)
+    tp->tv_nsec = (tp->tv_nsec/res->tv_nsec)*res->tv_nsec;
+
+  return ret;
 }
 
 struct timespec
@@ -779,10 +887,10 @@ timespec_add(struct timespec time1, struct timespec time2)
   result.tv_sec = time1.tv_sec + time2.tv_sec;
   result.tv_nsec = time1.tv_nsec + time2.tv_nsec;
   if (result.tv_nsec >= 1000000000L)
-  {
-    result.tv_sec++;
-    result.tv_nsec -= 1000000000L;
-  }
+    {
+      result.tv_sec++;
+      result.tv_nsec -= 1000000000L;
+    }
   return result;
 }
 
@@ -805,4 +913,3 @@ timespec_cmp(struct timespec time1, struct timespec time2)
   else
     return 0;
 }
-#endif /* __linux__ */
diff --git a/src/misc.h b/src/misc.h
index 1207df5..e3026b4 100644
--- a/src/misc.h
+++ b/src/misc.h
@@ -3,15 +3,14 @@
 #define __MISC_H__
 
 #include <stdint.h>
-#if defined(__linux__)
-# include <time.h>
-#endif
+#include <time.h>
 
 struct onekeyval {
   char *name;
   char *value;
 
   struct onekeyval *next;
+  struct onekeyval *sort;
 };
 
 struct keyval {
@@ -40,6 +39,9 @@ safe_hextou64(const char *str, uint64_t *val);
 
 
 /* Key/value functions */
+struct keyval *
+keyval_alloc(void);
+
 int
 keyval_add(struct keyval *kv, const char *name, const char *value);
 
@@ -55,6 +57,9 @@ keyval_get(struct keyval *kv, const char *name);
 void
 keyval_clear(struct keyval *kv);
 
+void
+keyval_sort(struct keyval *kv);
+
 
 char *
 m_realpath(const char *pathname);
@@ -62,6 +67,9 @@ m_realpath(const char *pathname);
 char *
 unicode_fixup_string(char *str);
 
+char *
+trimwhitespace(const char *str);
+
 uint32_t
 djb_hash(void *data, size_t len);
 
@@ -74,8 +82,7 @@ b64_encode(uint8_t *in, size_t len);
 uint64_t
 murmur_hash64(const void *key, int len, uint32_t seed);
 
-#if defined(__linux__)
-/* Timer functions for platforms without hi-res timers */
+/* Timer function for platforms without hi-res timers */
 int
 clock_gettime_with_res(clockid_t clock_id, struct timespec *tp, struct timespec *res);
 
@@ -84,6 +91,5 @@ timespec_add(struct timespec time1, struct timespec time2);
 
 int
 timespec_cmp(struct timespec time1, struct timespec time2);
-#endif /* __linux__ */
 
 #endif /* !__MISC_H__ */
diff --git a/src/pipe.c b/src/pipe.c
index 25e39dd..c8c9660 100644
--- a/src/pipe.c
+++ b/src/pipe.c
@@ -108,14 +108,15 @@ pipe_audio_get(struct evbuffer *evbuf, int wanted)
 
   got = read(g_fd, g_buf, wanted);
 
-  if (got < 0)
+  if ((got < 0) && (errno != EAGAIN))
     {
       DPRINTF(E_LOG, L_PLAYER, "Could not read from pipe: %s\n", strerror(errno));
       return -1;
     }
 
-  // If the other end of the pipe is not writing we just return silence
-  if (got == 0)
+  // If the other end of the pipe is not writing or the read was blocked,
+  // we just return silence
+  if (got <= 0)
     {
       memset(g_buf, 0, wanted);
       got = wanted;
diff --git a/src/pipe.h b/src/pipe.h
index ba57a81..116ead9 100644
--- a/src/pipe.h
+++ b/src/pipe.h
@@ -3,7 +3,11 @@
 #define __PIPE_H__
 
 #include "db.h"
-#include <event.h>
+#ifdef HAVE_LIBEVENT2
+# include <event2/buffer.h>
+#else
+# include <event.h>
+#endif
 
 int
 pipe_setup(struct media_file_info *mfi);
diff --git a/src/player.c b/src/player.c
index e2e3f7b..a181ca5 100644
--- a/src/player.c
+++ b/src/player.c
@@ -34,17 +34,17 @@
 #if defined(__linux__)
 # include <sys/timerfd.h>
 #elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
-# include <sys/time.h>
-# include <sys/event.h>
+# include <signal.h>
 #endif
 
-#if defined(HAVE_SYS_EVENTFD_H) && defined(HAVE_EVENTFD)
-# define USE_EVENTFD
-# include <sys/eventfd.h>
+#ifdef HAVE_LIBEVENT2
+# include <event2/event.h>
+# include <event2/buffer.h>
+#else
+# include <event.h>
+# define evbuffer_get_length(x) (x)->off
 #endif
 
-#include <event.h>
-
 #include <gcrypt.h>
 
 #include "db.h"
@@ -58,6 +58,10 @@
 #include "raop.h"
 #include "laudio.h"
 
+#ifdef LASTFM
+# include "lastfm.h"
+#endif
+
 /* These handle getting the media data */
 #include "transcode.h"
 #include "pipe.h"
@@ -137,15 +141,11 @@ static const char *raop_devtype[] =
 
 struct event_base *evbase_player;
 
-#ifdef USE_EVENTFD
-static int exit_efd;
-#else
 static int exit_pipe[2];
-#endif
 static int cmd_pipe[2];
 static int player_exit;
-static struct event exitev;
-static struct event cmdev;
+static struct event *exitev;
+static struct event *cmdev;
 static pthread_t tid_player;
 
 /* Player status */
@@ -157,15 +157,17 @@ static char shuffle;
 static player_status_handler update_handler;
 
 /* Playback timer */
-static int pb_timer_fd;
-static struct event pb_timer_ev;
 #if defined(__linux__)
+static int pb_timer_fd;
+#else
+timer_t pb_timer;
+#endif
+static struct event *pb_timer_ev;
 static struct timespec pb_timer_last;
 static struct timespec packet_timer_last;
 static uint64_t MINIMUM_STREAM_PERIOD;
 static struct timespec packet_time = { 0, AIRTUNES_V2_STREAM_PERIOD };
 static struct timespec timer_res;
-#endif
 
 /* Sync source */
 static enum player_sync_source pb_sync_source;
@@ -218,7 +220,7 @@ command_async_end(struct player_command *cmd)
   pthread_mutex_unlock(&cmd->lck);
 
   /* Process commands again */
-  event_add(&cmdev, NULL);
+  event_add(cmdev, NULL);
 }
 
 static void
@@ -375,11 +377,7 @@ player_get_current_pos_clock(uint64_t *pos, struct timespec *ts, int commit)
   uint64_t delta;
   int ret;
 
-#if defined(__linux__)
   ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &timer_res);
-#else
-  ret = clock_gettime(CLOCK_MONOTONIC, ts);
-#endif
   if (ret < 0)
     {
       DPRINTF(E_LOG, L_PLAYER, "Couldn't get clock: %s\n", strerror(errno));
@@ -423,11 +421,7 @@ player_get_current_pos_laudio(uint64_t *pos, struct timespec *ts, int commit)
 
   *pos = laudio_get_pos();
 
-#if defined(__linux__)
   ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &timer_res);
-#else
-  ret = clock_gettime(CLOCK_MONOTONIC, ts);
-#endif
   if (ret < 0)
     {
       DPRINTF(E_LOG, L_PLAYER, "Couldn't get clock: %s\n", strerror(errno));
@@ -465,10 +459,78 @@ player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit)
   return -1;
 }
 
+static int
+pb_timer_start(struct timespec *ts)
+{
+  struct itimerspec next;
+  int ret;
+
+  next.it_interval.tv_sec = 0;
+  next.it_interval.tv_nsec = 0;
+  next.it_value.tv_sec = ts->tv_sec;
+  next.it_value.tv_nsec = ts->tv_nsec;
+
+#if defined(__linux__)
+  ret = timerfd_settime(pb_timer_fd, TFD_TIMER_ABSTIME, &next, NULL);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not arm playback timer: %s\n", strerror(errno));
+
+      return -1;
+    }
+
+  ret = event_add(pb_timer_ev, NULL);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not add playback timer event\n");
+
+      return -1;
+    }
+#else
+  ret = timer_settime(pb_timer, TIMER_ABSTIME, &next, NULL);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not arm playback timer: %s\n", strerror(errno));
+
+      return -1;
+    }
+#endif
+
+  return 0;
+}
+
+static int
+pb_timer_stop(void)
+{
+  struct itimerspec next;
+  int ret;
+
+  memset(&next, 0, sizeof(struct itimerspec));
+
+#if defined(__linux__)
+  ret = timerfd_settime(pb_timer_fd, TFD_TIMER_ABSTIME, &next, NULL);
+
+  event_del(pb_timer_ev);
+#else
+  ret = timer_settime(pb_timer, TIMER_ABSTIME, &next, NULL);
+#endif
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not disarm playback timer: %s\n", strerror(errno));
+
+      return -1;
+    }
+
+  return 0;
+}
+
 /* Forward */
 static void
 playback_abort(void);
 
+static int
+queue_clear(struct player_command *cmd);
+
 static void
 player_laudio_status_cb(enum laudio_state status)
 {
@@ -655,6 +717,8 @@ player_queue_make(struct query_params *qp, const char *sort)
 
   q_head->pl_prev = q_tail;
   q_tail->pl_next = q_head;
+  q_head->shuffle_prev = q_tail;
+  q_tail->shuffle_next = q_head;
 
   return q_head;
 }
@@ -821,9 +885,12 @@ player_queue_make_daap(struct player_source **head, const char *query, const cha
 	}
       else
 	{
-	  DPRINTF(E_LOG, L_PLAYER, "Unknown queuefilter: %s\n", queuefilter);
+	  DPRINTF(E_LOG, L_PLAYER, "Unknown queuefilter %s\n", queuefilter);
 
-	  return -1;
+	  // If the queuefilter is unkown, ignore it and use the query parameter instead to build the sql query
+	  id = 0;
+	  qp.type = Q_ITEMS;
+	  qp.filter = daap_query_parse_sql(query);
 	}
     }
   else
@@ -971,8 +1038,11 @@ source_stop(struct player_source *ps)
     }
 }
 
-static struct player_source *
-source_shuffle(struct player_source *head)
+/*
+ * Shuffles the items between head and tail (excluding head and tail)
+ */
+static void
+source_shuffle(struct player_source *head, struct player_source *tail)
 {
   struct player_source *ps;
   struct player_source **ps_array;
@@ -980,34 +1050,55 @@ source_shuffle(struct player_source *head)
   int i;
 
   if (!head)
-    return NULL;
+    return;
 
-  ps = head;
+  if (!tail)
+    return;
+
+  if (!shuffle)
+    {
+      ps = head;
+      do
+	{
+	  ps->shuffle_next = ps->pl_next;
+	  ps->shuffle_prev = ps->pl_prev;
+	  ps = ps->pl_next;
+	}
+      while (ps != head);
+    }
+
+  // Count items in queue (excluding head and tail)
+  ps = head->shuffle_next;
   nitems = 0;
-  do
+  while (ps != tail)
     {
       nitems++;
-      ps = ps->pl_next;
+      ps = ps->shuffle_next;
     }
-  while (ps != head);
 
+  // Do not reshuffle queue with one item
+  if (nitems < 1)
+    return;
+
+  // Construct array for number of items in queue
   ps_array = (struct player_source **)malloc(nitems * sizeof(struct player_source *));
   if (!ps_array)
     {
       DPRINTF(E_LOG, L_PLAYER, "Could not allocate memory for shuffle array\n");
-      return NULL;
+      return;
     }
 
-  ps = head;
+  // Fill array with items in queue (excluding head and tail)
+  ps = head->shuffle_next;
   i = 0;
   do
     {
       ps_array[i] = ps;
 
-      ps = ps->pl_next;
+      ps = ps->shuffle_next;
       i++;
     }
-  while (ps != head);
+  while (ps != tail);
 
   shuffle_ptr(&shuffle_rng, (void **)ps_array, nitems);
 
@@ -1022,29 +1113,39 @@ source_shuffle(struct player_source *head)
 	ps->shuffle_next = ps_array[i + 1];
     }
 
-  ps_array[0]->shuffle_prev = ps_array[nitems - 1];
-  ps_array[nitems - 1]->shuffle_next = ps_array[0];
-
-  ps = ps_array[0];
+  // Insert shuffled items between head and tail
+  ps_array[0]->shuffle_prev = head;
+  ps_array[nitems - 1]->shuffle_next = tail;
+  head->shuffle_next = ps_array[0];
+  tail->shuffle_prev = ps_array[nitems - 1];
 
   free(ps_array);
 
-  return ps;
+  return;
 }
 
 static void
 source_reshuffle(void)
 {
-  struct player_source *ps;
-
-  ps = source_shuffle(source_head);
-  if (!ps)
-    return;
+  struct player_source *head;
+  struct player_source *tail;
 
   if (cur_streaming)
-    shuffle_head = cur_streaming;
+    head = cur_streaming;
   else
-    shuffle_head = ps;
+    head = source_head;
+
+  if (repeat == REPEAT_ALL)
+    tail = head;
+  else if (shuffle)
+    tail = shuffle_head;
+  else
+    tail = source_head;
+
+  source_shuffle(head, tail);
+
+  if (repeat == REPEAT_ALL)
+    shuffle_head = head;
 }
 
 /* Helper */
@@ -1395,6 +1496,9 @@ source_check(void)
       i++;
 
       db_file_inc_playcount((int)cur_playing->id);
+#ifdef LASTFM
+      lastfm_scrobble((int)cur_playing->id);
+#endif
 
       /* Stop playback if:
        * - at end of playlist (NULL)
@@ -1499,7 +1603,7 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
 	    return -1;
 	}
 
-      if (EVBUFFER_LENGTH(audio_buf) == 0)
+      if (evbuffer_get_length(audio_buf) == 0)
 	{
 	  switch (cur_streaming->type)
 	    {
@@ -1541,7 +1645,7 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
 static void
 playback_write(void)
 {
-  uint8_t rawbuf[AIRTUNES_V2_PACKET_SAMPLES * 2 * 2];
+  uint8_t rawbuf[STOB(AIRTUNES_V2_PACKET_SAMPLES)];
   int ret;
 
   source_check();
@@ -1569,19 +1673,19 @@ playback_write(void)
     raop_v2_write(rawbuf, last_rtptime);
 }
 
-#if defined(__linux__)
 static void
 player_playback_cb(int fd, short what, void *arg)
 {
-  struct itimerspec next;
-  uint64_t ticks;
-  int ret;
   uint32_t packet_send_count = 0;
   struct timespec next_tick;
   struct timespec stream_period  = { 0, MINIMUM_STREAM_PERIOD };
+  int ret;
+#if defined(__linux__)
+  uint64_t ticks;
 
   /* Acknowledge timer */
   read(fd, &ticks, sizeof(ticks));
+#endif /* __linux__ */
 
   /* Decide how many packets to send */
   next_tick = timespec_add(pb_timer_last, stream_period);
@@ -1599,80 +1703,18 @@ player_playback_cb(int fd, short what, void *arg)
 	  return;
 	}
     }
-
-  while(timespec_cmp(packet_timer_last, next_tick) < 0);
+  while (timespec_cmp(packet_timer_last, next_tick) < 0);
 
   /* Make sure playback is still running */
   if (player_state == PLAY_STOPPED)
     return;
 
-  pb_timer_last.tv_nsec += MINIMUM_STREAM_PERIOD;
-  if (pb_timer_last.tv_nsec >= 1000000000)
-    {
-      pb_timer_last.tv_sec++;
-      pb_timer_last.tv_nsec -= 1000000000;
-    }
+  pb_timer_last = timespec_add(pb_timer_last, stream_period);
 
-  next.it_interval.tv_sec = 0;
-  next.it_interval.tv_nsec = 0;
-  next.it_value.tv_sec = pb_timer_last.tv_sec;
-  next.it_value.tv_nsec = pb_timer_last.tv_nsec;
-
-  ret = timerfd_settime(pb_timer_fd, TFD_TIMER_ABSTIME, &next, NULL);
-  if (ret < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not set playback timer: %s\n", strerror(errno));
-
-      playback_abort();
-      return;
-    }
-
-  ret = event_add(&pb_timer_ev, NULL);
+  ret = pb_timer_start(&pb_timer_last);
   if (ret < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not re-add playback timer event\n");
-
-      playback_abort();
-      return;
-    }
-}
-#endif /* __linux__ */
-
-
-#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
-static void
-player_playback_cb(int fd, short what, void *arg)
-{
-  struct timespec ts;
-  struct kevent kev;
-  int ret;
-
-  ts.tv_sec = 0;
-  ts.tv_nsec = 0;
-
-  while (kevent(pb_timer_fd, NULL, 0, &kev, 1, &ts) > 0)
-    {
-      if (kev.filter != EVFILT_TIMER)
-        continue;
-
-      playback_write();
-
-      /* Make sure playback is still running */
-      if (player_state == PLAY_STOPPED)
-	return;
-    }
-
-  ret = event_add(&pb_timer_ev, NULL);
-  if (ret < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not re-add playback timer event\n");
-
-      playback_abort();
-      return;
-    }
+    playback_abort();
 }
-#endif /* __FreeBSD__ || __FreeBSD_kernel__ */
-
 
 static void
 device_free(struct raop_device *dev)
@@ -2030,24 +2072,14 @@ device_activate_cb(struct raop_device *dev, struct raop_session *rs, enum raop_s
 
   if ((player_state == PLAY_PLAYING) && (raop_sessions == 1))
     {
-#if defined(__linux__)
       ret = clock_gettime_with_res(CLOCK_MONOTONIC, &ts, &timer_res);
-#else
-      ret = clock_gettime(CLOCK_MONOTONIC, &ts);
-#endif
       if (ret < 0)
 	{
 	  DPRINTF(E_LOG, L_PLAYER, "Could not get current time: %s\n", strerror(errno));
 
-#if defined(__linux__)
 	  /* Fallback to nearest timer expiration time */
 	  ts.tv_sec = pb_timer_last.tv_sec;
 	  ts.tv_nsec = pb_timer_last.tv_nsec;
-#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
-	  if (cur_cmd->ret != -2)
-	    cur_cmd->ret = -1;
-	  goto out;
-#endif
 	}
 
       raop_playback_start(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, &ts);
@@ -2167,22 +2199,19 @@ playback_abort(void)
   if (raop_sessions > 0)
     raop_playback_stop();
 
-  if (event_initialized(&pb_timer_ev))
-    event_del(&pb_timer_ev);
-
-  if (pb_timer_fd != -1)
-    close(pb_timer_fd);
-  pb_timer_fd = -1;
+  pb_timer_stop();
 
   if (cur_playing)
     source_stop(cur_playing);
   else
     source_stop(cur_streaming);
 
+  queue_clear(NULL);
+
   cur_playing = NULL;
   cur_streaming = NULL;
 
-  evbuffer_drain(audio_buf, EVBUFFER_LENGTH(audio_buf));
+  evbuffer_drain(audio_buf, evbuffer_get_length(audio_buf));
 
   status_update(PLAY_STOPPED);
 
@@ -2300,12 +2329,7 @@ playback_stop(struct player_command *cmd)
    */
   cmd->raop_pending = raop_flush(device_command_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES);
 
-  if (event_initialized(&pb_timer_ev))
-    event_del(&pb_timer_ev);
-
-  if (pb_timer_fd != -1)
-    close(pb_timer_fd);
-  pb_timer_fd = -1;
+  pb_timer_stop();
 
   if (cur_playing)
     {
@@ -2321,7 +2345,7 @@ playback_stop(struct player_command *cmd)
   cur_playing = NULL;
   cur_streaming = NULL;
 
-  evbuffer_drain(audio_buf, EVBUFFER_LENGTH(audio_buf));
+  evbuffer_drain(audio_buf, evbuffer_get_length(audio_buf));
 
   status_update(PLAY_STOPPED);
 
@@ -2338,11 +2362,6 @@ playback_stop(struct player_command *cmd)
 static int
 playback_start_bh(struct player_command *cmd)
 {
-#if defined(__linux__)
-  struct itimerspec next;
-#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
-  struct kevent kev;
-#endif
   int ret;
 
   if ((laudio_status == LAUDIO_CLOSED) && (raop_sessions == 0))
@@ -2366,11 +2385,7 @@ playback_start_bh(struct player_command *cmd)
 	}
     }
 
-#if defined(__linux__)
   ret = clock_gettime_with_res(CLOCK_MONOTONIC, &pb_pos_stamp, &timer_res);
-#else
-  ret = clock_gettime(CLOCK_MONOTONIC, &pb_pos_stamp);
-#endif
   if (ret < 0)
     {
       DPRINTF(E_LOG, L_PLAYER, "Couldn't get current clock: %s\n", strerror(errno));
@@ -2378,9 +2393,8 @@ playback_start_bh(struct player_command *cmd)
       goto out_fail;
     }
 
-  memset(&pb_timer_ev, 0, sizeof(struct event));
+  pb_timer_stop();
 
-#if defined(__linux__)
   /*
    * initialize the packet timer to the same relative time that we have 
    * for the playback timer.
@@ -2391,58 +2405,9 @@ playback_start_bh(struct player_command *cmd)
   pb_timer_last.tv_sec = pb_pos_stamp.tv_sec;
   pb_timer_last.tv_nsec = pb_pos_stamp.tv_nsec;
 
-  pb_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
-  if (pb_timer_fd < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer: %s\n", strerror(errno));
-
-      goto out_fail;
-    }
-
-  next.it_interval.tv_sec = 0;
-  next.it_interval.tv_nsec = 0;
-  next.it_value.tv_sec = pb_timer_last.tv_sec;
-  next.it_value.tv_nsec = pb_timer_last.tv_nsec;
-
-  ret = timerfd_settime(pb_timer_fd, TFD_TIMER_ABSTIME, &next, NULL);
+  ret = pb_timer_start(&pb_timer_last);
   if (ret < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not set playback timer: %s\n", strerror(errno));
-
-      goto out_fail;
-    }
-#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
-  pb_timer_fd = kqueue();
-  if (pb_timer_fd < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not create kqueue: %s\n", strerror(errno));
-
-      goto out_fail;
-    }
-
-  memset(&kev, 0, sizeof(struct kevent));
-
-  EV_SET(&kev, 1, EVFILT_TIMER, EV_ADD | EV_ENABLE, 0, AIRTUNES_V2_STREAM_PERIOD, 0);
-
-  ret = kevent(pb_timer_fd, &kev, 1, NULL, 0, NULL);
-  if (ret < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not add kevent timer: %s\n", strerror(errno));
-
-      goto out_fail;
-    }
-#endif
-
-  event_set(&pb_timer_ev, pb_timer_fd, EV_READ, player_playback_cb, NULL);
-  event_base_set(evbase_player, &pb_timer_ev);
-
-  ret = event_add(&pb_timer_ev, NULL);
-  if (ret < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not set up playback timer event\n");
-
-      goto out_fail;
-    }
+    goto out_fail;
 
   /* Everything OK, start RAOP */
   if (raop_sessions > 0)
@@ -2453,9 +2418,6 @@ playback_start_bh(struct player_command *cmd)
   return 0;
 
  out_fail:
-  if (pb_timer_fd != -1)
-    close(pb_timer_fd);
-  pb_timer_fd = -1;
   playback_abort();
 
   return -1;
@@ -2633,6 +2595,7 @@ static int
 playback_prev_bh(struct player_command *cmd)
 {
   int ret;
+  int pos_sec;
 
   if (!cur_streaming)
     {
@@ -2646,12 +2609,34 @@ playback_prev_bh(struct player_command *cmd)
 
   source_stop(cur_streaming);
 
-  ret = source_prev();
-  if (ret < 0)
+  /* Compute the playing time in seconds for the current song. */
+  if (cur_streaming->end > cur_streaming->stream_start)
+    pos_sec = (cur_streaming->end - cur_streaming->stream_start) / 44100;
+  else
+    pos_sec = 0;
+
+  /* Only skip to the previous song if the playing time is less than 3 seconds,
+   otherwise restart the current song. */
+  DPRINTF(E_DBG, L_PLAYER, "Skipping song played %d sec\n", pos_sec);
+  if (pos_sec < 3)
     {
-      playback_abort();
+      ret = source_prev();
+      if (ret < 0)
+	{
+	  playback_abort();
 
-      return -1;
+	  return -1;
+	}
+    }
+  else
+    {
+      ret = source_open(cur_streaming, 1);
+      if (ret < 0)
+	{
+	  playback_abort();
+
+	  return -1;
+	}
     }
 
   if (player_state == PLAY_STOPPED)
@@ -2668,6 +2653,7 @@ playback_prev_bh(struct player_command *cmd)
   return 0;
 }
 
+
 static int
 playback_next_bh(struct player_command *cmd)
 {
@@ -2846,12 +2832,7 @@ playback_pause(struct player_command *cmd)
   if (laudio_status != LAUDIO_CLOSED)
     laudio_stop();
 
-  if (event_initialized(&pb_timer_ev))
-    event_del(&pb_timer_ev);
-
-  if (pb_timer_fd != -1)
-    close(pb_timer_fd);
-  pb_timer_fd = -1;
+  pb_timer_stop();
 
   if (ps->play_next)
     source_stop(ps->play_next);
@@ -2860,7 +2841,7 @@ playback_pause(struct player_command *cmd)
   cur_streaming = ps;
   cur_streaming->play_next = NULL;
 
-  evbuffer_drain(audio_buf, EVBUFFER_LENGTH(audio_buf));
+  evbuffer_drain(audio_buf, evbuffer_get_length(audio_buf));
 
   metadata_purge();
 
@@ -3378,10 +3359,7 @@ queue_add(struct player_command *cmd)
   struct player_source *ps_tail;
 
   ps = cmd->arg.ps;
-
-  ps_shuffle = source_shuffle(ps);
-  if (!ps_shuffle)
-    ps_shuffle = ps;
+  ps_shuffle = ps;
 
   if (source_head)
     {
@@ -3411,6 +3389,9 @@ queue_add(struct player_command *cmd)
       shuffle_head = ps_shuffle;
     }
 
+  if (shuffle)
+    source_reshuffle();
+
   if (cur_plid != 0)
     cur_plid = 0;
 
@@ -3425,10 +3406,7 @@ queue_add_next(struct player_command *cmd)
   struct player_source *ps_playing;
 
   ps = cmd->arg.ps;
-
-  ps_shuffle = source_shuffle(ps);
-  if (!ps_shuffle)
-    ps_shuffle = ps;
+  ps_shuffle = ps;
 
   if (source_head && cur_streaming)
   {
@@ -3452,6 +3430,9 @@ queue_add_next(struct player_command *cmd)
     shuffle_head = ps_shuffle;
   }
 
+  if (shuffle)
+    source_reshuffle();
+
   if (cur_plid != 0)
     cur_plid = 0;
 
@@ -3708,7 +3689,7 @@ command_cb(int fd, short what, void *arg)
     }
 
  readd:
-  event_add(&cmdev, NULL);
+  event_add(cmdev, NULL);
 }
 
 
@@ -4545,8 +4526,6 @@ player_init(void)
 
   cur_cmd = NULL;
 
-  pb_timer_fd = -1;
-
   source_head = NULL;
   shuffle_head = NULL;
   cur_playing = NULL;
@@ -4559,9 +4538,8 @@ player_init(void)
 
   update_handler = NULL;
 
-  history = (struct player_history *) calloc(1, sizeof(struct player_history));
+  history = (struct player_history *)calloc(1, sizeof(struct player_history));
 
-#if defined(__linux__)
   /*
    * Determine if the resolution of the system timer is > or < the size
    * of an audio packet. NOTE: this assumes the system clock resolution
@@ -4573,8 +4551,30 @@ player_init(void)
 
       return -1;
     }
+
+#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
+  /* FreeBSD will report a resolution of 1, but actually has a resolution
+   * larger than an audio packet
+   */
+  if (timer_res.tv_nsec == 1)
+    timer_res.tv_nsec = 2 * AIRTUNES_V2_STREAM_PERIOD;
+#endif
+
   MINIMUM_STREAM_PERIOD = MAX(timer_res.tv_nsec, AIRTUNES_V2_STREAM_PERIOD);
+
+  /* Create a timer */
+#if defined(__linux__)
+  pb_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
+  ret = pb_timer_fd;
+#else
+  ret = timer_create(CLOCK_MONOTONIC, NULL, &pb_timer);
 #endif
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer: %s\n", strerror(errno));
+
+      return -1;
+    }
 
   /* Random RTP time start */
   gcry_randomize(&rnd, sizeof(rnd), GCRY_STRONG_RANDOM);
@@ -4593,33 +4593,22 @@ player_init(void)
     {
       DPRINTF(E_LOG, L_PLAYER, "Could not allocate evbuffer for audio buffer\n");
 
-      return -1;
+      goto audio_fail;
     }
 
   raop_v6enabled = cfg_getbool(cfg_getsec(cfg, "general"), "ipv6");
 
-
-#ifdef USE_EVENTFD
-  exit_efd = eventfd(0, EFD_CLOEXEC);
-  if (exit_efd < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not create eventfd: %s\n", strerror(errno));
-
-      goto exit_fail;
-    }
-#else
-# if defined(__linux__)
+#if defined(__linux__)
   ret = pipe2(exit_pipe, O_CLOEXEC);
-# else
+#else
   ret = pipe(exit_pipe);
-# endif
+#endif
   if (ret < 0)
     {
       DPRINTF(E_LOG, L_PLAYER, "Could not create pipe: %s\n", strerror(errno));
 
       goto exit_fail;
     }
-#endif /* USE_EVENTFD */
 
 # if defined(__linux__)
   ret = pipe2(cmd_pipe, O_CLOEXEC);
@@ -4641,17 +4630,70 @@ player_init(void)
       goto evbase_fail;
     }
 
-#ifdef USE_EVENTFD
-  event_set(&exitev, exit_efd, EV_READ, exit_cb, NULL);
+#ifdef HAVE_LIBEVENT2
+  exitev = event_new(evbase_player, exit_pipe[0], EV_READ, exit_cb, NULL);
+  if (!exitev)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not create exit event\n");
+      goto evnew_fail;
+    }
+
+  cmdev = event_new(evbase_player, cmd_pipe[0], EV_READ, command_cb, NULL);
+  if (!cmdev)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not create cmd event\n");
+      goto evnew_fail;
+    }
+
+# if defined(__linux__)
+  pb_timer_ev = event_new(evbase_player, pb_timer_fd, EV_READ, player_playback_cb, NULL);
+# else
+  pb_timer_ev = evsignal_new(evbase_player, SIGALRM, player_playback_cb, NULL);
+# endif
+  if (!pb_timer_ev)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer event\n");
+      goto evnew_fail;
+    }
 #else
-  event_set(&exitev, exit_pipe[0], EV_READ, exit_cb, NULL);
-#endif
-  event_base_set(evbase_player, &exitev);
-  event_add(&exitev, NULL);
+  exitev = (struct event *)malloc(sizeof(struct event));
+  if (!exitev)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not create exit event\n");
+      goto evnew_fail;
+    }
+  event_set(exitev, exit_pipe[0], EV_READ, exit_cb, NULL);
+  event_base_set(evbase_player, exitev);
+
+  cmdev = (struct event *)malloc(sizeof(struct event));
+  if (!cmdev)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not create cmd event\n");
+      goto evnew_fail;
+    }
+  event_set(cmdev, cmd_pipe[0], EV_READ, command_cb, NULL);
+  event_base_set(evbase_player, cmdev);
 
-  event_set(&cmdev, cmd_pipe[0], EV_READ, command_cb, NULL);
-  event_base_set(evbase_player, &cmdev);
-  event_add(&cmdev, NULL);
+  pb_timer_ev = (struct event *)malloc(sizeof(struct event));
+  if (!pb_timer_ev)
+    {
+      DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer event\n");
+      goto evnew_fail;
+    }
+# if defined(__linux__)
+  event_set(pb_timer_ev, pb_timer_fd, EV_READ, player_playback_cb, NULL);
+# else
+  signal_set(pb_timer_ev, SIGALRM, player_playback_cb, NULL);
+# endif
+  event_base_set(evbase_player, pb_timer_ev);
+#endif /* HAVE_LIBEVENT2 */
+
+  event_add(exitev, NULL);
+  event_add(cmdev, NULL);
+
+#ifndef __linux__
+  event_add(pb_timer_ev, NULL);
+#endif
 
   ret = laudio_init(player_laudio_status_cb);
   if (ret < 0)
@@ -4698,19 +4740,22 @@ player_init(void)
  raop_fail:
   laudio_deinit();
  laudio_fail:
+ evnew_fail:
   event_base_free(evbase_player);
  evbase_fail:
   close(cmd_pipe[0]);
   close(cmd_pipe[1]);
  cmd_fail:
-#ifdef USE_EVENTFD
-  close(exit_efd);
-#else
   close(exit_pipe[0]);
   close(exit_pipe[1]);
-#endif
  exit_fail:
   evbuffer_free(audio_buf);
+ audio_fail:
+#if defined(__linux__)
+  close(pb_timer_fd);
+#else
+  timer_delete(pb_timer);
+#endif
 
   return -1;
 }
@@ -4720,26 +4765,15 @@ void
 player_deinit(void)
 {
   int ret;
-
-#ifdef USE_EVENTFD
-  ret = eventfd_write(exit_efd, 1);
-  if (ret < 0)
-    {
-      DPRINTF(E_LOG, L_PLAYER, "Could not send exit event: %s\n", strerror(errno));
-
-      return;
-    }
-#else
   int dummy = 42;
 
   ret = write(exit_pipe[1], &dummy, sizeof(dummy));
   if (ret != sizeof(dummy))
     {
-      DPRINTF(E_LOG, L_PLAYER, "Could not write to exit fd: %s\n", strerror(errno));
+      DPRINTF(E_LOG, L_PLAYER, "Could not write to exit pipe: %s\n", strerror(errno));
 
       return;
     }
-#endif
 
   ret = pthread_join(tid_player, NULL);
   if (ret != 0)
@@ -4754,22 +4788,20 @@ player_deinit(void)
 
   free(history);
 
+  pb_timer_stop();
+#if defined(__linux__)
+  close(pb_timer_fd);
+#else
+  timer_delete(pb_timer);
+#endif
+
   evbuffer_free(audio_buf);
 
   laudio_deinit();
   raop_deinit();
 
-  if (event_initialized(&pb_timer_ev))
-    event_del(&pb_timer_ev);
-
-  event_del(&cmdev);
-
-#ifdef USE_EVENTFD
-  close(exit_efd);
-#else
   close(exit_pipe[0]);
   close(exit_pipe[1]);
-#endif
   close(cmd_pipe[0]);
   close(cmd_pipe[1]);
   cmd_pipe[0] = -1;
diff --git a/src/player.h b/src/player.h
index f233e36..dd68054 100644
--- a/src/player.h
+++ b/src/player.h
@@ -4,14 +4,9 @@
 
 #include <stdint.h>
 
-#if defined(__linux__)
 /* AirTunes v2 packet interval in ns */
 /* (352 samples/packet * 1e9 ns/s) / 44100 samples/s = 7981859 ns/packet */
 # define AIRTUNES_V2_STREAM_PERIOD 7981859
-#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
-/* AirTunes v2 packet interval in ms */
-# define AIRTUNES_V2_STREAM_PERIOD   8
-#endif
 
 /* AirTunes v2 number of samples per packet */
 #define AIRTUNES_V2_PACKET_SAMPLES  352
diff --git a/src/raop.c b/src/raop.c
index fa43cb3..bf77f8b 100644
--- a/src/raop.c
+++ b/src/raop.c
@@ -851,7 +851,7 @@ raop_metadata_prepare(int id, uint64_t rtptime)
       goto skip_artwork;
     }
 
-  ret = artwork_get_item_filename(dbmfi.path, 600, 600, ART_CAN_PNG | ART_CAN_JPEG, rmd->artwork);
+  ret = artwork_get_item(id, 600, 600, ART_CAN_PNG | ART_CAN_JPEG, rmd->artwork);
   if (ret < 0)
     {
       DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for '%s' (%d); no artwork will be sent\n", dbmfi.title, id);
diff --git a/src/spotify.c b/src/spotify.c
index 6fa978c..611975e 100644
--- a/src/spotify.c
+++ b/src/spotify.c
@@ -71,7 +71,6 @@ enum spotify_state
   SPOTIFY_STATE_PAUSED,
   SPOTIFY_STATE_STOPPING,
   SPOTIFY_STATE_STOPPED,
-  SPOTIFY_STATE_SEEKED,
 };
 
 struct audio_get_param
@@ -924,8 +923,6 @@ playback_seek(struct spotify_command *cmd)
 
   audio_fifo_flush();
 
-  g_state = SPOTIFY_STATE_SEEKED;
-
   return 0;
 }
 
@@ -1280,7 +1277,7 @@ static void connectionstate_updated(sp_session *session)
     {
       DPRINTF(E_LOG, L_SPOTIFY, "Connection to Spotify (re)established\n");
     }
-  else if ((g_state == SPOTIFY_STATE_PLAYING) || (g_state == SPOTIFY_STATE_SEEKED))
+  else if (g_state == SPOTIFY_STATE_PLAYING)
     {
       DPRINTF(E_LOG, L_SPOTIFY, "Music interrupted - connection error or logged out\n");
       spotify_playback_stop_nonblock();
diff --git a/src/spotify.h b/src/spotify.h
index 72a3dcc..e4fce85 100644
--- a/src/spotify.h
+++ b/src/spotify.h
@@ -3,7 +3,12 @@
 #define __SPOTIFY_H__
 
 #include "db.h"
-#include <event.h>
+#ifdef HAVE_LIBEVENT2
+# include <event2/event.h>
+# include <event2/buffer.h>
+#else
+# include <event.h>
+#endif
 
 int
 spotify_playback_play(struct media_file_info *mfi);
diff --git a/src/transcode.c b/src/transcode.c
index 836d72e..4806c36 100644
--- a/src/transcode.c
+++ b/src/transcode.c
@@ -41,7 +41,9 @@
 #include <libavcodec/avcodec.h>
 #include <libavformat/avformat.h>
 #include <libavutil/mathematics.h>
-#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
+#if defined(HAVE_LIBSWRESAMPLE)
+# include <libswresample/swresample.h>
+#elif defined(HAVE_LIBAVRESAMPLE)
 # include <libavutil/opt.h>
 # include <libavresample/avresample.h>
 #endif
@@ -73,7 +75,9 @@ struct transcode_ctx {
   int16_t *abuffer;
 
   /* Resampling */
-#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
+#if defined(HAVE_LIBSWRESAMPLE)
+  SwrContext *resample_ctx;
+#elif defined(HAVE_LIBAVRESAMPLE)
   AVAudioResampleContext *resample_ctx;
 #else
   ReSampleContext *resample_ctx;
@@ -162,12 +166,14 @@ transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted)
 #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
   AVFrame *frame = NULL;
   int got_frame;
-  int out_linesize;
   int out_samples;
 #elif LIBAVCODEC_VERSION_MAJOR >= 54 || (LIBAVCODEC_VERSION_MAJOR == 53 && LIBAVCODEC_VERSION_MINOR >= 35)
   AVFrame *frame = NULL;
   int got_frame;
 #endif
+#if defined(HAVE_LIBAVRESAMPLE)
+  int out_linesize;
+#endif
 
   processed = 0;
 
@@ -280,13 +286,27 @@ transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted)
 	    continue;
 #endif
 
+	  // TODO Use the AVFrame resampling API's - probably much safer and easier than the following mess
 	  if (ctx->need_resample)
 #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
 	    {
+	      out_samples = 0;
+# if defined(HAVE_LIBSWRESAMPLE)
+	      out_samples = av_rescale_rnd(
+		swr_get_delay(ctx->resample_ctx, ctx->acodec->sample_rate) + frame->nb_samples,
+		44100,
+		ctx->acodec->sample_rate,
+		AV_ROUND_UP
+	      );
+
+	      out_samples = swr_convert(ctx->resample_ctx, (uint8_t **)&ctx->re_abuffer, out_samples,
+	                                       (const uint8_t **)frame->data, frame->nb_samples);
+# elif defined(HAVE_LIBAVRESAMPLE)
 	      av_samples_get_buffer_size(&out_linesize, 2, frame->nb_samples, AV_SAMPLE_FMT_S16, 0);
 
-	      out_samples = avresample_convert(ctx->resample_ctx, (uint8_t **)&ctx->re_abuffer, out_linesize, frame->nb_samples,
+	      out_samples = avresample_convert(ctx->resample_ctx, (uint8_t **)&ctx->re_abuffer, out_linesize, XCODE_BUFFER_SIZE,
 	                                       (uint8_t **)frame->data, frame->linesize[0], frame->nb_samples);
+# endif
 	      if (out_samples < 0)
 		{
 		  DPRINTF(E_LOG, L_XCODE, "Resample returned no samples!\n");
@@ -367,7 +387,7 @@ transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted)
 #elif LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
   if (frame)
     avcodec_free_frame(&frame);
-#else
+#elif LIBAVCODEC_VERSION_MAJOR >= 54 || (LIBAVCODEC_VERSION_MAJOR == 53 && LIBAVCODEC_VERSION_MINOR >= 35)
   if (frame)
     av_free(frame);
 #endif
@@ -573,7 +593,7 @@ transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t
 
   if (ctx->need_resample)
     {
-#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
+#if defined(HAVE_LIBSWRESAMPLE) || defined(HAVE_LIBAVRESAMPLE)
       if (!ctx->acodec->channel_layout)
 	{
 	  DPRINTF(E_DBG, L_XCODE, "Resample requires channel_layout, but none from ffmpeg. Setting to default.\n");
@@ -584,8 +604,19 @@ transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t
       DPRINTF(E_DBG, L_XCODE, "Will resample, decoded stream is: %s, %d channels (layout %" PRIu64 "), %d Hz\n",
               av_get_sample_fmt_name(ctx->acodec->sample_fmt),  ctx->acodec->channels,
               ctx->acodec->channel_layout, ctx->acodec->sample_rate);
+#endif
+#if defined(HAVE_LIBSWRESAMPLE)
+      ctx->resample_ctx = swr_alloc_set_opts(
+	NULL,                 // we're allocating a new context
+	AV_CH_LAYOUT_STEREO,  // out_ch_layout
+	AV_SAMPLE_FMT_S16,    // out_sample_fmt
+	44100,                // out_sample_rate
+	ctx->acodec->channel_layout, // in_ch_layout
+	ctx->acodec->sample_fmt,     // in_sample_fmt
+	ctx->acodec->sample_rate,    // in_sample_rate
+	0,                    // log_offset
+	NULL);
 
-      ctx->resample_ctx = avresample_alloc_context();
       if (!ctx->resample_ctx)
 	{
 	  DPRINTF(E_LOG, L_XCODE, "Out of memory for resample context\n");
@@ -593,6 +624,31 @@ transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t
 	  goto setup_fail_codec;
 	}
 
+      ret = swr_init(ctx->resample_ctx);
+      if (ret < 0)
+	{
+	  DPRINTF(E_LOG, L_XCODE, "Could not open resample context\n");
+
+	  swr_free(&ctx->resample_ctx);
+	  goto setup_fail_codec;
+	}
+
+      ctx->re_abuffer = av_realloc(ctx->re_abuffer, XCODE_BUFFER_SIZE * 2);
+      if (!ctx->re_abuffer)
+	{
+	  DPRINTF(E_LOG, L_XCODE, "Could not allocate resample buffer\n");
+
+	  swr_free(&ctx->resample_ctx);
+	  goto setup_fail_codec;
+	}
+#elif defined(HAVE_LIBAVRESAMPLE)
+      ctx->resample_ctx = avresample_alloc_context();
+      if (!ctx->resample_ctx)
+	{
+	  DPRINTF(E_LOG, L_XCODE, "Out of memory for resample context\n");
+
+	  goto setup_fail_codec;
+	}
       av_opt_set_int(ctx->resample_ctx, "in_sample_fmt",      ctx->acodec->sample_fmt, 0);
       av_opt_set_int(ctx->resample_ctx, "out_sample_fmt",     AV_SAMPLE_FMT_S16, 0);
       av_opt_set_int(ctx->resample_ctx, "in_channel_layout",  ctx->acodec->channel_layout, 0);
@@ -617,7 +673,6 @@ transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t
 	  avresample_free(&ctx->resample_ctx);
 	  goto setup_fail_codec;
 	}
-
 #else
       DPRINTF(E_DBG, L_XCODE, "Setting up resampling (%d@%d)\n", ctx->acodec->channels, ctx->acodec->sample_rate);
 
@@ -699,7 +754,9 @@ transcode_cleanup(struct transcode_ctx *ctx)
 
   if (ctx->need_resample)
     {
-#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
+#if defined(HAVE_LIBSWRESAMPLE)
+      swr_free(&ctx->resample_ctx);
+#elif defined(HAVE_LIBAVRESAMPLE)
       avresample_free(&ctx->resample_ctx);
 #else
       audio_resample_close(ctx->resample_ctx);
@@ -712,15 +769,17 @@ transcode_cleanup(struct transcode_ctx *ctx)
 
 
 int
-transcode_needed(struct evkeyvalq *headers, char *file_codectype)
+transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype)
 {
-  const char *client_codecs;
-  const char *user_agent;
   char *codectype;
   cfg_t *lib;
   int size;
   int i;
 
+  // If client is a Remote we will AirPlay, which means we will transcode to PCM
+  if (user_agent && strcasestr(user_agent, "remote"))
+    return 1;
+
   if (!file_codectype)
     {
       DPRINTF(E_LOG, L_XCODE, "Can't proceed, codectype is unknown (null)\n");
@@ -763,10 +822,8 @@ transcode_needed(struct evkeyvalq *headers, char *file_codectype)
 	}
     }
 
-  client_codecs = evhttp_find_header(headers, "Accept-Codecs");
   if (!client_codecs)
     {
-      user_agent = evhttp_find_header(headers, "User-Agent");
       if (user_agent)
 	{
 	  DPRINTF(E_DBG, L_XCODE, "User-Agent: %s\n", user_agent);
@@ -789,12 +846,6 @@ transcode_needed(struct evkeyvalq *headers, char *file_codectype)
 
 	      client_codecs = itunes_codecs;
 	    }
-	  else if (strncmp(user_agent, "Remote", strlen("Remote")) == 0)
-	    {
-	      DPRINTF(E_DBG, L_XCODE, "Client is Remote, using iTunes codecs\n");
-
-	      client_codecs = itunes_codecs;
-	    }
 	  else if (strncmp(user_agent, "AppleCoreMedia", strlen("AppleCoreMedia")) == 0)
 	    {
 	      DPRINTF(E_DBG, L_XCODE, "Client is AppleCoreMedia, using iTunes codecs\n");
diff --git a/src/transcode.h b/src/transcode.h
index 46b3839..b266e9f 100644
--- a/src/transcode.h
+++ b/src/transcode.h
@@ -2,11 +2,10 @@
 #ifndef __TRANSCODE_H__
 #define __TRANSCODE_H__
 
-#include <event.h>
 #ifdef HAVE_LIBEVENT2
-# include <event2/http.h>
+# include <event2/buffer.h>
 #else
-# include "evhttp/evhttp.h"
+# include <event.h>
 #endif
 
 struct transcode_ctx;
@@ -24,6 +23,6 @@ void
 transcode_cleanup(struct transcode_ctx *ctx);
 
 int
-transcode_needed(struct evkeyvalq *headers, char *file_codectype);
+transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype);
 
 #endif /* !__TRANSCODE_H__ */

-- 
forked-daapd packaging



More information about the pkg-multimedia-commits mailing list