[SCM] forked-daapd/experimental: Imported Upstream version 22.3+dfsg1

rbalint at users.alioth.debian.org rbalint at users.alioth.debian.org
Tue Feb 24 05:57:54 UTC 2015


The following commit has been merged in the experimental branch:
commit 8a775a7538677c6c2222381c861843d92bc2a3ac
Author: Balint Reczey <balint at balintreczey.hu>
Date:   Tue Feb 24 05:59:02 2015 +0100

    Imported Upstream version 22.3+dfsg1

diff --git a/ChangeLog b/ChangeLog
index 287167d..7b85d90 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,10 @@
 ChangeLog for forked-daapd
 --------------------------
 
+version 22.1:
+	- artwork cache
+	- some Spotify fixing up
+
 version 22.0:
 	- queue handling improvements
 	- added DAAP cache, good for low-power devices like the RPi
diff --git a/INSTALL b/INSTALL
index 470a6a6..0998da5 100644
--- a/INSTALL
+++ b/INSTALL
@@ -23,8 +23,7 @@ sudo apt-get install \
  build-essential git autotools-dev autoconf libtool gettext gawk gperf \
  antlr3 libantlr3c-dev libconfuse-dev libunistring-dev libsqlite3-dev \
  libavcodec-dev libavformat-dev libswscale-dev libavutil-dev libasound2-dev \
- libmxml-dev libgcrypt11-dev libavahi-client-dev libavl-dev zlib1g-dev \
- libevent-dev
+ libmxml-dev libgcrypt11-dev libavahi-client-dev zlib1g-dev libevent-dev
 
 Depending on the version of libav/ffmpeg in your distribution you may also need
 libavresample-dev.
@@ -50,17 +49,48 @@ Finally, read the bit in the bottom of this document about init scripts, read
 the README, edit the configuration file and restart the server.
 
 
-Long version - requirements
----------------------------
+Quick version for Fedora
+------------------------
+
+If you haven't already enabled the free RPM fusion packages do that, since you
+will need ffmpeg. You can google how to do that. Then run:
+
+sudo yum install \
+ git automake autoconf gettext-devel gperf gawk libtool \
+ sqlite-devel libconfuse-devel libunistring-devel mxml-devel libevent-devel \
+ avahi-devel libgcrypt-devel zlib-devel alsa-lib-devel ffmpeg-devel
+
+Now you need to install ANTLR3, but you probably can't use the version that
+comes with the package manager. Instead you can install it by running this
+script:
+
+scripts/antlr34_install.sh
+
+Then run the following:
+
+ git clone https://github.com/ejurgensen/forked-daapd.git
+ cd forked-daapd
+ autoreconf -i
+ ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var
+ make
+ sudo make install
+
+Finally, read the bit in the bottom of this document about init scripts, read
+the README, edit the configuration file and start the server.
+
 
-System-specific requirements:
- - Linux:
-   + glibc 2.9+ (for signalfd)
-   + libasound (ALSA sound support - or you can use OSS4)
- - FreeBSD:
-   + OSS4 sound support
-   + libiconv
+Quick version for FreeBSD
+-------------------------
 
+The build process for FreeBSD is rather complicated, but the good news is that
+there is a script in the 'scripts' folder that will at least attempt to do all
+the work for you. And should the script not work for you, you can still look
+through it and use it as an installation guide.
+
+
+
+Long version - requirements
+---------------------------
 
 Required tools:
  - ANTLR v3 is required to build forked-daapd, along with its C runtime
@@ -92,8 +122,6 @@ Libraries:
         from <http://www.nongnu.org/confuse/>
  - libevent 1.4+ (best with version 1.4 or 2.1.4+)
         from <http://libevent.org/>
- - libavl 0.3.5
-        from <http://ftp.debian.org/debian/pool/main/liba/libavl>
  - MiniXML (aka mxml or libmxml)
         from <http://minixml.org/software.php>
  - gcrypt 1.2.0+
@@ -102,6 +130,8 @@ Libraries:
         from <http://zlib.net/>
  - libunistring 0.9.3+
         from <http://www.gnu.org/software/libunistring/#downloading>
+ - libasound (optional - ALSA support, recommended for Linux)
+	from <http://www.alsa-project.org/>
  - libflac (optional - FLAC support)
         from <http://flac.sourceforge.net/download.html>
  - taglib (optional - Musepack support)
@@ -116,9 +146,6 @@ Libraries:
 If using binary packages, remember that you need the development packages to
 build forked-daapd (usually named -dev or -devel).
 
-libavl is not the GNU libavl. There doesn't seem to be an upstream website
-anymore, but you can fetch it from any Debian mirror.
-
 sqlite3 needs to be built with support for the unlock notify API; this isn't
 always the case in binary packages, so you may need to rebuild sqlite3 to
 enable the unlock notify API (you can check for the presence of the
diff --git a/README b/README.md
similarity index 84%
rename from README
rename to README.md
index cad4bb2..a72dc25 100644
--- a/README
+++ b/README.md
@@ -1,5 +1,4 @@
-forked-daapd
-------------
+# forked-daapd
 
 forked-daapd is a Linux/FreeBSD DAAP (iTunes) and RSP (Roku) media server.
 
@@ -10,53 +9,50 @@ DAAP stands for Digital Audio Access Protocol, and is the protocol used
 by iTunes and friends to share/stream media libraries over the network.
 
 RSP is Roku's own media sharing protocol. Roku are the makers of the
-SoundBridge devices. See <http://www.roku.com>.
+SoundBridge devices. See http://www.roku.com.
 
 The source for this version of forked-daapd can be found here:
 
-  <https://github.com/ejurgensen/forked-daapd.git>
+  https://github.com/ejurgensen/forked-daapd.git
 
 The original (now unmaintained) source can be found here:
 
-  <http://git.debian.org/?p=users/jblache/forked-daapd.git>
+  http://git.debian.org/?p=users/jblache/forked-daapd.git
 
 forked-daapd is a complete rewrite of mt-daapd (Firefly Media Server).
 
 
-Contents of this README
------------------------
+## Contents of this README
 
-- Getting started
-- Supported clients
-- Using Remote
-- AirPlay devices/speakers
-- Local audio output
-- Supported formats
-- Streaming MPEG4
-- Playlists and internet radio
-- Artwork
-- Library
-- Command line and web interface
-- Spotify
-- LastFM
+- [Getting started](#getting-started)
+- [Supported clients](#supported-clients)
+- [Using Remote](#using-remote)
+- [AirPlay devices/speakers](#airplay-devicesspeakers)
+- [Local audio output](#local-audio-output)
+- [Supported formats](#supported-formats)
+- [Streaming MPEG4](#streaming-mpeg4)
+- [Playlists and internet radio](#playlists-and-internet-radio)
+- [Artwork](#artwork)
+- [Library](#library)
+- [Command line and web interface](#command-line-and-web-interface)
+- [Spotify](#spotify)
+- [LastFM](#lastfm)
 
 
-Getting started
----------------
+## Getting started
 
-After installation (see INSTALL) do the following:
+After installation (see [INSTALL](INSTALL)) do the following:
 
- 1. Edit the configuration file (usually /etc/forked-daapd.conf) to suit your
+ 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)
+ 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
+    `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
------------------
+## Supported clients
 
 forked-daapd supports 4 kinds of clients:
 
@@ -74,9 +70,8 @@ regardless of the protocol.
 Here is a list of working and non-working DAAP and Remote clients. The list is
 probably obsolete when you read it :-)
 
-+--------------------------+------------+--------+---------------+-----------------+
 |          Client          | Developer  |  Type  |   Platform    | Working (vers.) |
-+--------------------------+------------+--------+---------------+-----------------+
+| ------------------------ | ---------- | ------ | ------------- | --------------- |
 | iTunes                   | Apple      | DAAP   | Win, OSX      | Yes (11.2)      |
 | Rhythmbox                | Gnome      | DAAP   | Linux         | Yes             |
 | WinAmp DAAPClient        | WardFamily | DAAP   | WinAmp        | Yes             |
@@ -89,19 +84,16 @@ probably obsolete when you read it :-)
 | Remote for iTunes        | Hyperfine  | Remote | Android       | Yes             |
 | Remote for Windows Phone | Komodex    | Remote | Windows Phone | Yes (2.2.1.0)   |
 | TunesRemote SE           |            | Remote | Java          | Yes (r108)      |
-+--------------------------+------------+--------+---------------+-----------------+
 
 
 
-Using Remote
-------------
+## Using Remote
 
 If you plan to use Remote with forked-daapd, read the following sections
 carefully. The pairing process described is similar for other controllers, but
 some do not require pairing.
 
-  Pairing with Remote on iPod/iPhone
-  ----------------------------------
+### Pairing with Remote on iPod/iPhone
 
 forked-daapd can be paired with Apple's Remote application for iPod/iPhone/iPad;
 this is how the pairing process works:
@@ -109,26 +101,29 @@ this is how the pairing process works:
  1. Start forked-daapd
  2. Start Remote, go to Settings, Add Library
  3. Look in the log file for a message saying:
-
-      "Discovered remote 'Foobar' (id 71624..."
-
+    
+    ```
+    "Discovered remote 'Foobar' (id 71624..."
+    ```
+   
     This tells you the name of your device (Foobar in this example).
-
+    
     If you cannot find this message, it means that forked-daapd did not receive
     a mDNS announcement from your Remote. You have a network issue and mDNS
     doesn't work properly on your network.
-
+    
  4. Prepare a text file with a filename ending with .remote; the filename
     doesn't matter, only the .remote ending does. This file must contain
     two lines: the first line is the name of your iPod/iPhone/iPad, the second
     is the 4-digit pairing code displayed by Remote.
-
+    
     If your iPod/iPhone/iPad is named "Foobar" and Remote gives you the pairing
     code 5387, the file content must be:
-
-      Foobar
-      5387 
-
+    ```
+    Foobar
+    5387 
+    ```
+    
  5. Move this file somewhere in your library
 
 At this point, you should be done with the pairing process and Remote should
@@ -143,16 +138,18 @@ or pairing code. Start over the pairing process and try again.
 
 If you have trouble pairing with forked-daapd, you can use avahi-browse for 
 troubleshooting:
- - in a terminal, run avahi-browse -r -k _touch-remote._tcp
+ - in a terminal, run `avahi-browse -r -k _touch-remote._tcp`
  - start Remote, goto Settings, Add Library
  - after a couple seconds at most, you should get something similar to this:
 
+```
 + ath0 IPv4 59eff13ea2f98dbbef6c162f9df71b784a3ef9a3      _touch-remote._tcp   local
 = ath0 IPv4 59eff13ea2f98dbbef6c162f9df71b784a3ef9a3      _touch-remote._tcp   local
    hostname = [Foobar.local]
    address = [192.168.1.1]
    port = [49160]
    txt = ["DvTy=iPod touch" "RemN=Remote" "txtvers=1" "RemV=10000" "Pair=FAEA410630AEC05E" "DvNm=Foobar"]
+```
 
 The name of your iPod/iPhone/iPad is the value of the DvNm field above. In this
 example, the correct value is Foobar.
@@ -165,8 +162,7 @@ capture the output in a file to extract the real, correct name.
 
 Hit Ctrl-C to terminate avahi-browse.
 
-  Selecting output devices
-  ------------------------
+### Selecting output devices
 
 Remote gets a list of output devices from the server; this list includes any
 and all devices on the network we know of that advertise AirPlay: AirPort
@@ -183,8 +179,7 @@ server startup, provided they appear in the 5 minutes following the startup
 and no playback has occured yet.
 
 
-AirPlay devices/speakers
-------------------------
+## AirPlay devices/speakers
 
 forked-daapd will discover the AirPlay devices available on your network. For
 devices that are password-protected, the device's AirPlay name and password
@@ -192,8 +187,7 @@ must be given in the configuration file. See the sample configuration file
 for the syntax.
 
 
-Local audio output
-------------------
+## Local audio output
 
 The audio section of the configuration file supports 2 parameters for the local
 audio device:
@@ -203,8 +197,7 @@ audio device:
    OSS4.
 
 
-Supported formats
------------------
+## Supported formats
 
 forked-daapd should support pretty much all media formats. It relies on libav
 (ffmpeg) to extract metadata and decode the files on the fly when the client
@@ -223,8 +216,7 @@ added. Currently supported:
  - WAV: wav
 
 
-Streaming MPEG4
----------------
+## Streaming MPEG4
 
 Depending on the client application, you may need to optimize your MPEG4 files
 for streaming. Stream-optimized MPEG4 files have their metadata at the beginning
@@ -240,7 +232,9 @@ for that.
 
 The mp4creator tool from the mpeg4ip suite can be used to optimize MPEG4 files,
 with the -optimize option:
+```
   $ mp4creator -optimize foo.m4a
+```
 
 Don't forget to make a backup copy of your file, just in case.
 
@@ -249,8 +243,7 @@ happily write the metadata back at the end of the file after you've modified
 them. Watch out for that.
 
 
-Playlists and internet radio
-----------------------------
+## Playlists and internet radio
 
 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.
@@ -268,8 +261,7 @@ the iTunes DB; use itunes_overrides = true if you prefer iTunes' metadata.
 Smart playlists are not supported at the moment.
 
 
-Artwork
--------
+## Artwork
 
 forked-daapd has support for artwork.
 
@@ -281,34 +273,31 @@ forked-daapd scales down (never up) the artwork on-the-fly to match the
 constraints given by the client. Note, however, that the bigger the picture,
 the more time and resources it takes to perform the scaling operation.
 
-As for the naming convention, it is quite simple; consider your foo.mp3 song,
-residing at /bar/foo.mp3:
- - if it has embedded artwork, this will be used as the artwork for this file;
- - failing that, if /bar/foo.{png,jpg} exists, it is used;
- - failing that, if /bar/{artwork,cover,Folder}.{png,jpg} exists, it is used;
- - failing that, if /bar/bar.{png,jpg} exists, it is used
-
-For "groups" (same album name and album artist), the situation is a bit
-different:
+The naming convention for album and artist artwork (group artwork) is as 
+follows:
  - if a file {artwork,cover,Folder}.{png,jpg} is found in one of the directories
    containing files that are part of the group, it is used as the artwork. The
    first file found is used, ordering is not guaranteed;
  - failing that, if [directory name].{png,jpg} is found in one of the
    directories containing files that are part of the group, it is used as the
    artwork. The first file found is used, ordering is not guaranteed;
- - failing that, individual files are examined and the first artwork found 
-   (embedded or in the same dir and named the same as the file) is used. Here
-   again, ordering is not guaranteed.
+ - failing that, individual files are examined and the first file found 
+   with an embedded artwork is used. Here again, ordering is not guaranteed.
+
+Artwork for individual songs is not supported, artwork for individual songs is 
+found by resolving to the group artwork.
 
 {artwork,cover,Folder} are the default, you can add other base names in the
 configuration file.
 
-You can use symlinks for the artwork files; the artwork is not scanned/indexed
-in any way in the database and there is no caching on forked-daapd's side.
+You can use symlinks for the artwork files; the artwork is not scanned/indexed.
 
+forked-daapd caches artwork in a separate cache file. The default path is 
+`/var/cache/forked-daapd/cache.db` and can be configured in the configuration 
+file. The cache.db file can be deleted without losing the library and pairing 
+informations.
 
-Library
--------
+## Library
 
 The library is scanned in bulk mode at startup, but the server will be available
 even while this scan is in progress. You can follow the progress of the scan in
@@ -348,8 +337,8 @@ Pipes have no metadata, so they will be added with "Unknown artist" and "Unknown
 album". The name of the pipe will be used as the track title.
 
 
-  Libraries on network mounts
-  --------------------------- 
+### Libraries on network mounts
+
 Most network filesharing protocols do not offer notifications when the library
 is changed. So that means forked-daapd cannot update its database in real time.
 Instead you can schedule a cron job to update the database.
@@ -357,18 +346,22 @@ Instead you can schedule a cron job to update the database.
 The first step in doing this is to add two entries to the 'directories'
 configuration item in forked-daapd.conf:
 
+```
   directories = { "/some/local/dir", "/your/network/mount/library" }
+```
 
 Now you can make a cron job that runs this command:
 
+```
   touch /some/local/dir/trigger.init-rescan
+```
 
 When forked-daapd detects a file with filename ending .init-rescan it will
 perform a bulk scan similar to the startup scan.
 
 
-  Troubleshooting library issues
-  ------------------------------
+### Troubleshooting library issues
+
 If you place a file with the filename ending .full-rescan in your library,
 you can trigger a full rescan of your library. This will clear all music and
 playlists from forked-daapd's database and initiate a fresh bulk scan. Pairing
@@ -377,8 +370,7 @@ not necessary during normal operation.
 
 
 
-Command line and web interface
-------------------------------
+## Command line and web interface
 
 forked-daapd is meant to be used with the clients mentioned above, so it does
 not have a command line interface nor does it have a web interface. You can,
@@ -390,28 +382,29 @@ Say you have a playlist with a radio station, and you want to make a script that
 starts playback of that station:
 
 1. Run 'sqlite3 [your forked-daapd db]'. Use 'select id,title from files' to get
-the id of the radio station, and use 'select id,title from playlists' to get the
-id of the playlist.
+   the id of the radio station, and use 'select id,title from playlists' to get the
+   id of the playlist.
 2. Convert the two ids to hex.
 3. Put the following lines in the script with the relevant ids inserted (also
-observe that you must use a session-id < 100, and that you must login and
-logout):
+   observe that you must use a session-id < 100, and that you must login and
+   logout):
 
+```
 curl "http://localhost:3689/login?pairing-guid=0x1&request-session-id=50"
 curl "http://localhost:3689/ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x[PLAYLIST-ID]'&container-item-spec='dmap.containeritemid:0x[FILE ID]'&session-id=50"
 curl "http://localhost:3689/logout?session-id=50"
+```
 
 
-Spotify
--------
+## Spotify
 
 forked-daapd has *some* support for Spotify. It must be compiled with the
---enable-spotify option (see INSTALL). You must have also have libspotify
+`--enable-spotify option` (see [INSTALL](INSTALL)). You must have also have libspotify
 installed, otherwise the Spotify integration will not be available. You can
 get libspotify here:
 
-  - Original (binary) tar.gz, see <https://developer.spotify.com>
-  - Debian package (libspotify-dev), see <https://apt.mopidy.com>
+  - Original (binary) tar.gz, see https://developer.spotify.com
+  - Debian package (libspotify-dev), see https://apt.mopidy.com
   
 You must also have a Spotify premium account. If you normally log into Spotify
 with your Facebook account you must first go to Spotify's web site where you can
@@ -441,10 +434,9 @@ forked-daapd do the playback - so that means you can't stream from forked-daapd
 to iTunes.
 
 
-LastFM
-------
+## LastFM
 
-If forked-daapd was built with LastFM scrobbling enabled (see the INSTALL file)
+If forked-daapd was built with LastFM scrobbling enabled (see the [INSTALL](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
diff --git a/configure.ac b/configure.ac
index 4fc034c..e378edb 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,10 +1,10 @@
 dnl Process this file with autoconf to produce a configure script.
 
-AC_INIT([forked-daapd], [22.0])
+AC_INIT([forked-daapd], [22.3])
 AC_CONFIG_SRCDIR([config.h.in])
 AC_CONFIG_MACRO_DIR([m4])
 AC_CONFIG_HEADERS([config.h])
-AM_INIT_AUTOMAKE([-Wno-portability subdir-objects])
+AM_INIT_AUTOMAKE([foreign -Wno-portability subdir-objects])
 
 AC_USE_SYSTEM_EXTENSIONS
 
@@ -40,6 +40,7 @@ AC_CHECK_HEADERS([sys/wait.h])
 AC_CHECK_HEADERS([sys/param.h])
 AC_CHECK_HEADERS([sys/select.h])
 AC_CHECK_HEADERS([dirent.h])
+AC_CHECK_HEADERS([regex.h])
 AC_CHECK_FUNCS(posix_fadvise)
 AC_CHECK_FUNCS(strptime)
 AC_CHECK_FUNCS(strtok_r)
@@ -70,45 +71,30 @@ AC_ARG_ENABLE(lastfm, AS_HELP_STRING([--enable-lastfm], [enable LastFM support (
                       use_lastfm=true;
                       CPPFLAGS="${CPPFLAGS} -DLASTFM")
 
+case "$host" in
+	*-*-linux-*)
+		use_oss4=false
+		;;
+	*-*-kfreebsd*-*|*-*-freebsd*)
+		use_oss4=true
+		;;
+esac
+
+AC_ARG_WITH(oss4, AS_HELP_STRING([--with-oss4], [use OSS4 (default Linux=no, FreeBSD=yes)]),
+                  use_oss4=true;
+                  )
+
+AC_ARG_WITH(alsa, AS_HELP_STRING([--with-alsa], [use ALSA (default Linux=yes, FreeBSD=no)]),
+                  use_oss4=false;
+                  )
+
 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
-    no)
-    case "$host" in
-      *-*-linux-*)
-      use_oss4=false
-      ;;
-      *-*-kfreebsd*-*|*-*-freebsd-*)
-      AC_MSG_ERROR([OSS4 must be enabled on FreeBSD systems])
-      ;;
-    esac
-    ;;
-    yes)
-    use_oss4=true
-    oss4_includedir=/usr/lib/oss/include/sys
-    ;;
-    [[\\/$]]* | ?:[[\\/]]* )
-    use_oss4=true
-    oss4_includedir="$withval"
-    ;;
-    *)
-    AC_MSG_ERROR([expected an absolute directory name for --with-oss4: $withval])
-    ;;
-    esac ],
-  [ case "$host" in
-    *-*-linux-*)
-    use_oss4=false
-    ;;
-    *-*-kfreebsd*-*|*-*-freebsd-*)
-    use_oss4=true
-    oss4_includedir=/usr/lib/oss/include/sys
-    ;;
-    esac ])
+AM_CONDITIONAL(COND_OSS4, test x$use_oss4 = xtrue)
+AM_CONDITIONAL(COND_ALSA, test x$use_oss4 != xtrue)
 
 dnl Checks for libraries.
 gl_LIBUNISTRING
@@ -159,11 +145,8 @@ fi
 PKG_CHECK_MODULES(LIBAV, [ libavformat libavcodec libswscale libavutil $libxxresample ])
 
 save_LIBS="$LIBS"
-dnl Check for av_lock_manager (ffmpeg >= 0.5.1)
-AC_CHECK_LIB([avcodec], [av_lockmgr_register], , AC_MSG_ERROR([libav (ffmpeg) >= 0.5.1 required]))
-dnl Check for avcodec_find_best_pix_fmt_of_list (is only in ffmpeg, not libav)
 AC_CHECK_LIB([avcodec], [avcodec_find_best_pix_fmt_of_list],
-	AC_DEFINE(FFMPEG_INCOMPATIBLE_API, 1, [Define to 1 if you have ffmpeg (and not libav).]))
+	AC_DEFINE(HAVE_FFMPEG, 1, [Define to 1 if you have ffmpeg (and not libav).]))
 LIBS="$save_LIBS"
 
 # Check for libavformat >= 53; url -> avio switch
@@ -200,10 +183,6 @@ else
 	)
 fi
 
-AC_CHECK_HEADER(avl.h, , AC_MSG_ERROR([avl.h not found]))
-AC_CHECK_LIB([avl], [avl_alloc_tree], [LIBAVL_LIBS="-lavl"], AC_MSG_ERROR([libavl not found]))
-AC_SUBST(LIBAVL_LIBS)
-
 AC_CHECK_HEADER(antlr3.h, , AC_MSG_ERROR([antlr3.h not found]))
 AC_CHECK_LIB([antlr3c], [antlr3BaseRecognizerNew], [ANTLR3C_LIBS="-lantlr3c"], AC_MSG_ERROR([ANTLR3 C runtime (libantlr3c) not found]))
 AC_CHECK_LIB([antlr3c], [antlr3NewAsciiStringInPlaceStream],
@@ -242,39 +221,25 @@ if test x$use_lastfm = xtrue; then
 		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
-        use_alsa=true
+if test x$use_oss4 != xtrue; then
 	PKG_CHECK_MODULES(ALSA, [ alsa ])
 	AC_DEFINE(LAUDIO_USE_ALSA, 1, [define if local audio output uses ALSA])
-    fi
+else
+	AC_CHECK_HEADER(sys/soundcard.h, , AC_MSG_ERROR([sys/soundcard.h not found]))
+fi
 
-    AC_CHECK_HEADERS([sys/eventfd.h])
-    AC_CHECK_FUNCS(eventfd_write)
+case "$host" in
+  *-*-linux-*)
+	AC_CHECK_HEADERS([sys/eventfd.h])
+	AC_CHECK_FUNC(eventfd_write, AC_DEFINE(HAVE_EVENTFD, 1, [Define to 1 if you have eventfd]))
 
-    AC_CHECK_HEADER(sys/signalfd.h, , AC_MSG_ERROR([signalfd required; glibc 2.9+ recommended]))
+	AC_CHECK_HEADER(sys/signalfd.h, , AC_MSG_ERROR([signalfd required; glibc 2.9+ recommended]))
 
-    AC_CHECK_HEADER(sys/timerfd.h, , AC_MSG_ERROR([timerfd required; glibc 2.8+ recommended]))
-    AC_CHECK_FUNC(timerfd_create, , AC_MSG_ERROR([timerfd required; glibc 2.8+ recommended]))
-    ;;
+	AC_CHECK_HEADER(sys/timerfd.h, , AC_MSG_ERROR([timerfd required; glibc 2.8+ recommended]))
+	AC_CHECK_FUNC(timerfd_create, , AC_MSG_ERROR([timerfd required; glibc 2.8+ recommended]))
+	;;
 esac
 
-if test x$use_oss4 = xtrue; then
-   OSS4CPPFLAGS="-I$oss4_includedir"
-
-   save_CPPFLAGS="$CPPFLAGS"
-   CPPFLAGS="$OSS4CPPFLAGS"
-
-   AC_CHECK_HEADER(soundcard.h, , AC_MSG_ERROR([soundcard.h not found in $oss4_includedir]))
-
-   CPPFLAGS="$save_CPPFLAGS"
-fi
-
-AM_CONDITIONAL(COND_ALSA, test x$use_alsa = xtrue)
-AM_CONDITIONAL(COND_OSS4, test x$use_oss4 = xtrue)
-AC_SUBST(OSS4CPPFLAGS)
-
 AC_CHECK_SIZEOF(void *)
 
 AC_CHECK_HEADERS(getopt.h,,)
diff --git a/forked-daapd.conf b/forked-daapd.conf
index 9f78371..870b3d1 100644
--- a/forked-daapd.conf
+++ b/forked-daapd.conf
@@ -9,21 +9,30 @@
 
 general {
 	# Username
+	# Make sure the user has read access to the library directories you set
+ 	# below, and full access to the databases, log and local audio
 	uid = "daapd"
-	logfile = "/var/log/forked-daapd.log"
+
 	# Database location
 #	db_path = "/var/cache/forked-daapd/songs3.db"
+
+	# Log file and level
 	# Available levels: fatal, log, warning, info, debug, spam
+	logfile = "/var/log/forked-daapd.log"
 	loglevel = log
+
 	# Admin password for the non-existent web interface
 	admin_password = "unused"
+
 	# Enable/disable IPv6
 	ipv6 = no
-	# Location of DAAP cache
-#	daapcache_path = "/var/cache/forked-daapd/daapcache.db"
+
+	# Location of cache database
+#	cache_path = "/var/cache/forked-daapd/cache.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
+#	cache_daap_threshold = 1000
 }
 
 # Library configuration
@@ -31,8 +40,10 @@ library {
 	# Name of the library as displayed by the clients
 	# %h: hostname, %v: version
 	name = "My Music on %h"
+
 	# TCP port to listen on. Default port is 3689 (daap)
 	port = 3689
+
 	# Password for the library. Optional.
 #	password = ""
 
@@ -80,11 +91,22 @@ library {
 	# forked-daapd will look for jpg and png files with these base names
 #	artwork_basenames = { "artwork", "cover", "Folder" }
 
+	# Enable searching for artwork corresponding to each individual media
+	# file instead of only looking for album artwork. This is disabled by
+	# default to reduce cache size.
+#	artwork_individual = false
+
 	# File types the scanner should ignore
 	# Non-audio files will never be added to the database, but here you
 	# can prevent the scanner from even probing them. This might improve
-	# scan time. By default .db and .ini are ignored.
-#	filetypes_ignore = { ".db", ".ini" }
+	# scan time. By default .db, .ini, .db-journal and .pdf are ignored.
+#	filetypes_ignore = { ".db", ".ini", ".db-journal", ".pdf" }
+
+	# File paths the scanner should ignore
+	# If you want to exclude files on a more advanced basis you can enter
+	# one or more POSIX regular expressions, and any file with a matching
+	# path will be ignored.
+#	filepath_ignore = { "myregex" }
 
 	# Disable startup file scanning
 	# When forked-daapd starts it will do an initial file scan of your
@@ -93,7 +115,7 @@ library {
 	# initial file scan and save some system ressources. Disabling this scan
 	# may lead to forked-daapd's database coming out of sync with the
 	# library. If that happens read the instructions in the README on how
-	# to trigger a full rescan.
+	# to trigger a rescan.
 #	filescan_disable = false
 
 	# Should iTunes metadata override ours?
@@ -110,8 +132,10 @@ library {
 audio {
 	# Name - used in the speaker list in Remote
 	nickname = "Computer"
+
 	# Audio device name for local audio output
 #	card = "default"
+
 	# Mixer channel to use for volume control - ALSA/Linux only
 	# If not set, PCM will be used if available, otherwise Master.
 #	mixer = ""
@@ -123,6 +147,7 @@ audio {
 	# forked-daapd's volume goes to 11! If that's more than you can handle
 	# you can set a lower value here
 #	max_volume = 11
+
 	# AirPlay password
 #	password = "s1kr3t"
 #}
@@ -131,9 +156,37 @@ audio {
 spotify {
 	# Directory where user settings should be stored (credentials)
 #	settings_dir = "/var/cache/forked-daapd/libspotify"
+
 	# Cache directory
 #	cache_dir = "/tmp"
+
 	# Set preferred bitrate for music streaming
 	# 0: No preference (default), 1: 96kbps, 2: 160kbps, 3: 320kbps
 #	bitrate = 0
 }
+
+# SQLite configuration (allows to modify the operation of the SQLite databases)
+# Make sure to read the SQLite documentation for the corresponding PRAGMA statements as
+# changing them from the defaults may increase the possibility of database corruptions!
+# By default the SQLite default values are used. 
+sqlite {
+	# Cache size in number of db pages for the library database
+	# (SQLite default page size is 1024 bytes and cache size is 2000 pages)
+#	pragma_cache_size_library = 2000
+	
+	# Cache size in number of db pages for the daap cache database
+	# (SQLite default page size is 1024 bytes and cache size is 2000 pages)
+#	pragma_cache_size_cache = 2000
+	
+	# Sets the journal mode for the database
+	# DELETE (default), TRUNCATE, PERSIST, MEMORY, WAL, OFF 
+#	pragma_journal_mode = DELETE
+	
+	# Change the setting of the "synchronous" flag
+	# 0: OFF, 1: NORMAL, 2: FULL (default)
+#	pragma_synchronous = 2
+
+	# Should the database be vacuumed on startup? (increases startup time,
+	# but may reduce database size). Default is yes.
+#	vacuum = yes
+}
diff --git a/scripts/antlr34_install.sh b/scripts/antlr34_install.sh
new file mode 100755
index 0000000..6cb6918
--- /dev/null
+++ b/scripts/antlr34_install.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+WORKDIR=~/antlr34.tmp
+echo "This script will install antlr 3.4 (and matching libantlr) on your computer."
+read -p "Should the script create $WORKDIR and use it for building? [Y/n] " yn
+if [ "$yn" = "n" ]; then
+	exit
+fi
+mkdir -p $WORKDIR
+if [ ! -d $WORKDIR ]; then
+	echo "Error creating $WORKDIR"
+	exit
+fi
+cd $WORKDIR
+
+read -p "Should the script download and build antlr and libantlr3c? [Y/n] " yn
+if [ "$yn" = "n" ]; then
+	exit
+fi
+read -p "Should the script install with prefix /usr or /usr/local? [U/l] " yn
+if [ "$yn" = "l" ]; then
+	PREFIX=/usr/local
+else
+	PREFIX=/usr
+fi
+read -p "Should the script build libantlr3c for 64 bit? [Y/n] " yn
+if [ "$yn" != "n" ]; then
+	ENABLE64BIT="--enable-64bit"
+fi
+wget --no-check-certificate https://github.com/antlr/website-antlr3/raw/gh-pages/download/antlr-3.4-complete.jar
+wget --no-check-certificate https://github.com/antlr/website-antlr3/raw/gh-pages/download/C/libantlr3c-3.4.tar.gz
+tar xzf libantlr3c-3.4.tar.gz
+cd libantlr3c-3.4
+./configure $ENABLE64BIT --prefix=$PREFIX && make && sudo make install
+cd $WORKDIR
+
+sudo mkdir -p "$PREFIX/share/java"
+sudo install antlr-3.4-complete.jar "$PREFIX/share/java"
+printf "#!/bin/sh
+export CLASSPATH
+CLASSPATH=\$CLASSPATH:$PREFIX/share/java/antlr-3.4-complete.jar:$PREFIX/share/java
+/usr/bin/java org.antlr.Tool \$*
+" > antlr3
+sudo install antlr3 "$PREFIX/bin"
+
diff --git a/scripts/freebsd_install_10.1.sh b/scripts/freebsd_install_10.1.sh
new file mode 100755
index 0000000..520d3c7
--- /dev/null
+++ b/scripts/freebsd_install_10.1.sh
@@ -0,0 +1,139 @@
+#!/bin/sh
+# Credit thorsteneckel who made the how-to that is the basis for this
+# script, see https://gist.github.com/thorsteneckel/c0610fb415c8d0486bce
+
+echo "This script will install forked-daapd in FreeBSD 10.1. The script is not"
+echo "very polished, so you might want to look through it before running it."
+read -p "Continue? [y/N] " yn
+if [ "$yn" != "y" ]; then
+	exit
+fi
+
+DEPS="gmake autoconf automake libtool gettext gperf glib pkgconf wget git \
+     ffmpeg libconfuse libevent2 mxml libgcrypt libunistring libiconv \
+     libplist avahi sqlite3"
+echo "The script can install the following dependency packages for you:"
+echo $DEPS
+read -p "Should the script install these packages? [y/N] " yn
+if [ "$yn" = "y" ]; then
+	sudo pkg install $DEPS;
+fi
+
+JRE="openjdk8-jre"
+read -p "Should the script install $JRE for you? [y/N] " yn
+if [ "$yn" = "y" ]; then
+	sudo pkg install $JRE;
+	read -p "Should the script add the mount points to /etc/fstab that $JRE requests? [y/N] " yn
+	if [ "$yn" = "y" ]; then
+		sudo sh -c 'echo "fdesc	/dev/fd	fdescfs	rw	0	0" >> /etc/fstab'
+		sudo sh -c 'echo "proc	/proc	procfs	rw	0	0" >> /etc/fstab'
+		sudo mount /dev/fd
+		sudo mount /proc
+	fi
+fi
+
+WORKDIR=~/forked-daapd_build
+CONFIG=/usr/local/etc/forked-daapd.conf
+read -p "Should the script create $WORKDIR and use it for building? [Y/n] " yn
+if [ "$yn" = "n" ]; then
+	exit
+fi
+mkdir -p $WORKDIR
+if [ ! -d $WORKDIR ]; then
+	echo "Error creating $WORKDIR"
+	exit
+fi
+cd $WORKDIR
+
+read -p "Should the script install antlr and libantlr3c? [y/N] " yn
+if [ "$yn" = "y" ]; then
+	read -p "Should the script build libantlr3c for 64 bit? [Y/n] " yn
+	if [ "$yn" != "n" ]; then
+		ENABLE64BIT="--enable-64bit"
+	fi
+
+	wget --no-check-certificate https://github.com/antlr/website-antlr3/raw/gh-pages/download/antlr-3.4-complete.jar
+	wget --no-check-certificate https://github.com/antlr/website-antlr3/raw/gh-pages/download/C/libantlr3c-3.4.tar.gz
+
+	sudo install antlr-3.4-complete.jar /usr/local/share/java
+	printf "#!/bin/sh
+export CLASSPATH
+CLASSPATH=\$CLASSPATH:/usr/local/share/java/antlr-3.4-complete.jar:/usr/local/share/java
+/usr/local/bin/java org.antlr.Tool \$*
+" > antlr3
+	sudo install antlr3 /usr/local/bin
+
+	tar xzf libantlr3c-3.4.tar.gz
+	cd libantlr3c-3.4
+	./configure $ENABLE64BIT && gmake && sudo gmake install
+	cd $WORKDIR
+fi
+
+read -p "Should the script build forked-daapd? [y/N] " yn
+if [ "$yn" = "y" ]; then
+	git clone https://github.com/ejurgensen/forked-daapd.git
+	cd forked-daapd
+
+	#Cleanup in case this is a re-run
+	gmake clean
+	git clean -f
+	autoreconf -vi
+
+#These should no longer be required, but if you run into trouble you can try enabling them
+#export CC=cc
+#export LIBUNISTRING_CFLAGS=-I/usr/include
+#export LIBUNISTRING_LIBS=-L/usr/lib
+#export ZLIB_CFLAGS=-I/usr/include
+#export ZLIB_LIBS=-L/usr/lib
+
+	export CFLAGS="-march=native -g -I/usr/local/include -I/usr/include"
+	export LDFLAGS="-L/usr/local/lib -L/usr/lib"
+	./configure --build=i386-portbld-freebsd10.1 && gmake
+
+	read -p "Should the script install forked-daapd and add service startup scripts? [y/N] " yn
+	if [ "$yn" = "y" ]; then
+		if [ -f $CONFIG ]; then
+			echo "Backing up old config file to $CONFIG.bak"
+			sudo cp "$CONFIG" "$CONFIG.bak"
+		fi
+		sudo gmake install
+
+		sudo sed -i -- 's/\/var\/cache/\/usr\/local\/var\/cache/g' $CONFIG
+		# Setup user and startup scripts
+		echo "daapd::::::forked-daapd:/nonexistent:/usr/sbin/nologin:" | sudo adduser -w no -D -f -
+		sudo chown -R daapd:daapd /usr/local/var/cache/forked-daapd
+		if [ ! -f scripts/freebsd_start_10.1.sh ]; then
+			echo "Could not find FreeBSD startup script"
+			exit
+		fi
+		sudo install scripts/freebsd_start_10.1.sh /usr/local/etc/rc.d/forked-daapd
+
+		service forked-daapd enabled
+		if [ $? -ne 0 ]; then
+			sudo sh -c 'echo "forked_daapd_enable=\"YES\"" >> /etc/rc.conf'
+		fi
+	fi
+
+	cd $WORKDIR
+fi
+
+read -p "Should the script enable and start dbus and avahi-daemon? [y/N] " yn
+if [ "$yn" = "y" ]; then
+	service dbus enabled
+	if [ $? -ne 0 ]; then
+		sudo sh -c 'echo "dbus_enable=\"YES\"" >> /etc/rc.conf'
+	fi
+	sudo service dbus start
+
+	service avahi-daemon enabled
+	if [ $? -ne 0 ]; then
+		sudo sh -c 'echo "avahi_daemon_enable=\"YES\"" >> /etc/rc.conf'
+	fi
+	sudo service avahi-daemon start
+fi
+
+read -p "Should the script (re)start forked-daapd and display the log output? [y/N] " yn
+if [ "$yn" = "y" ]; then
+	sudo service forked-daapd restart
+	tail -f /var/log/forked-daapd.log
+fi
diff --git a/scripts/freebsd_start_10.1.sh b/scripts/freebsd_start_10.1.sh
new file mode 100755
index 0000000..ebefd81
--- /dev/null
+++ b/scripts/freebsd_start_10.1.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+ 
+# PROVIDE: forked-daapd
+# REQUIRE: avahi_daemon dbus
+ 
+# Add the following lines to /etc/rc.conf to enable `forked-daapd':
+#
+# forked_daapd_enable="YES"
+# forked_daapd_flags="<set as needed>"
+ 
+. /etc/rc.subr
+ 
+name="forked_daapd"
+rcvar=`set_rcvar`
+ 
+command="/usr/local/sbin/forked-daapd"
+command_args="-P /var/run/forked-daapd.pid"
+pidfile="/var/run/forked-daapd.pid"
+required_files="/usr/local/etc/forked-daapd.conf"
+ 
+# read configuration and set defaults
+load_rc_config "$name"
+: ${forked_daapd_enable="NO"}
+ 
+run_rc_command "$1"
diff --git a/scripts/pairinghelper.sh b/scripts/pairinghelper.sh
new file mode 100755
index 0000000..6c19682
--- /dev/null
+++ b/scripts/pairinghelper.sh
@@ -0,0 +1,64 @@
+#!/bin/sh
+
+# Set location of the config file
+conf_path=/etc/forked-daapd.conf
+
+if [ ! -f $conf_path ]; then
+	echo "Error: Couldn't find $conf_path"
+	echo "Set the correct config file location in the script"
+	exit
+fi
+
+logfile=`awk '$1=="logfile"{print $3}' $conf_path`
+logfile="${logfile%\"}"
+logfile="${logfile#\"}"
+library_path=`awk '$1=="directories"{print $4}' $conf_path`
+library_path="${library_path%\"}"
+library_path="${library_path#\"}"
+
+if [ ! -f $logfile ]; then
+	echo "Error: Couldn't find logfile in $logfile"
+	exit
+fi
+if [ ! -d $library_path ]; then
+	echo "Error: Couldn't find library in $library_path"
+	exit
+fi
+
+echo "This script will help you pair Remote with forked-daapd"
+echo "Please verify that these paths are correct:"
+echo "  Log file: $logfile"
+echo "  Library:  $library_path"
+read -p "Confirm? [Y/n] " yn
+if [ "$yn" = "n" ]; then
+	exit
+fi
+echo "Please start the pairing process in Remote by selecting Add library"
+read -p "Press ENTER when ready..." yn
+echo -n "Looking in $logfile for Remote announcement..."
+sleep 5
+
+remote=`grep "Discovered remote" $logfile | tail -1 | grep -Po "'.*?'"`
+remote="${remote%\'}"
+remote="${remote#\'}"
+
+if [ -z "$remote" ]; then
+	echo "not found"
+	exit
+else
+	echo "found"
+fi
+
+read -p "Ready to pair Remote '$remote', please enter PIN: " pin
+if [ -z "$pin" ]; then
+	echo "Error: Invalid PIN"
+	exit
+fi
+
+echo "Writing pair.remote to $library_path..."
+echo -e "$remote\n$pin" > "$library_path/pair.remote"
+sleep 1
+echo "Removing pair.remote from library again..."
+rm "$library_path/pair.remote"
+echo "All done"
+
diff --git a/src/DAAP2SQL.g b/src/DAAP2SQL.g
index 93e9a42..ac23a6e 100644
--- a/src/DAAP2SQL.g
+++ b/src/DAAP2SQL.g
@@ -69,11 +69,7 @@ expr	returns [ pANTLR3_STRING result, int valid ]
 @init { $result = NULL; $valid = 1; }
 	:	^(OPAND a = expr b = expr)
 		{
-			if (!$a.valid || !$b.valid)
-			{
-				$valid = 0;
-			}
-			else
+			if ($a.valid && $b.valid)
 			{
 				$result = $a.result->factory->newRaw($a.result->factory);
 				$result->append8($result, "(");
@@ -82,15 +78,25 @@ expr	returns [ pANTLR3_STRING result, int valid ]
 				$result->appendS($result, $b.result);
 				$result->append8($result, ")");
 			}
-		}
-	|	^(OPOR a = expr b = expr)
-		{
-			if (!$a.valid || !$b.valid)
+			else if ($a.valid)
 			{
-				$valid = 0;
+				$result = $a.result->factory->newRaw($a.result->factory);
+				$result->appendS($result, $a.result);
+			}
+			else if ($b.valid)
+			{
+				$result = $b.result->factory->newRaw($b.result->factory);
+				$result->appendS($result, $b.result);
 			}
 			else
 			{
+				$valid = 0;
+			}
+		}
+	|	^(OPOR a = expr b = expr)
+		{
+			if ($a.valid && $b.valid)
+			{
 				$result = $a.result->factory->newRaw($a.result->factory);
 				$result->append8($result, "(");
 				$result->appendS($result, $a.result);
@@ -98,6 +104,20 @@ expr	returns [ pANTLR3_STRING result, int valid ]
 				$result->appendS($result, $b.result);
 				$result->append8($result, ")");
 			}
+			else if ($a.valid)
+			{
+				$result = $a.result->factory->newRaw($a.result->factory);
+				$result->appendS($result, $a.result);
+			}
+			else if ($b.valid)
+			{
+				$result = $b.result->factory->newRaw($b.result->factory);
+				$result->appendS($result, $b.result);
+			}
+			else
+			{
+				$valid = 0;
+			}
 		}
 	|	STR
 		{
@@ -194,13 +214,22 @@ expr	returns [ pANTLR3_STRING result, int valid ]
 					goto STR_result_valid_0; /* ABORT */
 				}
 
+				/* No need to exclude empty artist and album, as forked-daapd makes sure there always exists an artist/album. */
+				if (neg_op && op == ':'
+					&& (strcmp((char *)field, "daap.songalbumartist") == 0 
+						|| strcmp((char *)field, "daap.songartist") == 0 
+						|| strcmp((char *)field, "daap.songalbum") == 0))
+				{
+					DPRINTF(E_DBG, L_DAAP, "Ignoring clause '\%s\%s\%c'\n", field, (neg_op) ? "!" : "", op);
+					$valid = 0;
+					goto STR_result_valid_0;
+				}
+				
 				/* Need to check against NULL too */
 				if (op == ':')
 					$result->append8($result, "(");
 			}
 
-			$result->append8($result, dqfm->db_col);
-
 			/* Int field: check integer conversion */
 			if (dqfm->as_int)
 			{
@@ -225,6 +254,16 @@ expr	returns [ pANTLR3_STRING result, int valid ]
 				}
 
 				*end = '\0'; /* Cut out potential garbage - we're being kind */
+
+				/* forked-daapd only has media_kind = 1 for music - so remove media_kind = 32 to imporve select query performance. */
+				if (llval == 32
+					&& (strcmp((char *)field, "com.apple.itunes.mediakind") == 0 
+						|| strcmp((char *)field, "com.apple.itunes.extended-media-kind") == 0))
+				{
+					DPRINTF(E_DBG, L_DAAP, "Ignoring clause '\%s\%s\%c\%s'\n", field, (neg_op) ? "!" : "", op, val);
+					$valid = 0;
+					goto STR_result_valid_0;
+				}
 			}
 			/* String field: escape string, check for '*' */
 			else
@@ -252,12 +291,14 @@ expr	returns [ pANTLR3_STRING result, int valid ]
 					val[0] = '\%';
 				}
 
-				if (val[strlen((char *)val) - 1] == '*')
+				if (val[0] && val[1] && val[strlen((char *)val) - 1] == '*')
 				{
 					op = '\%';
 					val[strlen((char *)val) - 1] = '\%';
 				}
 			}
+			
+			$result->append8($result, dqfm->db_col);
 
 			switch(op)
 			{
diff --git a/src/Makefile.am b/src/Makefile.am
index 6701bd2..2468168 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -50,6 +50,8 @@ EVHTTP_SRC=
 RTSP_SRC=evrtsp/rtsp.c evrtp/evrtsp.h evrtsp/rtsp-internal.h evrtsp/log.h
 endif
 
+AVL_SRC=avl/avl.c avl/avl.h
+
 GPERF_FILES = \
 	daap_query.gperf \
 	rsp_query.gperf \
@@ -76,8 +78,7 @@ ANTLR_PRODUCTS =
 
 forked_daapd_CPPFLAGS = -D_GNU_SOURCE \
 	-DDATADIR="\"$(pkgdatadir)\"" -DCONFDIR="\"$(sysconfdir)\"" \
-	-DSTATEDIR="\"$(localstatedir)\"" -DPKGLIBDIR="\"$(pkglibdir)\"" \
-	@OSS4CPPFLAGS@
+	-DSTATEDIR="\"$(localstatedir)\"" -DPKGLIBDIR="\"$(pkglibdir)\""
 
 forked_daapd_CFLAGS = \
 	@ZLIB_CFLAGS@ @AVAHI_CFLAGS@ @SQLITE3_CFLAGS@ @LIBAV_CFLAGS@ \
@@ -88,7 +89,7 @@ forked_daapd_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@ \
+	@MINIXML_LIBS@ @ANTLR3C_LIBS@ @LIBPLIST_LIBS@ \
 	@LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @SPOTIFY_LIBS@ \
 	@LIBCURL_LIBS@
 
@@ -96,7 +97,8 @@ forked_daapd_SOURCES = main.c \
 	db.c db.h \
 	logger.c logger.h \
 	conffile.c conffile.h \
-	daap_cache.h daap_cache.c \
+	cache.c cache.h \
+	$(AVL_SRC) \
 	filescanner.c filescanner.h \
 	filescanner_ffmpeg.c filescanner_playlist.c filescanner_icy.c $(ITUNES_SRC) \
 	mdns_avahi.c mdns.h \
diff --git a/src/artwork.c b/src/artwork.c
index 2446890..de8b411 100644
--- a/src/artwork.c
+++ b/src/artwork.c
@@ -37,6 +37,7 @@
 #include "misc.h"
 #include "logger.h"
 #include "conffile.h"
+#include "cache.h"
 
 #if LIBAVFORMAT_VERSION_MAJOR >= 53
 # include "avio_evbuffer.h"
@@ -56,19 +57,18 @@ static const char *cover_extension[] =
     "jpg", "png",
   };
 
-
 static int
-artwork_read(char *filename, struct evbuffer *evbuf)
+artwork_read(char *path, struct evbuffer *evbuf)
 {
   uint8_t buf[4096];
   struct stat sb;
   int fd;
   int ret;
 
-  fd = open(filename, O_RDONLY);
+  fd = open(path, O_RDONLY);
   if (fd < 0)
     {
-      DPRINTF(E_WARN, L_ART, "Could not open artwork file '%s': %s\n", filename, strerror(errno));
+      DPRINTF(E_WARN, L_ART, "Could not open artwork file '%s': %s\n", path, strerror(errno));
 
       return -1;
     }
@@ -76,7 +76,7 @@ artwork_read(char *filename, struct evbuffer *evbuf)
   ret = fstat(fd, &sb);
   if (ret < 0)
     {
-      DPRINTF(E_WARN, L_ART, "Could not stat() artwork file '%s': %s\n", filename, strerror(errno));
+      DPRINTF(E_WARN, L_ART, "Could not stat() artwork file '%s': %s\n", path, strerror(errno));
 
       goto out_fail;
     }
@@ -152,7 +152,7 @@ rescale_needed(AVCodecContext *src, int max_w, int max_h, int *target_w, int *ta
 }
 
 static int
-artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, int format, struct evbuffer *evbuf)
+artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, struct evbuffer *evbuf)
 {
   uint8_t *buf;
   uint8_t *outbuf;
@@ -181,6 +181,9 @@ artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, int forma
 
   src = src_ctx->streams[s]->codec;
 
+  // Avoids threading issue in both ffmpeg and libav that prevents decoding embedded png's
+  src->thread_count = 1;
+
   img_decoder = avcodec_find_decoder(src->codec_id);
   if (!img_decoder)
     {
@@ -220,35 +223,29 @@ artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, int forma
   dst_fmt->video_codec = AV_CODEC_ID_NONE;
 
   /* Try to keep same codec if possible */
-  if ((src->codec_id == AV_CODEC_ID_PNG) && (format & ART_CAN_PNG))
+  if (src->codec_id == AV_CODEC_ID_PNG)
     dst_fmt->video_codec = AV_CODEC_ID_PNG;
-  else if ((src->codec_id == AV_CODEC_ID_MJPEG) && (format & ART_CAN_JPEG))
+  else if (src->codec_id == AV_CODEC_ID_MJPEG)
     dst_fmt->video_codec = AV_CODEC_ID_MJPEG;
 
   /* If not possible, select new codec */
   if (dst_fmt->video_codec == AV_CODEC_ID_NONE)
     {
-      if (format & ART_CAN_PNG)
-	dst_fmt->video_codec = AV_CODEC_ID_PNG;
-      else if (format & ART_CAN_JPEG)
-	dst_fmt->video_codec = AV_CODEC_ID_MJPEG;
+      dst_fmt->video_codec = AV_CODEC_ID_PNG;
     }
 #else
   dst_fmt->video_codec = CODEC_ID_NONE;
 
   /* Try to keep same codec if possible */
-  if ((src->codec_id == CODEC_ID_PNG) && (format & ART_CAN_PNG))
+  if (src->codec_id == CODEC_ID_PNG)
     dst_fmt->video_codec = CODEC_ID_PNG;
-  else if ((src->codec_id == CODEC_ID_MJPEG) && (format & ART_CAN_JPEG))
+  else if (src->codec_id == CODEC_ID_MJPEG)
     dst_fmt->video_codec = CODEC_ID_MJPEG;
 
   /* If not possible, select new codec */
   if (dst_fmt->video_codec == CODEC_ID_NONE)
     {
-      if (format & ART_CAN_PNG)
-	dst_fmt->video_codec = CODEC_ID_PNG;
-      else if (format & ART_CAN_JPEG)
-	dst_fmt->video_codec = CODEC_ID_MJPEG;
+      dst_fmt->video_codec = CODEC_ID_PNG;
     }
 #endif
 
@@ -317,7 +314,7 @@ artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, int forma
 #endif
 
 #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
-# ifndef FFMPEG_INCOMPATIBLE_API
+# ifndef HAVE_FFMPEG
   dst->pix_fmt = avcodec_find_best_pix_fmt2((enum AVPixelFormat *)img_encoder->pix_fmts, src->pix_fmt, 1, NULL);
 # else
   dst->pix_fmt = avcodec_find_best_pix_fmt_of_list((enum AVPixelFormat *)img_encoder->pix_fmts, src->pix_fmt, 1, NULL);
@@ -624,7 +621,7 @@ artwork_rescale(AVFormatContext *src_ctx, int s, int out_w, int out_h, int forma
 }
 
 static int
-artwork_get(char *filename, int max_w, int max_h, int format, struct evbuffer *evbuf)
+artwork_get(char *path, int max_w, int max_h, struct evbuffer *evbuf)
 {
   AVFormatContext *src_ctx;
   int s;
@@ -638,13 +635,13 @@ artwork_get(char *filename, int max_w, int max_h, int format, struct evbuffer *e
   src_ctx = NULL;
 
 #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3)
-  ret = avformat_open_input(&src_ctx, filename, NULL, NULL);
+  ret = avformat_open_input(&src_ctx, path, NULL, NULL);
 #else
-  ret = av_open_input_file(&src_ctx, filename, NULL, 0, NULL);
+  ret = av_open_input_file(&src_ctx, path, NULL, 0, NULL);
 #endif
   if (ret < 0)
     {
-      DPRINTF(E_WARN, L_ART, "Cannot open artwork file '%s': %s\n", filename, strerror(AVUNERROR(ret)));
+      DPRINTF(E_WARN, L_ART, "Cannot open artwork file '%s': %s\n", path, strerror(AVUNERROR(ret)));
 
       return -1;
     }
@@ -675,7 +672,7 @@ artwork_get(char *filename, int max_w, int max_h, int format, struct evbuffer *e
       if (src_ctx->streams[s]->codec->codec_id == CODEC_ID_PNG)
 #endif
 	{
-	  format_ok = (format & ART_CAN_PNG) ? ART_FMT_PNG : 0;
+	  format_ok = ART_FMT_PNG;
 	  break;
 	}
 #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
@@ -684,14 +681,14 @@ artwork_get(char *filename, int max_w, int max_h, int format, struct evbuffer *e
       else if (src_ctx->streams[s]->codec->codec_id == CODEC_ID_MJPEG)
 #endif
 	{
-	  format_ok = (format & ART_CAN_JPEG) ? ART_FMT_JPEG : 0;
+	  format_ok = ART_FMT_JPEG;
 	  break;
 	}
     }
 
   if (s == src_ctx->nb_streams)
     {
-      DPRINTF(E_LOG, L_ART, "Artwork file '%s' not a PNG or JPEG file\n", filename);
+      DPRINTF(E_LOG, L_ART, "Artwork file '%s' not a PNG or JPEG file\n", path);
 
 #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21)
       avformat_close_input(&src_ctx);
@@ -706,12 +703,12 @@ artwork_get(char *filename, int max_w, int max_h, int format, struct evbuffer *e
   /* Fastpath */
   if (!ret && format_ok)
     {
-      ret = artwork_read(filename, evbuf);
+      ret = artwork_read(path, evbuf);
       if (ret == 0)
 	ret = format_ok;
     }
   else
-    ret = artwork_rescale(src_ctx, s, target_w, target_h, format, evbuf);
+    ret = artwork_rescale(src_ctx, s, target_w, target_h, evbuf);
 
 #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21)
   avformat_close_input(&src_ctx);
@@ -730,7 +727,7 @@ artwork_get(char *filename, int max_w, int max_h, int format, struct evbuffer *e
 
 #if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6)
 static int
-artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, struct evbuffer *evbuf)
+artwork_get_embedded_image(char *path, int max_w, int max_h, struct evbuffer *evbuf)
 {
   AVFormatContext *src_ctx;
   AVStream *src_st;
@@ -740,14 +737,14 @@ artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, str
   int format_ok;
   int ret;
 
-  DPRINTF(E_SPAM, L_ART, "Trying embedded artwork in %s\n", filename);
+  DPRINTF(E_SPAM, L_ART, "Trying embedded artwork in %s\n", path);
 
   src_ctx = NULL;
 
-  ret = avformat_open_input(&src_ctx, filename, NULL, NULL);
+  ret = avformat_open_input(&src_ctx, path, NULL, NULL);
   if (ret < 0)
     {
-      DPRINTF(E_WARN, L_ART, "Cannot open media file '%s': %s\n", filename, strerror(AVUNERROR(ret)));
+      DPRINTF(E_WARN, L_ART, "Cannot open media file '%s': %s\n", path, strerror(AVUNERROR(ret)));
 
       return -1;
     }
@@ -772,7 +769,7 @@ artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, str
 	  if (src_ctx->streams[s]->codec->codec_id == CODEC_ID_PNG)
 #endif
 	    {
-	      format_ok = (format & ART_CAN_PNG) ? ART_FMT_PNG : 0;
+	      format_ok = ART_FMT_PNG;
 	      break;
 	    }
 #if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
@@ -781,7 +778,7 @@ artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, str
 	  else if (src_ctx->streams[s]->codec->codec_id == CODEC_ID_MJPEG)
 #endif
 	    {
-	      format_ok = (format & ART_CAN_JPEG) ? ART_FMT_JPEG : 0;
+	      format_ok = ART_FMT_JPEG;
 	      break;
 	    }
 	}
@@ -789,13 +786,13 @@ artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, str
 
   if (s == src_ctx->nb_streams)
     {
-      DPRINTF(E_SPAM, L_ART, "Did not find embedded artwork in '%s'\n", filename);
+      DPRINTF(E_SPAM, L_ART, "Did not find embedded artwork in '%s'\n", path);
 
       avformat_close_input(&src_ctx);
       return -1;
     }
   else
-    DPRINTF(E_DBG, L_ART, "Found embedded artwork in '%s'\n", filename);
+    DPRINTF(E_DBG, L_ART, "Found embedded artwork in '%s'\n", path);
 
   src_st = src_ctx->streams[s];
 
@@ -827,7 +824,7 @@ artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, str
     {
       DPRINTF(E_DBG, L_ART, "Artwork too large, rescaling image\n");
 
-      ret = artwork_rescale(src_ctx, s, target_w, target_h, format, evbuf);
+      ret = artwork_rescale(src_ctx, s, target_w, target_h, evbuf);
     }
 
   avformat_close_input(&src_ctx);
@@ -842,280 +839,318 @@ artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, str
 }
 #endif
 
+/*
+ * Looks for basename(in_path).{png,jpg}, so if is in_path is /foo/bar.mp3 it
+ * will look for /foo/bar.png and /foo/bar.jpg
+ *
+ * @param in_path path to the item we are getting artwork for
+ * @param max_w maximum image width
+ * @param max_h maximum image height
+ * @param out_path path to artwork, input must be either NULL or char[PATH_MAX]
+ * @param evbuf the event buffer that will contain the (scaled) image
+ * @return ART_FMT_* on success, 0 on nothing found, -1 on error
+ */
 static int
-artwork_get_own_image(char *path, int max_w, int max_h, int format, struct evbuffer *evbuf)
+artwork_get_own_image(char *in_path, int max_w, int max_h, char *out_path, struct evbuffer *evbuf)
 {
-  char artwork[PATH_MAX];
+  char path[PATH_MAX];
   char *ptr;
   int len;
+  int nextensions;
   int i;
   int ret;
 
-  ret = snprintf(artwork, sizeof(artwork), "%s", path);
-  if ((ret < 0) || (ret >= sizeof(artwork)))
+  ret = snprintf(path, sizeof(path), "%s", in_path);
+  if ((ret < 0) || (ret >= sizeof(path)))
     {
-      DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX\n");
-
+      DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", in_path);
       return -1;
     }
 
-  ptr = strrchr(artwork, '.');
+  ptr = strrchr(path, '.');
   if (ptr)
     *ptr = '\0';
 
-  len = strlen(artwork);
+  len = strlen(path);
 
-  for (i = 0; i < (sizeof(cover_extension) / sizeof(cover_extension[0])); i++)
+  nextensions = sizeof(cover_extension) / sizeof(cover_extension[0]);
+
+  for (i = 0; i < nextensions; i++)
     {
-      ret = snprintf(artwork + len, sizeof(artwork) - len, ".%s", cover_extension[i]);
-      if ((ret < 0) || (ret >= sizeof(artwork) - len))
+      ret = snprintf(path + len, sizeof(path) - len, ".%s", cover_extension[i]);
+      if ((ret < 0) || (ret >= sizeof(path) - len))
 	{
-	  DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX (ext %s)\n", cover_extension[i]);
-
+	  DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s)\n", in_path);
 	  continue;
 	}
 
-      DPRINTF(E_SPAM, L_ART, "Trying own artwork file %s\n", artwork);
+      DPRINTF(E_SPAM, L_ART, "Trying own artwork file %s\n", path);
 
-      ret = access(artwork, F_OK);
+      ret = access(path, F_OK);
       if (ret < 0)
 	continue;
 
       break;
     }
 
-  if (i == (sizeof(cover_extension) / sizeof(cover_extension[0])))
-    return -1;
+  if (i == nextensions)
+    return 0;
+
+  DPRINTF(E_DBG, L_ART, "Found own artwork file %s\n", path);
 
-  DPRINTF(E_DBG, L_ART, "Found own artwork file %s\n", artwork);
+  if (out_path)
+    strcpy(out_path, path);
 
-  return artwork_get(artwork, max_w, max_h, format, evbuf);
+  return artwork_get(path, max_w, max_h, evbuf);
 }
 
+/*
+ * Looks for cover files in a directory, so if dir is /foo/bar and the user has
+ * configured the cover file names "cover" and "artwork" it will look for
+ * /foo/bar/cover.{png,jpg}, /foo/bar/artwork.{png,jpg} and also
+ * /foo/bar/bar.{png,jpg} (so called parentdir artwork)
+ *
+ * @param dir the directory to search
+ * @param max_w maximum image width
+ * @param max_h maximum image height
+ * @param out_path path to artwork, input must be either NULL or char[PATH_MAX]
+ * @param evbuf the event buffer that will contain the (scaled) image
+ * @return ART_FMT_* on success, 0 on nothing found, -1 on error
+ */
 static int
-artwork_get_dir_image(char *path, int isdir, int max_w, int max_h, int format, struct evbuffer *evbuf)
+artwork_get_dir_image(char *dir, int max_w, int max_h, char *out_path, struct evbuffer *evbuf)
 {
-  char artwork[PATH_MAX];
-  char *ptr;
+  char path[PATH_MAX];
+  char parentdir[PATH_MAX];
   int i;
   int j;
   int len;
   int ret;
   cfg_t *lib;
   int nbasenames;
+  int nextensions;
+  char *ptr;
 
-  ret = snprintf(artwork, sizeof(artwork), "%s", path);
-  if ((ret < 0) || (ret >= sizeof(artwork)))
+  ret = snprintf(path, sizeof(path), "%s", dir);
+  if ((ret < 0) || (ret >= sizeof(path)))
     {
-      DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX\n");
-
+      DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", dir);
       return -1;
     }
 
-  if (!isdir)
-    {
-      ptr = strrchr(artwork, '/');
-      if (ptr)
-	*ptr = '\0';
-    }
-
-  len = strlen(artwork);
+  len = strlen(path);
 
   lib = cfg_getsec(cfg, "library");
   nbasenames = cfg_size(lib, "artwork_basenames");
 
   if (nbasenames == 0)
-    return -1;
+    return 0;
+
+  nextensions = sizeof(cover_extension) / sizeof(cover_extension[0]);
 
   for (i = 0; i < nbasenames; i++)
     {
-      for (j = 0; j < (sizeof(cover_extension) / sizeof(cover_extension[0])); j++)
+      for (j = 0; j < nextensions; j++)
 	{
-	  ret = snprintf(artwork + len, sizeof(artwork) - len, "/%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
-	  if ((ret < 0) || (ret >= sizeof(artwork) - len))
+	  ret = snprintf(path + len, sizeof(path) - len, "/%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
+	  if ((ret < 0) || (ret >= sizeof(path) - len))
 	    {
-	      DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX (%s.%s)\n", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
-
+	      DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s/%s)\n", dir, cfg_getnstr(lib, "artwork_basenames", i));
 	      continue;
 	    }
 
-	  DPRINTF(E_SPAM, L_ART, "Trying directory artwork file %s\n", artwork);
+	  DPRINTF(E_SPAM, L_ART, "Trying directory artwork file %s\n", path);
 
-	  ret = access(artwork, F_OK);
+	  ret = access(path, F_OK);
 	  if (ret < 0)
 	    continue;
 
+	  // If artwork file exists (ret == 0), exit the loop
 	  break;
 	}
 
-      if (j < (sizeof(cover_extension) / sizeof(cover_extension[0])))
+      // In case the previous loop exited early, we found an existing artwork file and exit the outer loop
+      if (j < nextensions)
 	break;
     }
 
+  // If the loop for directory artwork did not exit early, look for parent directory artwork
   if (i == nbasenames)
-    return -1;
-
-  DPRINTF(E_DBG, L_ART, "Found directory artwork file %s\n", artwork);
-
-  return artwork_get(artwork, max_w, max_h, format, evbuf);
-}
-
-static int
-artwork_get_parentdir_image(char *path, int isdir, int max_w, int max_h, int format, struct evbuffer *evbuf)
-{
-  char artwork[PATH_MAX];
-  char parentdir[PATH_MAX];
-  char *ptr;
-  int len;
-  int i;
-  int ret;
-
-  ret = snprintf(artwork, sizeof(artwork), "%s", path);
-  if ((ret < 0) || (ret >= sizeof(artwork)))
-    {
-      DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX\n");
-
-      return -1;
-    }
-
-  if (!isdir)
     {
-      ptr = strrchr(artwork, '/');
+      ptr = strrchr(path, '/');
       if (ptr)
 	*ptr = '\0';
-    }
 
-  ptr = strrchr(artwork, '/');
-  if ((!ptr) || (strlen(ptr) <= 1))
-    return -1;
-  strcpy(parentdir, ptr + 1);
+      ptr = strrchr(path, '/');
+      if ((!ptr) || (strlen(ptr) <= 1))
+	{
+	  DPRINTF(E_LOG, L_ART, "Could not find parent dir name (%s)\n", path);
+	  return -1;
+	}
+      strcpy(parentdir, ptr + 1);
 
-  len = strlen(artwork);
+      len = strlen(path);
 
-  for (i = 0; i < (sizeof(cover_extension) / sizeof(cover_extension[0])); i++)
-    {
-      ret = snprintf(artwork + len, sizeof(artwork) - len, "/%s.%s", parentdir, cover_extension[i]);
-      if ((ret < 0) || (ret >= sizeof(artwork) - len))
+      for (i = 0; i < nextensions; i++)
 	{
-	  DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX (%s.%s)\n", parentdir, cover_extension[i]);
+	  ret = snprintf(path + len, sizeof(path) - len, "/%s.%s", parentdir, cover_extension[i]);
+	  if ((ret < 0) || (ret >= sizeof(path) - len))
+	    {
+	      DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s)\n", parentdir);
+	      continue;
+	    }
 
-	  continue;
-	}
+	  DPRINTF(E_SPAM, L_ART, "Trying parent directory artwork file %s\n", path);
 
-      DPRINTF(E_SPAM, L_ART, "Trying parent directory artwork file %s\n", artwork);
+	  ret = access(path, F_OK);
+	  if (ret < 0)
+	    continue;
 
-      ret = access(artwork, F_OK);
-      if (ret < 0)
-	continue;
+	  break;
+	}
 
-      break;
+      if (i == nextensions)
+	return 0;
     }
 
-  if (i == (sizeof(cover_extension) / sizeof(cover_extension[0])))
-    return -1;
+  DPRINTF(E_DBG, L_ART, "Found directory artwork file %s\n", path);
 
-  DPRINTF(E_DBG, L_ART, "Found parent directory artwork file %s\n", artwork);
+  if (out_path)
+    strcpy(out_path, path);
 
-  return artwork_get(artwork, max_w, max_h, format, evbuf);
+  return artwork_get(path, max_w, max_h, evbuf);
 }
 
+/*
+ * Given an artwork type (eg embedded, Spotify, own) this function will direct
+ * to the appropriate handler
+ *
+ * @param in_path path to the item we are getting artwork for
+ * @param artwork type of the artwork
+ * @param max_w maximum image width
+ * @param max_h maximum image height
+ * @param out_path path to artwork, input must be either NULL or char[PATH_MAX]
+ * @param evbuf the event buffer that will contain the (scaled) image
+ * @return ART_FMT_* on success, 0 on nothing found, -1 on error
+ */
 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)
+artwork_get_item_path(char *in_path, int artwork, int max_w, int max_h, char *out_path, struct evbuffer *evbuf)
 {
   int ret;
 
   ret = 0;
+  if (out_path)
+    strcpy(out_path, in_path);
+
   switch (artwork)
     {
       case ARTWORK_NONE:
 	break;
+      case ARTWORK_UNKNOWN:
+      case ARTWORK_OWN:
+	if (cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual"))
+	  ret = artwork_get_own_image(in_path, max_w, max_h, out_path, evbuf);	
+	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 = spotify_artwork_get(evbuf, in_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)
       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;
+	ret = artwork_get_embedded_image(in_path, max_w, max_h, evbuf);
+	break;
 #endif
-      default:
-	/* Not a normal file */
-	if (data_kind != 0)
-	  break;
-
-	/* Look for basename(filename).{png,jpg} */
-	ret = artwork_get_own_image(path, max_w, max_h, format, evbuf);
-	if (ret > 0)
-	  break;
-
-	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;
     }
 
-  if (ret > 0)
-    return ret;
-  else
-    return -1;
+  return ret;
 }
 
+/*
+ * Get the artwork for the given media file and the given maxiumum width/height
 
-int
-artwork_get_item(int id, int max_w, int max_h, int format, struct evbuffer *evbuf)
+ * @param mfi the media file structure for the file whose image should be returned
+ * @param max_w maximum image width
+ * @param max_h maximum image height
+ * @param evbuf the event buffer that will contain the (scaled) image
+ * @return ART_FMT_* on success, 0 on nothing found, -1 on error
+ */
+static int
+artwork_get_item_mfi(struct media_file_info *mfi, int max_w, int max_h, struct evbuffer *evbuf)
 {
-  struct media_file_info *mfi;
+  char path[PATH_MAX];
+  int cached;
+  int format;
   int ret;
 
-  DPRINTF(E_DBG, L_ART, "Artwork request for item %d\n", id);
+  DPRINTF(E_DBG, L_ART, "Looking for artwork for item with id %d\n", mfi->id);
 
-  mfi = db_file_fetch_byid(id);
-  if (!mfi)
-    return -1;
+  ret = cache_artwork_get(CACHE_ARTWORK_INDIVIDUAL, mfi->id, max_w, max_h, &cached, &format, evbuf);
+  if ((ret == 0) && cached)
+    {
+      DPRINTF(E_DBG, L_ART, "Item %d found in cache with format %d\n", mfi->id, format);
+      return format;
+    }
 
-  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 (%s)\n", id, mfi->fname);
+  if (mfi->data_kind == 0)
+    {
+      format = artwork_get_item_path(mfi->path, mfi->artwork, max_w, max_h, path, evbuf);
+      
+      if (format > 0)
+	cache_artwork_add(CACHE_ARTWORK_INDIVIDUAL, mfi->id, max_w, max_h, format, path, evbuf);
 
-  free_mfi(mfi, 0);
+      return format;
+    }
 
-  return ret;
+  return -1;
 }
 
-int
-artwork_get_group(int id, int max_w, int max_h, int format, struct evbuffer *evbuf)
+/*
+ * Get the artwork image for the given persistentid and the given maximum width/height
+ *
+ * The function first checks if there is a cache entry, if not it will first look for directory artwork files.
+ * If no directory artwork files are found, it looks for individual artwork (embedded images or images from spotify).
+ *
+ * @param persistentid persistent songalbumid or songartistid
+ * @param max_w maximum image width
+ * @param max_h maximum image height
+ * @param evbuf the event buffer that will contain the (scaled) image
+ * @return ART_FMT_* on success, 0 on nothing found, -1 on error
+ */
+static int
+artwork_get_group_persistentid(int64_t persistentid, int max_w, int max_h, struct evbuffer *evbuf)
 {
   struct query_params qp;
   struct db_media_file_info dbmfi;
+  char path[PATH_MAX];
   char *dir;
-  int got_art;
+  int cached;
+  int format;
   int ret;
   int artwork;
+  int got_spotifyitem;
   uint32_t data_kind;
 
-  DPRINTF(E_DBG, L_ART, "Artwork request for group %d\n", id);
+  DPRINTF(E_DBG, L_ART, "Looking for artwork for group with persistentid %" PRIi64 "\n", persistentid);
+
+  got_spotifyitem = 0;
 
-  /* Try directory artwork first */
+  /*
+   * First check if the artwork cache has a cached entry for the given persistent id and requested width/height
+   */
+  ret = cache_artwork_get(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, &cached, &format, evbuf);
+  if ((ret == 0) && cached)
+    {
+      DPRINTF(E_DBG, L_ART, "Group %" PRIi64 " found in cache with format %d\n", persistentid, format);
+      return format;
+    }
+
+  /* Image is not in the artwork cache. Try directory artwork first */
   memset(&qp, 0, sizeof(struct query_params));
 
   qp.type = Q_GROUP_DIRS;
-  qp.id = id;
+  qp.persistentid = persistentid;
 
   ret = db_query_start(&qp);
   if (ret < 0)
@@ -1126,27 +1161,33 @@ artwork_get_group(int id, int max_w, int max_h, int format, struct evbuffer *evb
       goto files_art;
     }
 
-  got_art = 0;
-  while (!got_art && ((ret = db_query_fetch_string(&qp, &dir)) == 0) && (dir))
+  format = 0;
+  while (((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)
+      /* The db query may return non-directories (eg if item is an internet stream or Spotify) */
+      if (access(dir, F_OK) < 0)
 	continue;
 
-      /* If Spotify item don't look for artwork */
-      if (strncmp(dir, "spotify:", strlen("spotify:")) == 0)
-	continue;
+      format = artwork_get_dir_image(dir, max_w, max_h, path, evbuf);
 
-      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);
+      if (format > 0)
+	break;	
     }
 
   db_query_end(&qp);
 
   if (ret < 0)
-    DPRINTF(E_LOG, L_ART, "Error fetching Q_GROUP_DIRS results\n");
-  else if (got_art > 0)
-    return got_art;
+    { 
+      DPRINTF(E_LOG, L_ART, "Error fetching Q_GROUP_DIRS results\n");
+      goto files_art;
+    }
+
+  /* Found artwork, cache it and return */  
+  if (format > 0)
+    {
+      cache_artwork_add(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, format, path, evbuf);
+      return format;
+    }
 
 
   /* Then try individual files */
@@ -1154,33 +1195,169 @@ artwork_get_group(int id, int max_w, int max_h, int format, struct evbuffer *evb
   memset(&qp, 0, sizeof(struct query_params));
 
   qp.type = Q_GROUP_ITEMS;
-  qp.id = id;
+  qp.persistentid = persistentid;
 
   ret = db_query_start(&qp);
   if (ret < 0)
     {
       DPRINTF(E_LOG, L_ART, "Could not start Q_GROUP_ITEMS query\n");
-
       return -1;
     }
 
-  got_art = 0;
-  while (!got_art && ((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id))
+  format = 0;
+  while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id))
     {
       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);
+      format = artwork_get_item_path(dbmfi.path, artwork, max_w, max_h, path, evbuf);
+
+      if (artwork == ARTWORK_SPOTIFY)
+	got_spotifyitem = 1;
+
+      if (format > 0)
+	break;
     }
 
   db_query_end(&qp);
 
   if (ret < 0)
-    DPRINTF(E_LOG, L_ART, "Error fetching Q_GROUP_ITEMS results\n");
-  else if (got_art > 0)
-    return got_art;
+    {
+      DPRINTF(E_LOG, L_ART, "Error fetching Q_GROUP_ITEMS results\n");
+      return -1;
+    }
 
-  DPRINTF(E_DBG, L_ART, "No artwork found for group %d\n", id);
+  /* Found artwork, cache it and return */
+  if (format > 0)
+    {
+      cache_artwork_add(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, format, path, evbuf);
+      return format;
+    }
+  else if (format < 0)
+    {
+      DPRINTF(E_WARN, L_ART, "Error getting artwork for group %" PRIi64 "\n", persistentid);
+      return -1;
+    }
 
-  return -1;
+  DPRINTF(E_DBG, L_ART, "No artwork found for group %" PRIi64 "\n", persistentid);
+
+  /* Add cache entry for no artwork available */
+  if (!got_spotifyitem)
+    cache_artwork_add(CACHE_ARTWORK_GROUP, persistentid, max_w, max_h, 0, "", evbuf);
+
+  return 0;
+}
+
+/*
+ * Get the artwork image for the given item id and the given maximum width/height
+ *
+ * @param id the mfi item id
+ * @param max_w maximum image width
+ * @param max_h maximum image height
+ * @param evbuf the event buffer that will contain the (scaled) image
+ * @return ART_FMT_* on success, -1 on error or no artwork found
+ */
+int
+artwork_get_item(int id, int max_w, int max_h, struct evbuffer *evbuf)
+{
+  struct media_file_info *mfi;
+  int format;
+
+  mfi = db_file_fetch_byid(id);
+  if (!mfi)
+    {
+      DPRINTF(E_LOG, L_ART, "Artwork request for item %d, but no such item in database\n", id);
+      return -1;
+    }
+
+  DPRINTF(E_DBG, L_ART, "Artwork request for item %d (%s)\n", id, mfi->fname);
+
+  format = 0;
+  if (cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual"))
+    format = artwork_get_item_mfi(mfi, max_w, max_h, evbuf);
+
+  /* No individual artwork or individual artwork disabled, try group artwork */
+  if (format <= 0)
+    format = artwork_get_group_persistentid(mfi->songalbumid, max_w, max_h, evbuf);
+
+  free_mfi(mfi, 0);
+
+  if (format <= 0)
+    {
+      DPRINTF(E_DBG, L_ART, "No artwork found for item %d\n", id);
+      return -1;
+    }
+
+  return format;
+}
+
+/*
+ * Get the artwork image for the given group id and the given maximum width/height
+ *
+ * @param id the group id (not the persistent id)
+ * @param max_w maximum image width
+ * @param max_h maximum image height
+ * @param evbuf the event buffer that will contain the (scaled) image
+ * @return ART_FMT_* on success, -1 on error or no artwork found
+ */
+int
+artwork_get_group(int id, int max_w, int max_h, struct evbuffer *evbuf)
+{
+  int64_t persistentid;
+  int format;
+
+  DPRINTF(E_DBG, L_ART, "Artwork request for group %d\n", id);
+
+  /* Get the persistent id for the given group id */
+  if (db_group_persistentid_byid(id, &persistentid) < 0)
+    {
+      DPRINTF(E_LOG, L_ART, "Error fetching persistent id for group id %d\n", id);
+      return -1;
+    }
+
+  /* Load artwork image for the persistent id */
+  format = artwork_get_group_persistentid(persistentid, max_w, max_h, evbuf);
+  if (format <= 0)
+    {
+      DPRINTF(E_DBG, L_ART, "No artwork found for group %d\n", id);
+      return -1;
+    }
+
+  return format;
+}
+
+/* Checks if the file is an artwork file */
+int
+artwork_file_is_artwork(const char *filename)
+{
+  cfg_t *lib;
+  int n;
+  int i;
+  int j;
+  int ret;
+  char artwork[PATH_MAX];
+
+  lib = cfg_getsec(cfg, "library");
+  n = cfg_size(lib, "artwork_basenames");
+
+  for (i = 0; i < n; i++)
+    {
+      for (j = 0; j < (sizeof(cover_extension) / sizeof(cover_extension[0])); j++)
+	{
+	  ret = snprintf(artwork, sizeof(artwork), "%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
+	  if ((ret < 0) || (ret >= sizeof(artwork)))
+	    {
+	      DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX (%s.%s)\n", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]);
+	      continue;
+	    }
+
+	  if (strcmp(artwork, filename) == 0)
+	    return 1;
+	}
+
+      if (j < (sizeof(cover_extension) / sizeof(cover_extension[0])))
+	break;
+    }
+
+  return 0;
 }
diff --git a/src/artwork.h b/src/artwork.h
index a230464..b6b00a5 100644
--- a/src/artwork.h
+++ b/src/artwork.h
@@ -2,9 +2,6 @@
 #ifndef __ARTWORK_H__
 #define __ARTWORK_H__
 
-#define ART_CAN_PNG     (1 << 0)
-#define ART_CAN_JPEG    (1 << 1)
-
 #define ART_FMT_PNG     1
 #define ART_FMT_JPEG    2
 
@@ -16,10 +13,14 @@
 
 /* Get artwork for individual track */
 int
-artwork_get_item(int id, int max_w, int max_h, int format, struct evbuffer *evbuf);
+artwork_get_item(int id, int max_w, int max_h, 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);
+artwork_get_group(int id, int max_w, int max_h, struct evbuffer *evbuf);
+
+/* Checks if the file is an artwork file */
+int
+artwork_file_is_artwork(const char *filename);
 
 #endif /* !__ARTWORK_H__ */
diff --git a/src/cache.c b/src/cache.c
new file mode 100644
index 0000000..f9a6759
--- /dev/null
+++ b/src/cache.c
@@ -0,0 +1,1702 @@
+/*
+ * 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 "cache.h"
+
+
+#define CACHE_VERSION 2
+
+struct cache_command;
+
+typedef int (*cmd_func)(struct cache_command *cmd);
+
+struct cache_command
+{
+  pthread_mutex_t lck;
+  pthread_cond_t cond;
+
+  cmd_func func;
+
+  int nonblock;
+
+  struct {
+    char *query; // daap query
+    char *ua;    // user agent
+    int msec;
+
+    char *path;  // artwork path
+    int type;    // individual or group artwork
+    int64_t peristentid;
+    int max_w;
+    int max_h;
+    int format;
+    time_t mtime;
+    int cached;
+
+    struct evbuffer *evbuf;
+  } arg;
+
+  int ret;
+};
+
+/* --- Globals --- */
+// cache thread
+static pthread_t tid_cache;
+
+// Event base, pipes and events
+struct event_base *evbase_cache;
+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 cache
+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 cache_command *cmd)
+{
+  memset(cmd, 0, sizeof(struct cache_command));
+
+  pthread_mutex_init(&cmd->lck, NULL);
+  pthread_cond_init(&cmd->cond, NULL);
+}
+
+static void
+command_deinit(struct cache_command *cmd)
+{
+  pthread_cond_destroy(&cmd->cond);
+  pthread_mutex_destroy(&cmd->lck);
+}
+
+static int
+send_command(struct cache_command *cmd)
+{
+  int ret;
+
+  if (!cmd->func)
+    {
+      DPRINTF(E_LOG, L_CACHE, "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_CACHE, "Could not send command: %s\n", strerror(errno));
+      return -1;
+    }
+
+  return 0;
+}
+
+static int
+sync_command(struct cache_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 cache_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_CACHE, "Killing cache thread\n");
+
+  if (write(g_exit_pipe[1], &dummy, sizeof(dummy)) != sizeof(dummy))
+    DPRINTF(E_LOG, L_CACHE, "Could not write to exit fd: %s\n", strerror(errno));
+}
+
+
+/* --------------------------------- MAIN --------------------------------- */
+/*                              Thread: cache                              */
+
+static int
+cache_create_tables(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);"
+#define T_ARTWORK					\
+  "CREATE TABLE IF NOT EXISTS artwork ("		\
+  "   id                  INTEGER PRIMARY KEY NOT NULL,"\
+  "   type                INTEGER NOT NULL DEFAULT 0,"  \
+  "   persistentid        INTEGER NOT NULL,"		\
+  "   max_w               INTEGER NOT NULL,"		\
+  "   max_h               INTEGER NOT NULL,"		\
+  "   format              INTEGER NOT NULL,"		\
+  "   filepath            VARCHAR(4096) NOT NULL,"	\
+  "   db_timestamp        INTEGER DEFAULT 0,"		\
+  "   data                BLOB"				\
+  ");"
+#define I_ARTWORK_ID				\
+  "CREATE INDEX IF NOT EXISTS idx_persistentidwh ON artwork(type, persistentid, max_w, max_h);"
+#define I_ARTWORK_PATH				\
+  "CREATE INDEX IF NOT EXISTS idx_pathtime ON artwork(filepath, db_timestamp);"
+#define T_ADMIN_CACHE	\
+  "CREATE TABLE IF NOT EXISTS admin_cache("	\
+  " key VARCHAR(32) PRIMARY KEY NOT NULL,"	\
+  " value VARCHAR(32) NOT NULL"	\
+  ");"
+#define Q_CACHE_VERSION	\
+  "INSERT INTO admin_cache (key, value) VALUES ('cache_version', '%d');"
+
+  char *query;
+  char *errmsg;
+  int ret;
+
+
+  // Create reply cache table
+  ret = sqlite3_exec(g_db_hdl, T_REPLIES, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error creating cache table 'replies': %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_CACHE, "Error creating cache table 'queries': %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_CACHE, "Error creating index on replies(query): %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Create artwork table
+  ret = sqlite3_exec(g_db_hdl, T_ARTWORK, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error creating cache table 'artwork': %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Create index
+  ret = sqlite3_exec(g_db_hdl, I_ARTWORK_ID, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error creating index on artwork(type, persistentid, max_w, max_h): %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+  ret = sqlite3_exec(g_db_hdl, I_ARTWORK_PATH, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error creating index on artwork(filepath, db_timestamp): %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Create admin cache table
+  ret = sqlite3_exec(g_db_hdl, T_ADMIN_CACHE, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error creating cache table 'admin_cache': %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+  query = sqlite3_mprintf(Q_CACHE_VERSION, CACHE_VERSION);
+  ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error inserting cache version: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  sqlite3_free(query);
+
+  DPRINTF(E_DBG, L_CACHE, "Cache tables created\n");
+
+  return 0;
+#undef T_REPLIES
+#undef T_QUERIES
+#undef I_QUERY
+#undef T_ARTWORK
+#undef I_ARTWORK_ID
+#undef I_ARTWORK_PATH
+#undef T_ADMIN_CACHE
+#undef Q_CACHE_VERSION
+}
+
+static int
+cache_drop_tables(void)
+{
+#define D_REPLIES	"DROP TABLE IF EXISTS replies;"
+#define D_QUERIES	"DROP TABLE IF EXISTS queries;"
+#define D_QUERY		"DROP INDEX IF EXISTS idx_query;"
+#define D_ARTWORK	"DROP TABLE IF EXISTS artwork;"
+#define D_ARTWORK_ID	"DROP INDEX IF EXISTS idx_persistentidwh;"
+#define D_ARTWORK_PATH	"DROP INDEX IF EXISTS idx_pathtime;"
+#define D_ADMIN_CACHE	"DROP TABLE IF EXISTS admin_cache;"
+#define Q_VACUUM	"VACUUM;"
+
+  char *errmsg;
+  int ret;
+
+
+  // Drop reply cache table
+  ret = sqlite3_exec(g_db_hdl, D_REPLIES, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error dropping reply cache table: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Drop query table
+  ret = sqlite3_exec(g_db_hdl, D_QUERIES, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error dropping query table: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Drop index
+  ret = sqlite3_exec(g_db_hdl, D_QUERY, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error dropping query index: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Drop artwork table
+  ret = sqlite3_exec(g_db_hdl, D_ARTWORK, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error dropping artwork table: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Drop index
+  ret = sqlite3_exec(g_db_hdl, D_ARTWORK_ID, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error dropping artwork id index: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+  ret = sqlite3_exec(g_db_hdl, D_ARTWORK_PATH, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error dropping artwork path index: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Drop admin cache table
+  ret = sqlite3_exec(g_db_hdl, D_ADMIN_CACHE, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Error dropping admin cache table: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Vacuum
+  ret = sqlite3_exec(g_db_hdl, Q_VACUUM, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Error vacuuming cache database: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  DPRINTF(E_DBG, L_CACHE, "Cache tables dropped\n");
+
+  return 0;
+#undef D_REPLIES
+#undef D_QUERIES
+#undef D_QUERY
+#undef D_ARTWORK
+#undef D_ARTWORK_ID
+#undef D_ARTWORK_PATH
+#undef D_ADMIN_CACHE
+#undef Q_VACUUM
+}
+
+/*
+ * Compares the CACHE_VERSION against the version stored in the cache admin table.
+ * Drops the tables and indexes if the versions are different.
+ *
+ * @return 0 if versions are equal, 1 if versions are different or the admin table does not exist, -1 if an error occurred
+ */
+static int
+cache_check_version(void)
+{
+#define Q_VER "SELECT value FROM admin_cache WHERE key = 'cache_version';"
+  sqlite3_stmt *stmt;
+  int cur_ver;
+  int ret;
+
+  DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", Q_VER);
+
+  ret = sqlite3_prepare_v2(g_db_hdl, Q_VER, strlen(Q_VER) + 1, &stmt, NULL);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_WARN, L_CACHE, "Could not prepare statement: %s\n", sqlite3_errmsg(g_db_hdl));
+      return 1;
+    }
+
+  ret = sqlite3_step(stmt);
+  if (ret != SQLITE_ROW)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not step: %s\n", sqlite3_errmsg(g_db_hdl));
+      sqlite3_finalize(stmt);
+      return -1;
+    }
+
+  cur_ver = sqlite3_column_int(stmt, 0);
+  sqlite3_finalize(stmt);
+
+  if (cur_ver != CACHE_VERSION)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Database schema outdated, deleting cache v%d -> v%d\n", cur_ver, CACHE_VERSION);
+      ret = cache_drop_tables();
+      if (ret < 0)
+	{
+	  DPRINTF(E_LOG, L_CACHE, "Error deleting database tables\n");
+	  return -1;
+	}
+      return 1;
+    }
+  return 0;
+#undef Q_VER
+}
+
+static int
+cache_create(void)
+{
+#define Q_PRAGMA_CACHE_SIZE "PRAGMA cache_size=%d;"
+#define Q_PRAGMA_JOURNAL_MODE "PRAGMA journal_mode=%s;"
+#define Q_PRAGMA_SYNCHRONOUS "PRAGMA synchronous=%d;"
+  char *errmsg;
+  int ret;
+  int cache_size;
+  char *journal_mode;
+  int synchronous;
+  char *query;
+
+  // Open db
+  ret = sqlite3_open(g_db_path, &g_db_hdl);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not open cache database: %s\n", sqlite3_errmsg(g_db_hdl));
+
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+
+  // Check cache version
+  ret = cache_check_version();
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not check cache database version\n");
+
+      sqlite3_close(g_db_hdl);
+      return -1;
+    }
+  else if (ret > 0)
+    {
+      ret = cache_create_tables();
+      if (ret < 0)
+	{
+	  DPRINTF(E_LOG, L_CACHE, "Could not create cache database tables\n");
+
+	  sqlite3_close(g_db_hdl);
+	  return -1;
+	}
+    }
+
+  // Set page cache size in number of pages
+  cache_size = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_cache_size_cache");
+  if (cache_size > -1)
+    {
+      query = sqlite3_mprintf(Q_PRAGMA_CACHE_SIZE, cache_size);
+      ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg);
+      if (ret != SQLITE_OK)
+	{
+	  DPRINTF(E_LOG, L_CACHE, "Error setting pragma_cache_size_cache: %s\n", errmsg);
+
+	  sqlite3_free(errmsg);
+	  sqlite3_close(g_db_hdl);
+	  return -1;
+	}
+    }
+
+  // Set journal mode
+  journal_mode = cfg_getstr(cfg_getsec(cfg, "sqlite"), "pragma_journal_mode");
+  if (journal_mode)
+    {
+      query = sqlite3_mprintf(Q_PRAGMA_JOURNAL_MODE, journal_mode);
+      ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg);
+      if (ret != SQLITE_OK)
+	{
+	  DPRINTF(E_LOG, L_CACHE, "Error setting pragma_journal_mode: %s\n", errmsg);
+
+	  sqlite3_free(errmsg);
+	  sqlite3_close(g_db_hdl);
+	  return -1;
+	}
+    }
+
+  // Set synchronous flag
+  synchronous = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_synchronous");
+  if (synchronous > -1)
+    {
+      query = sqlite3_mprintf(Q_PRAGMA_SYNCHRONOUS, synchronous);
+      ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg);
+      if (ret != SQLITE_OK)
+	{
+	  DPRINTF(E_LOG, L_CACHE, "Error setting pragma_synchronous: %s\n", errmsg);
+
+	  sqlite3_free(errmsg);
+	  sqlite3_close(g_db_hdl);
+	  return -1;
+	}
+    }
+
+  DPRINTF(E_DBG, L_CACHE, "Cache created\n");
+
+  return 0;
+#undef Q_PRAGMA_CACHE_SIZE
+#undef Q_PRAGMA_JOURNAL_MODE
+#undef Q_PRAGMA_SYNCHRONOUS
+}
+
+static void
+cache_close(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);
+
+  DPRINTF(E_DBG, L_CACHE, "Cache closed\n");
+}
+
+/* Adds the reply (stored in evbuf) to the cache */
+static int
+cache_daap_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 datalen;
+  int ret;
+
+#ifdef HAVE_LIBEVENT2
+  datalen = evbuffer_get_length(evbuf);
+  data = evbuffer_pullup(evbuf, -1);
+#else
+  datalen = 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_CACHE, "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, datalen, SQLITE_STATIC);
+
+  ret = sqlite3_step(stmt);
+  if (ret != SQLITE_DONE)
+    {
+      DPRINTF(E_LOG, L_CACHE, "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_CACHE, "Error finalizing query for cache update: %s\n", sqlite3_errmsg(g_db_hdl));
+      return -1;
+    }
+
+  //DPRINTF(E_DBG, L_CACHE, "Wrote cache reply, size %d\n", datalen);
+
+  return 0;
+#undef Q_TMPL
+}
+
+/* Adds the query to the list of queries for which we will build and cache a reply */
+static int
+cache_daap_query_add(struct cache_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_CACHE, "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_CACHE, "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_CACHE, "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_CACHE, "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_CACHE, "Error cleaning up query list before update: %s\n", errmsg);
+      sqlite3_free(errmsg);
+      return -1;
+    }
+
+  cache_daap_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
+cache_daap_query_get(struct cache_command *cmd)
+{
+#define Q_TMPL "SELECT reply FROM replies WHERE query = ?;"
+  sqlite3_stmt *stmt;
+  char *query;
+  int datalen;
+  int ret;
+
+  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_CACHE, "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_CACHE, "Error stepping query for cache update: %s\n", sqlite3_errmsg(g_db_hdl));
+      goto error_get;
+    }
+
+  datalen = sqlite3_column_bytes(stmt, 0);
+
+  if (!cmd->arg.evbuf)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Error: DAAP reply evbuffer is NULL\n");
+      goto error_get;
+    }
+
+  ret = evbuffer_add(cmd->arg.evbuf, sqlite3_column_blob(stmt, 0), datalen);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Out of memory for DAAP reply evbuffer\n");
+      goto error_get;
+    }
+
+  ret = sqlite3_finalize(stmt);
+  if (ret != SQLITE_OK)
+    DPRINTF(E_LOG, L_CACHE, "Error finalizing query for getting cache: %s\n", sqlite3_errmsg(g_db_hdl));
+
+  DPRINTF(E_INFO, L_CACHE, "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
+cache_daap_update_cb(int fd, short what, void *arg)
+{
+  sqlite3_stmt *stmt;
+  struct evbuffer *evbuf;
+  char *errmsg;
+  char *query;
+  int ret;
+
+  DPRINTF(E_INFO, L_CACHE, "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_CACHE, "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_CACHE, "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_CACHE, "Error building DAAP reply for query: %s\n", query);
+	  free(query);
+	  continue;
+	}
+
+      cache_daap_reply_add(query, evbuf);
+
+      free(query);
+      evbuffer_free(evbuf);
+    }
+
+  if (ret != SQLITE_DONE)
+    DPRINTF(E_LOG, L_CACHE, "Could not step: %s\n", sqlite3_errmsg(g_db_hdl));
+
+  sqlite3_finalize(stmt);
+
+  DPRINTF(E_INFO, L_CACHE, "DAAP cache updated\n");
+}
+
+/* This function will just set a timer, which when it times out will trigger
+ * the actual cache update. The purpose is to avoid avoid cache updates when
+ * the database is busy, eg during a library scan.
+ */
+static int
+cache_daap_update_timer(struct cache_command *cmd)
+{
+  if (!g_cacheev)
+    return -1;
+
+  evtimer_add(g_cacheev, &g_wait);
+
+  return 0;
+}
+
+
+/*
+ * Updates cached timestamps to current time for all cache entries for the given path, if the file was not modfied
+ * after the cached timestamp. All cache entries for the given path are deleted, if the file was
+ * modified after the cached timestamp.
+ *
+ * @param cmd->arg.path the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork)
+ * @param cmd->arg.mtime modified timestamp of the artwork file
+ * @return 0 if successful, -1 if an error occurred
+ */
+static int
+cache_artwork_ping_impl(struct cache_command *cmd)
+{
+#define Q_TMPL_PING "UPDATE artwork SET db_timestamp = %" PRIi64 " WHERE filepath = '%q' AND db_timestamp >= %" PRIi64 ";"
+#define Q_TMPL_DEL "DELETE FROM artwork WHERE filepath = '%q' AND db_timestamp < %" PRIi64 ";"
+
+  char *query;
+  char *errmsg;
+  int ret;
+
+  query = sqlite3_mprintf(Q_TMPL_PING, (int64_t)time(NULL), cmd->arg.path, (int64_t)cmd->arg.mtime);
+
+  DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", query);
+
+  ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Query error: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_free(query);
+      return -1;
+    }
+
+  sqlite3_free(query);
+
+  query = sqlite3_mprintf(Q_TMPL_DEL, cmd->arg.path, (int64_t)cmd->arg.mtime);
+
+  DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", query);
+
+  ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Query error: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_free(query);
+      return -1;
+    }
+
+  sqlite3_free(query);
+
+  return 0;
+
+#undef Q_TMPL_PING
+#undef Q_TMPL_DEL
+}
+
+/*
+ * Removes all cache entries for the given path
+ *
+ * @param cmd->arg.path the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork)
+ * @return 0 if successful, -1 if an error occurred
+ */
+static int
+cache_artwork_delete_by_path_impl(struct cache_command *cmd)
+{
+#define Q_TMPL_DEL "DELETE FROM artwork WHERE filepath = '%q';"
+
+  char *query;
+  char *errmsg;
+  int ret;
+
+  query = sqlite3_mprintf(Q_TMPL_DEL, cmd->arg.path);
+
+  DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", query);
+
+  ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Query error: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_free(query);
+      return -1;
+    }
+
+  sqlite3_free(query);
+
+  return 0;
+
+#undef Q_TMPL_DEL
+}
+
+/*
+ * Removes all cache entries with cached timestamp older than the given reference timestamp
+ *
+ * @param cmd->arg.mtime reference timestamp
+ * @return 0 if successful, -1 if an error occurred
+ */
+static int
+cache_artwork_purge_cruft_impl(struct cache_command *cmd)
+{
+#define Q_TMPL "DELETE FROM artwork WHERE db_timestamp < %" PRIi64 ";"
+
+  char *query;
+  char *errmsg;
+  int ret;
+
+  query = sqlite3_mprintf(Q_TMPL, (int64_t)cmd->arg.mtime);
+
+  DPRINTF(E_DBG, L_CACHE, "Running purge query '%s'\n", query);
+
+  ret = sqlite3_exec(g_db_hdl, query, NULL, NULL, &errmsg);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Query error: %s\n", errmsg);
+
+      sqlite3_free(errmsg);
+      sqlite3_free(query);
+      return -1;
+    }
+
+  DPRINTF(E_DBG, L_CACHE, "Purged %d rows\n", sqlite3_changes(g_db_hdl));
+
+  sqlite3_free(query);
+
+  return 0;
+
+#undef Q_TMPL
+}
+
+/*
+ * Adds the given (scaled) artwork image to the artwork cache
+ *
+ * @param cmd->arg.persistentid persistent songalbumid or songartistid
+ * @param cmd->arg.max_w maximum image width
+ * @param cmd->arg.max_h maximum image height
+ * @param cmd->arg.format ART_FMT_PNG for png, ART_FMT_JPEG for jpeg or 0 if no artwork available
+ * @param cmd->arg.filename the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork) or empty if no artwork available
+ * @param cmd->arg.evbuf event buffer containing the (scaled) image
+ * @return 0 if successful, -1 if an error occurred
+ */
+static int
+cache_artwork_add_impl(struct cache_command *cmd)
+{
+  sqlite3_stmt *stmt;
+  char *query;
+  uint8_t *data;
+  int datalen;
+  int ret;
+
+  query = "INSERT INTO artwork (id, persistentid, max_w, max_h, format, filepath, db_timestamp, data, type) VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?);";
+
+  ret = sqlite3_prepare_v2(g_db_hdl, query, -1, &stmt, 0);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not prepare statement: %s\n", sqlite3_errmsg(g_db_hdl));
+      return -1;
+    }
+
+#ifdef HAVE_LIBEVENT2
+  datalen = evbuffer_get_length(cmd->arg.evbuf);
+  data = evbuffer_pullup(cmd->arg.evbuf, -1);
+#else
+  datalen = EVBUFFER_LENGTH(cmd->arg.evbuf);
+  data = EVBUFFER_DATA(cmd->arg.evbuf);
+#endif
+
+  sqlite3_bind_int64(stmt, 1, cmd->arg.peristentid);
+  sqlite3_bind_int(stmt, 2, cmd->arg.max_w);
+  sqlite3_bind_int(stmt, 3, cmd->arg.max_h);
+  sqlite3_bind_int(stmt, 4, cmd->arg.format);
+  sqlite3_bind_text(stmt, 5, cmd->arg.path, -1, SQLITE_STATIC);
+  sqlite3_bind_int(stmt, 6, (uint64_t)time(NULL));
+  sqlite3_bind_blob(stmt, 7, data, datalen, SQLITE_STATIC);
+  sqlite3_bind_int(stmt, 8, cmd->arg.type);
+
+  ret = sqlite3_step(stmt);
+  if (ret != SQLITE_DONE)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Error stepping query for artwork add: %s\n", sqlite3_errmsg(g_db_hdl));
+      sqlite3_finalize(stmt);
+      return -1;
+    }
+
+  ret = sqlite3_finalize(stmt);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Error finalizing query for artwork add: %s\n", sqlite3_errmsg(g_db_hdl));
+      return -1;
+    }
+
+  return 0;
+}
+
+/*
+ * Get the cached artwork image for the given persistentid and maximum width/height
+ *
+ * If there is a cached entry for the given id and width/height, the parameter cached is set to 1.
+ * In this case format and data contain the cached values.
+ *
+ * @param cmd->arg.type individual or group artwork
+ * @param cmd->arg.persistentid persistent itemid, songalbumid or songartistid
+ * @param cmd->arg.max_w maximum image width
+ * @param cmd->arg.max_h maximum image height
+ * @param cmd->arg.cached set by this function to 0 if no cache entry exists, otherwise 1
+ * @param cmd->arg.format set by this function to the format of the cache entry
+ * @param cmd->arg.evbuf event buffer filled by this function with the scaled image
+ * @return 0 if successful, -1 if an error occurred
+ */
+static int
+cache_artwork_get_impl(struct cache_command *cmd)
+{
+#define Q_TMPL "SELECT a.format, a.data FROM artwork a WHERE a.type = %d AND a.persistentid = %" PRIi64 " AND a.max_w = %d AND a.max_h = %d;"
+  sqlite3_stmt *stmt;
+  char *query;
+  int datalen;
+  int ret;
+
+  query = sqlite3_mprintf(Q_TMPL, cmd->arg.type, cmd->arg.peristentid, cmd->arg.max_w, cmd->arg.max_h);
+  if (!query)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Out of memory for query string\n");
+      return -1;
+    }
+
+  DPRINTF(E_DBG, L_CACHE, "Running query '%s'\n", query);
+  ret = sqlite3_prepare_v2(g_db_hdl, query, -1, &stmt, 0);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not prepare statement: %s\n", sqlite3_errmsg(g_db_hdl));
+      ret = -1;
+      goto error_get;
+    }
+
+  ret = sqlite3_step(stmt);
+  if (ret != SQLITE_ROW)
+    {
+      cmd->arg.cached = 0;
+
+      if (ret == SQLITE_DONE)
+	{
+	  ret = 0;
+	  DPRINTF(E_DBG, L_CACHE, "No results\n");
+	}
+      else
+	{
+	  ret = -1;
+	  DPRINTF(E_LOG, L_CACHE, "Could not step: %s\n", sqlite3_errmsg(g_db_hdl));
+	}
+
+      goto error_get;
+    }
+
+  cmd->arg.format = sqlite3_column_int(stmt, 0);
+  datalen = sqlite3_column_bytes(stmt, 1);
+  if (!cmd->arg.evbuf)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Error: Artwork evbuffer is NULL\n");
+      goto error_get;
+    }
+
+  ret = evbuffer_add(cmd->arg.evbuf, sqlite3_column_blob(stmt, 1), datalen);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Out of memory for artwork evbuffer\n");
+      goto error_get;
+    }
+
+  cmd->arg.cached = 1;
+
+  ret = sqlite3_finalize(stmt);
+  if (ret != SQLITE_OK)
+    DPRINTF(E_LOG, L_CACHE, "Error finalizing query for getting cache: %s\n", sqlite3_errmsg(g_db_hdl));
+
+  DPRINTF(E_DBG, L_CACHE, "Cache hit: %s\n", query);
+
+  return 0;
+
+ error_get:
+  sqlite3_finalize(stmt);
+  return -1;
+#undef Q_TMPL
+}
+
+
+static void *
+cache(void *arg)
+{
+  int ret;
+
+  ret = cache_create();
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Error: Cache create failed. Cache will be disabled.\n");
+      pthread_exit(NULL);
+    }
+
+  /* The thread needs a connection with the main db, so it can generate DAAP
+   * replies through httpd_daap.c
+   */
+  ret = db_perthread_init();
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Error: DB init failed. Cache will be disabled.\n");
+      cache_close();
+
+      pthread_exit(NULL);
+    }
+
+  g_initialized = 1;
+
+  event_base_dispatch(evbase_cache);
+
+  if (g_initialized)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Cache event loop terminated ahead of time!\n");
+      g_initialized = 0;
+    }
+
+  db_perthread_deinit();
+
+  cache_close();
+
+  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_CACHE, "Error reading from exit pipe\n");
+
+  event_base_loopbreak(evbase_cache);
+
+  g_initialized = 0;
+
+  event_add(g_exitev, NULL);
+}
+
+static void
+command_cb(int fd, short what, void *arg)
+{
+  struct cache_command *cmd;
+  int ret;
+
+  ret = read(g_cmd_pipe[0], &cmd, sizeof(cmd));
+  if (ret != sizeof(cmd))
+    {
+      DPRINTF(E_LOG, L_CACHE, "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);
+}
+
+
+
+/* ---------------------------- DAAP cache API  --------------------------- */
+
+/* The DAAP cache will cache raw daap replies for queries added with
+ * cache_daap_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.
+ *
+ */
+
+void
+cache_daap_trigger(void)
+{
+  struct cache_command *cmd;
+
+  if (!g_initialized)
+    return;
+
+  cmd = (struct cache_command *)malloc(sizeof(struct cache_command));
+  if (!cmd)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not allocate cache_command\n");
+      return;
+    }
+
+  memset(cmd, 0, sizeof(struct cache_command));
+
+  cmd->nonblock = 1;
+
+  cmd->func = cache_daap_update_timer;
+
+  nonblock_command(cmd);
+}
+
+int
+cache_daap_get(const char *query, struct evbuffer *evbuf)
+{
+  struct cache_command cmd;
+  int ret;
+
+  if (!g_initialized)
+    return -1;
+
+  command_init(&cmd);
+
+  cmd.func = cache_daap_query_get;
+  cmd.arg.query = strdup(query);
+  cmd.arg.evbuf = evbuf;
+
+  ret = sync_command(&cmd);
+
+  command_deinit(&cmd);
+
+  return ret;
+}
+
+void
+cache_daap_add(const char *query, const char *ua, int msec)
+{
+  struct cache_command *cmd;
+
+  if (!g_initialized)
+    return;
+
+  cmd = (struct cache_command *)malloc(sizeof(struct cache_command));
+  if (!cmd)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not allocate cache_command\n");
+      return;
+    }
+
+  memset(cmd, 0, sizeof(struct cache_command));
+
+  cmd->nonblock = 1;
+
+  cmd->func = cache_daap_query_add;
+  cmd->arg.query = strdup(query);
+  cmd->arg.ua = strdup(ua);
+  cmd->arg.msec = msec;
+
+  nonblock_command(cmd);
+}
+
+int
+cache_daap_threshold(void)
+{
+  return g_cfg_threshold;
+}
+
+
+/* --------------------------- Artwork cache API -------------------------- */
+
+/*
+ * Updates cached timestamps to current time for all cache entries for the given path, if the file was not modfied
+ * after the cached timestamp.
+ *
+ * If the parameter "del" is greater than 0, all cache entries for the given path are deleted, if the file was
+ * modified after the cached timestamp.
+ *
+ * @param path the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork)
+ * @param mtime modified timestamp of the artwork file
+ * @param del if > 0 cached entries for the given path are deleted if the cached timestamp (db_timestamp) is older than mtime
+ * @return 0 if successful, -1 if an error occurred
+ */
+int
+cache_artwork_ping(char *path, time_t mtime)
+{
+  struct cache_command cmd;
+  int ret;
+
+  if (!g_initialized)
+    return -1;
+
+  command_init(&cmd);
+
+  cmd.func = cache_artwork_ping_impl;
+  cmd.arg.path = strdup(path);
+  cmd.arg.mtime = mtime;
+
+  ret = sync_command(&cmd);
+
+  command_deinit(&cmd);
+
+  return ret;
+}
+
+/*
+ * Removes all cache entries for the given path
+ *
+ * @param path the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork)
+ * @return 0 if successful, -1 if an error occurred
+ */
+int
+cache_artwork_delete_by_path(char *path)
+{
+  struct cache_command cmd;
+  int ret;
+
+  if (!g_initialized)
+    return -1;
+
+  command_init(&cmd);
+
+  cmd.func = cache_artwork_delete_by_path_impl;
+  cmd.arg.path = strdup(path);
+
+  ret = sync_command(&cmd);
+
+  command_deinit(&cmd);
+
+  return ret;
+}
+
+/*
+ * Removes all cache entries with cached timestamp older than the given reference timestamp
+ *
+ * @param ref reference timestamp
+ * @return 0 if successful, -1 if an error occurred
+ */
+int
+cache_artwork_purge_cruft(time_t ref)
+{
+  struct cache_command cmd;
+  int ret;
+
+  if (!g_initialized)
+    return -1;
+
+  command_init(&cmd);
+
+  cmd.func = cache_artwork_purge_cruft_impl;
+  cmd.arg.mtime = ref;
+
+  ret = sync_command(&cmd);
+
+  command_deinit(&cmd);
+
+  return ret;
+}
+
+/*
+ * Adds the given (scaled) artwork image to the artwork cache
+ *
+ * @param type individual or group artwork
+ * @param persistentid persistent itemid, songalbumid or songartistid
+ * @param max_w maximum image width
+ * @param max_h maximum image height
+ * @param format ART_FMT_PNG for png, ART_FMT_JPEG for jpeg or 0 if no artwork available
+ * @param filename the full path to the artwork file (could be an jpg/png image or a media file with embedded artwork) or empty if no artwork available
+ * @param evbuf event buffer containing the (scaled) image
+ * @return 0 if successful, -1 if an error occurred
+ */
+int
+cache_artwork_add(int type, int64_t persistentid, int max_w, int max_h, int format, char *filename, struct evbuffer *evbuf)
+{
+  struct cache_command cmd;
+  int ret;
+
+  if (!g_initialized)
+    return -1;
+
+  command_init(&cmd);
+
+  cmd.func = cache_artwork_add_impl;
+  cmd.arg.type = type;
+  cmd.arg.peristentid = persistentid;
+  cmd.arg.max_w = max_w;
+  cmd.arg.max_h = max_h;
+  cmd.arg.format = format;
+  cmd.arg.path = strdup(filename);
+  cmd.arg.evbuf = evbuf;
+
+  ret = sync_command(&cmd);
+
+  command_deinit(&cmd);
+
+  return ret;
+}
+
+/*
+ * Get the cached artwork image for the given persistentid and maximum width/height
+ *
+ * If there is a cached entry for the given id and width/height, the parameter cached is set to 1.
+ * In this case format and data contain the cached values.
+ *
+ * @param persistentid persistent songalbumid or songartistid
+ * @param max_w maximum image width
+ * @param max_h maximum image height
+ * @param cached set by this function to 0 if no cache entry exists, otherwise 1
+ * @param format set by this function to the format of the cache entry
+ * @param evbuf event buffer filled by this function with the scaled image
+ * @return 0 if successful, -1 if an error occurred
+ */
+int
+cache_artwork_get(int type, int64_t persistentid, int max_w, int max_h, int *cached, int *format, struct evbuffer *evbuf)
+{
+  struct cache_command cmd;
+  int ret;
+
+  if (!g_initialized)
+    return -1;
+
+  command_init(&cmd);
+
+  cmd.func = cache_artwork_get_impl;
+  cmd.arg.type = type;
+  cmd.arg.peristentid = persistentid;
+  cmd.arg.max_w = max_w;
+  cmd.arg.max_h = max_h;
+  cmd.arg.evbuf = evbuf;
+
+  ret = sync_command(&cmd);
+
+  *format = cmd.arg.format;
+  *cached = cmd.arg.cached;
+
+  command_deinit(&cmd);
+
+  return ret;
+}
+
+
+
+/* -------------------------- Cache general API --------------------------- */
+
+int
+cache_init(void)
+{
+  int ret;
+
+  g_initialized = 0;
+
+  g_db_path = cfg_getstr(cfg_getsec(cfg, "general"), "cache_path");
+  if (!g_db_path || (strlen(g_db_path) == 0))
+    {
+      DPRINTF(E_LOG, L_CACHE, "Cache path invalid, disabling cache\n");
+      return 0;
+    }
+
+  g_cfg_threshold = cfg_getint(cfg_getsec(cfg, "general"), "cache_daap_threshold");
+  if (g_cfg_threshold == 0)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Cache threshold set to 0, disabling cache\n");
+      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_CACHE, "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_CACHE, "Could not create command pipe: %s\n", strerror(errno));
+      goto cmd_fail;
+    }
+
+  evbase_cache = event_base_new();
+  if (!evbase_cache)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not create an event base\n");
+      goto evbase_fail;
+    }
+
+#ifdef HAVE_LIBEVENT2
+  g_exitev = event_new(evbase_cache, g_exit_pipe[0], EV_READ, exit_cb, NULL);
+  if (!g_exitev)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not create exit event\n");
+      goto evnew_fail;
+    }
+
+  g_cmdev = event_new(evbase_cache, g_cmd_pipe[0], EV_READ, command_cb, NULL);
+  if (!g_cmdev)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not create cmd event\n");
+      goto evnew_fail;
+    }
+
+  g_cacheev = evtimer_new(evbase_cache, cache_daap_update_cb, NULL);
+  if (!g_cmdev)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not create cache event\n");
+      goto evnew_fail;
+    }
+#else
+  g_exitev = (struct event *)malloc(sizeof(struct event));
+  if (!g_exitev)
+    {
+      DPRINTF(E_LOG, L_CACHE, "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_cache, g_exitev);
+
+  g_cmdev = (struct event *)malloc(sizeof(struct event));
+  if (!g_cmdev)
+    {
+      DPRINTF(E_LOG, L_CACHE, "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_cache, g_cmdev);
+
+  g_cacheev = (struct event *)malloc(sizeof(struct event));
+  if (!g_cacheev)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not create cache event\n");
+      goto evnew_fail;
+    }
+  event_set(g_cacheev, -1, EV_TIMEOUT, cache_daap_update_cb, NULL);
+  event_base_set(evbase_cache, g_cacheev);
+#endif
+
+  event_add(g_exitev, NULL);
+  event_add(g_cmdev, NULL);
+
+  DPRINTF(E_INFO, L_CACHE, "cache thread init\n");
+
+  ret = pthread_create(&tid_cache, NULL, cache, NULL);
+  if (ret < 0)
+    {
+      DPRINTF(E_LOG, L_CACHE, "Could not spawn cache thread: %s\n", strerror(errno));
+
+      goto thread_fail;
+    }
+
+  return 0;
+  
+ thread_fail:
+ evnew_fail:
+  event_base_free(evbase_cache);
+  evbase_cache = 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
+cache_deinit(void)
+{
+  int ret;
+
+  if (!g_initialized)
+    return;
+
+  thread_exit();
+
+  ret = pthread_join(tid_cache, NULL);
+  if (ret != 0)
+    {
+      DPRINTF(E_FATAL, L_CACHE, "Could not join cache thread: %s\n", strerror(errno));
+      return;
+    }
+
+  // Free event base (should free events too)
+  event_base_free(evbase_cache);
+
+  // 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/cache.h b/src/cache.h
new file mode 100644
index 0000000..eded991
--- /dev/null
+++ b/src/cache.h
@@ -0,0 +1,57 @@
+
+#ifndef __CACHE_H__
+#define __CACHE_H__
+
+#ifdef HAVE_LIBEVENT2
+# include <event2/event.h>
+# include <event2/buffer.h>
+#else
+# include <event.h>
+#endif
+
+
+/* ---------------------------- DAAP cache API  --------------------------- */
+
+void
+cache_daap_trigger(void);
+
+int
+cache_daap_get(const char *query, struct evbuffer *evbuf);
+
+void
+cache_daap_add(const char *query, const char *ua, int msec);
+
+int
+cache_daap_threshold(void);
+
+
+/* ---------------------------- Artwork cache API  --------------------------- */
+
+#define CACHE_ARTWORK_GROUP 0
+#define CACHE_ARTWORK_INDIVIDUAL 1
+
+int
+cache_artwork_ping(char *path, time_t mtime);
+
+int
+cache_artwork_delete_by_path(char *path);
+
+int
+cache_artwork_purge_cruft(time_t ref);
+
+int
+cache_artwork_add(int type, int64_t persistentid, int max_w, int max_h, int format, char *filename, struct evbuffer *evbuf);
+
+int
+cache_artwork_get(int type, int64_t persistentid, int max_w, int max_h, int *cached, int *format, struct evbuffer *evbuf);
+
+
+/* ---------------------------- Cache API  --------------------------- */
+
+int
+cache_init(void);
+
+void
+cache_deinit(void);
+
+#endif /* !__CACHE_H__ */
diff --git a/src/conffile.c b/src/conffile.c
index 88dc741..b9155de 100644
--- a/src/conffile.c
+++ b/src/conffile.c
@@ -53,8 +53,8 @@ static cfg_opt_t sec_general[] =
     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_STR("cache_path", STATEDIR "/cache/" PACKAGE "/cache.db", CFGF_NONE),
+    CFG_INT("cache_daap_threshold", 1000, CFGF_NONE),
     CFG_END()
   };
 
@@ -76,7 +76,9 @@ static cfg_opt_t sec_library[] =
     CFG_STR("name_podcasts", "Podcasts", CFGF_NONE),
     CFG_STR("name_audiobooks", "Audiobooks", CFGF_NONE),
     CFG_STR_LIST("artwork_basenames", "{artwork,cover,Folder}", CFGF_NONE),
-    CFG_STR_LIST("filetypes_ignore", "{.db,.ini}", CFGF_NONE),
+    CFG_BOOL("artwork_individual", cfg_false, CFGF_NONE),
+    CFG_STR_LIST("filetypes_ignore", "{.db,.ini,.db-journal,.pdf}", CFGF_NONE),
+    CFG_STR_LIST("filepath_ignore", NULL, CFGF_NONE),
     CFG_BOOL("filescan_disable", cfg_false, CFGF_NONE),
     CFG_BOOL("itunes_overrides", cfg_false, CFGF_NONE),
     CFG_STR_LIST("no_transcode", NULL, CFGF_NONE),
@@ -114,6 +116,17 @@ static cfg_opt_t sec_spotify[] =
     CFG_END()
   };
 
+/* SQLite section structure */
+static cfg_opt_t sec_sqlite[] =
+  {
+    CFG_INT("pragma_cache_size_library", -1, CFGF_NONE),
+    CFG_INT("pragma_cache_size_cache", -1, CFGF_NONE),
+    CFG_STR("pragma_journal_mode", NULL, CFGF_NONE),
+    CFG_INT("pragma_synchronous", -1, CFGF_NONE),
+    CFG_BOOL("vacuum", cfg_true, CFGF_NONE),
+    CFG_END()
+  };
+
 /* Config file structure */
 static cfg_opt_t toplvl_cfg[] =
   {
@@ -122,6 +135,7 @@ static cfg_opt_t toplvl_cfg[] =
     CFG_SEC("audio", sec_audio, CFGF_NONE),
     CFG_SEC("airplay", sec_airplay, CFGF_MULTI | CFGF_TITLE),
     CFG_SEC("spotify", sec_spotify, CFGF_NONE),
+    CFG_SEC("sqlite", sec_sqlite, CFGF_NONE),
     CFG_END()
   };
 
diff --git a/src/daap_cache.c b/src/daap_cache.c
deleted file mode 100644
index 4751244..0000000
--- a/src/daap_cache.c
+++ /dev/null
@@ -1,895 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 15d0fa7..0000000
--- a/src/daap_cache.h
+++ /dev/null
@@ -1,30 +0,0 @@
-
-#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 0782359..b94d77c 100644
--- a/src/db.c
+++ b/src/db.c
@@ -40,7 +40,7 @@
 
 #include "conffile.h"
 #include "logger.h"
-#include "daap_cache.h"
+#include "cache.h"
 #include "misc.h"
 #include "db.h"
 
@@ -291,6 +291,9 @@ db_smartpl_count_items(const char *smartpl_query);
 struct playlist_info *
 db_pl_fetch_byid(int id);
 
+static enum group_type
+db_group_type_bypersistentid(int64_t persistentid);
+
 
 char *
 db_escape_string(const char *str)
@@ -1194,13 +1197,13 @@ db_build_query_group_albums(struct query_params *qp, char **q)
   sort = sort_clause[qp->sort];
 
   if (idx && qp->filter)
-    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id), 1, f.album_artist, f.songartistid FROM files f, groups g WHERE f.songalbumid = g.persistentid AND g.type = %d AND f.disabled = 0 AND %s GROUP BY f.album, g.name %s %s;", G_ALBUMS, qp->filter, sort, idx);
+    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id), 1, f.album_artist, f.songartistid FROM files f JOIN groups g ON f.songalbumid = g.persistentid WHERE f.disabled = 0 AND %s GROUP BY f.songalbumid %s %s;", qp->filter, sort, idx);
   else if (idx)
-    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id), 1, f.album_artist, f.songartistid FROM files f, groups g WHERE f.songalbumid = g.persistentid AND g.type = %d AND f.disabled = 0 GROUP BY f.album, g.name %s %s;", G_ALBUMS, sort, idx);
+    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id), 1, f.album_artist, f.songartistid FROM files f JOIN groups g ON f.songalbumid = g.persistentid WHERE f.disabled = 0 GROUP BY f.songalbumid %s %s;", sort, idx);
   else if (qp->filter)
-    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id), 1, f.album_artist, f.songartistid FROM files f, groups g WHERE f.songalbumid = g.persistentid AND g.type = %d AND f.disabled = 0 AND %s GROUP BY f.album, g.name %s;", G_ALBUMS, qp->filter, sort);
+    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id), 1, f.album_artist, f.songartistid FROM files f JOIN groups g ON f.songalbumid = g.persistentid WHERE f.disabled = 0 AND %s GROUP BY f.songalbumid %s;", qp->filter, sort);
   else
-    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id), 1, f.album_artist, f.songartistid FROM files f, groups g WHERE f.songalbumid = g.persistentid AND g.type = %d AND f.disabled = 0 GROUP BY f.album, g.name %s;", G_ALBUMS, sort);
+    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album, f.album_sort, COUNT(f.id), 1, f.album_artist, f.songartistid FROM files f JOIN groups g ON f.songalbumid = g.persistentid WHERE f.disabled = 0 GROUP BY f.songalbumid %s;", sort);
 
   if (!query)
     {
@@ -1233,13 +1236,13 @@ db_build_query_group_artists(struct query_params *qp, char **q)
   sort = sort_clause[qp->sort];
 
   if (idx && qp->filter)
-    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id), COUNT(DISTINCT f.songalbumid), f.album_artist, f.songartistid FROM files f, groups g WHERE f.songartistid = g.persistentid AND g.type = %d AND f.disabled = 0 AND %s GROUP BY f.album_artist, g.name %s %s;", G_ARTISTS, qp->filter, sort, idx);
+    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id), COUNT(DISTINCT f.songalbumid), f.album_artist, f.songartistid FROM files f JOIN groups g ON f.songartistid = g.persistentid WHERE f.disabled = 0 AND %s GROUP BY f.songartistid %s %s;", qp->filter, sort, idx);
   else if (idx)
-    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id), COUNT(DISTINCT f.songalbumid), f.album_artist, f.songartistid FROM files f, groups g WHERE f.songartistid = g.persistentid AND g.type = %d AND f.disabled = 0 GROUP BY f.album_artist, g.name %s %s;", G_ARTISTS, sort, idx);
+    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id), COUNT(DISTINCT f.songalbumid), f.album_artist, f.songartistid FROM files f JOIN groups g ON f.songartistid = g.persistentid WHERE f.disabled = 0 GROUP BY f.songartistid %s %s;", sort, idx);
   else if (qp->filter)
-    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id), COUNT(DISTINCT f.songalbumid), f.album_artist, f.songartistid FROM files f, groups g WHERE f.songartistid = g.persistentid AND g.type = %d AND f.disabled = 0 AND %s GROUP BY f.album_artist, g.name %s;", G_ARTISTS, qp->filter, sort);
+    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id), COUNT(DISTINCT f.songalbumid), f.album_artist, f.songartistid FROM files f JOIN groups g ON f.songartistid = g.persistentid WHERE f.disabled = 0 AND %s GROUP BY f.songartistid %s;", qp->filter, sort);
   else
-    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id), COUNT(DISTINCT f.songalbumid), f.album_artist, f.songartistid FROM files f, groups g WHERE f.songartistid = g.persistentid AND g.type = %d AND f.disabled = 0 GROUP BY f.album_artist, g.name %s;", G_ARTISTS, sort);
+    query = sqlite3_mprintf("SELECT g.id, g.persistentid, f.album_artist, f.album_artist_sort, COUNT(f.id), COUNT(DISTINCT f.songalbumid), f.album_artist, f.songartistid FROM files f JOIN groups g ON f.songartistid = g.persistentid WHERE f.disabled = 0 GROUP BY f.songartistid %s;", sort);
 
   if (!query)
     {
@@ -1259,22 +1262,22 @@ db_build_query_group_items(struct query_params *qp, char **q)
   char *count;
   enum group_type gt;
 
-  gt = db_group_type_byid(qp->id);
+  gt = db_group_type_bypersistentid(qp->persistentid);
 
   switch (gt)
     {
       case G_ALBUMS:
-	count = sqlite3_mprintf("SELECT COUNT(*) FROM files f JOIN groups g ON f.songalbumid = g.persistentid"
-				" WHERE g.id = %d AND f.disabled = 0;", qp->id);
+	count = sqlite3_mprintf("SELECT COUNT(*) FROM files f"
+				" WHERE f.songalbumid = %" PRIi64 " AND f.disabled = 0;", qp->persistentid);
 	break;
 
       case G_ARTISTS:
-	count = sqlite3_mprintf("SELECT COUNT(*) FROM files f JOIN groups g ON f.songartistid = g.persistentid"
-				" WHERE g.id = %d AND f.disabled = 0;", qp->id);
+	count = sqlite3_mprintf("SELECT COUNT(*) FROM files f"
+				" WHERE f.songartistid = %" PRIi64 " AND f.disabled = 0;", qp->persistentid);
 	break;
 
       default:
-	DPRINTF(E_LOG, L_DB, "Unsupported group type %d for group id %d\n", gt, qp->id);
+	DPRINTF(E_LOG, L_DB, "Unsupported group type %d for group id %" PRIi64 "\n", gt, qp->persistentid);
 	return -1;
     }
 
@@ -1294,13 +1297,13 @@ db_build_query_group_items(struct query_params *qp, char **q)
   switch (gt)
     {
       case G_ALBUMS:
-	query = sqlite3_mprintf("SELECT f.* FROM files f JOIN groups g ON f.songalbumid = g.persistentid"
-				" WHERE g.id = %d AND f.disabled = 0;", qp->id);
+	query = sqlite3_mprintf("SELECT f.* FROM files f"
+				" WHERE f.songalbumid = %" PRIi64 " AND f.disabled = 0;", qp->persistentid);
 	break;
 
       case G_ARTISTS:
-	query = sqlite3_mprintf("SELECT f.* FROM files f JOIN groups g ON f.songartistid = g.persistentid"
-				" WHERE g.id = %d AND f.disabled = 0;", qp->id);
+	query = sqlite3_mprintf("SELECT f.* FROM files f"
+				" WHERE f.songartistid = %" PRIi64 " AND f.disabled = 0;", qp->persistentid);
 	break;
 
       default:
@@ -1325,24 +1328,24 @@ db_build_query_group_dirs(struct query_params *qp, char **q)
   char *count;
   enum group_type gt;
 
-  gt = db_group_type_byid(qp->id);
+  gt = db_group_type_bypersistentid(qp->persistentid);
 
   switch (gt)
     {
       case G_ALBUMS:
 	count = sqlite3_mprintf("SELECT COUNT(DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1)))"
-				" FROM files f JOIN groups g ON f.songalbumid = g.persistentid"
-				" WHERE g.id = %d AND f.disabled = 0;", qp->id);
+				" FROM files f"
+				" WHERE f.songalbumid = %" PRIi64 " AND f.disabled = 0;", qp->persistentid);
 	break;
 
       case G_ARTISTS:
 	count = sqlite3_mprintf("SELECT COUNT(DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1)))"
-				" FROM files f JOIN groups g ON f.songartistid = g.persistentid"
-				" WHERE g.id = %d AND f.disabled = 0;", qp->id);
+				" FROM files f"
+				" WHERE f.songartistid = %" PRIi64 " AND f.disabled = 0;", qp->persistentid);
 	break;
 
       default:
-	DPRINTF(E_LOG, L_DB, "Unsupported group type %d for group id %d\n", gt, qp->id);
+	DPRINTF(E_LOG, L_DB, "Unsupported group type %d for group id %" PRIi64 "\n", gt, qp->persistentid);
 	return -1;
     }
 
@@ -1363,14 +1366,14 @@ db_build_query_group_dirs(struct query_params *qp, char **q)
     {
       case G_ALBUMS:
 	query = sqlite3_mprintf("SELECT DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1))"
-				" FROM files f JOIN groups g ON f.songalbumid = g.persistentid"
-				" WHERE g.id = %d AND f.disabled = 0;", qp->id);
+				" FROM files f"
+				" WHERE f.songalbumid = %" PRIi64 " AND f.disabled = 0;", qp->persistentid);
 	break;
 
       case G_ARTISTS:
 	query = sqlite3_mprintf("SELECT DISTINCT(SUBSTR(f.path, 1, LENGTH(f.path) - LENGTH(f.fname) - 1))"
-				" FROM files f JOIN groups g ON f.songartistid = g.persistentid"
-				" WHERE g.id = %d AND f.disabled = 0;", qp->id);
+				" FROM files f"
+				" WHERE f.songartistid = %" PRIi64 " AND f.disabled = 0;", qp->persistentid);
 	break;
 
       default:
@@ -1430,28 +1433,28 @@ db_build_query_browse(struct query_params *qp, char *field, char *sort_field, ch
     }
   else
     {
-      size = strlen("ORDER BY f.") + strlen(field) + 1;
+      size = strlen("ORDER BY f.") + strlen(sort_field) + 1;
       sort = malloc(size);
       if (!sort)
 	{
 	  DPRINTF(E_LOG, L_DB, "Out of memory for sort string\n");
 	  return -1;
 	}
-      snprintf(sort, size, "ORDER BY f.%s", field);
+      snprintf(sort, size, "ORDER BY f.%s", sort_field);
     }
 
   if (idx && qp->filter)
-    query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
-			    " AND %s %s %s;", field, sort_field, field, qp->filter, sort, idx);
+    query = sqlite3_mprintf("SELECT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
+                            " AND %s GROUP BY f.%s %s %s;", field, sort_field, field, qp->filter, sort_field, sort, idx);
   else if (idx)
-    query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
-			    " %s %s;", field, sort_field, field, sort, idx);
+    query = sqlite3_mprintf("SELECT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
+                            " GROUP BY f.%s %s %s;", field, sort_field, field, sort_field, sort, idx);
   else if (qp->filter)
-    query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
-			    " AND %s %s;", field, sort_field, field, qp->filter, sort);
+    query = sqlite3_mprintf("SELECT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
+                            " AND %s GROUP BY f.%s %s;", field, sort_field, field, qp->filter, sort_field, sort);
   else
-    query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != '' %s",
-			    field, sort_field, field, sort);
+    query = sqlite3_mprintf("SELECT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
+                            " GROUP BY f.%s %s", field, sort_field, field, sort_field, sort);
 
   free(sort);
 
@@ -1581,7 +1584,7 @@ db_query_run(char *query, int free, int cache_update)
     sqlite3_free(query);
 
   if (cache_update)
-    daapcache_trigger();
+    cache_daap_trigger();
 
   return ((ret != SQLITE_OK) ? -1 : 0);
 }
@@ -2430,7 +2433,7 @@ db_file_add(struct media_file_info *mfi)
 
   sqlite3_free(query);
 
-  daapcache_trigger();
+  cache_daap_trigger();
 
   return 0;
 
@@ -2506,7 +2509,7 @@ db_file_update(struct media_file_info *mfi)
 
   sqlite3_free(query);
 
-  daapcache_trigger();
+  cache_daap_trigger();
 
   return 0;
 
@@ -3112,15 +3115,15 @@ db_groups_clear(void)
   return db_query_run("DELETE FROM groups;", 0, 1);
 }
 
-enum group_type
-db_group_type_byid(int id)
+static enum group_type
+db_group_type_bypersistentid(int64_t persistentid)
 {
-#define Q_TMPL "SELECT g.type FROM groups g WHERE g.id = '%d';"
+#define Q_TMPL "SELECT g.type FROM groups g WHERE g.persistentid = %" PRIi64 ";"
   char *query;
   sqlite3_stmt *stmt;
   int ret;
 
-  query = sqlite3_mprintf(Q_TMPL, id);
+  query = sqlite3_mprintf(Q_TMPL, persistentid);
   if (!query)
     {
       DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
@@ -3167,6 +3170,62 @@ db_group_type_byid(int id)
 #undef Q_TMPL
 }
 
+int
+db_group_persistentid_byid(int id, int64_t *persistentid)
+{
+#define Q_TMPL "SELECT g.persistentid FROM groups g WHERE g.id = %d;"
+  char *query;
+  sqlite3_stmt *stmt;
+  int ret;
+
+  query = sqlite3_mprintf(Q_TMPL, 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_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 -1;
+    }
+
+  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 -1;
+    }
+
+  *persistentid = sqlite3_column_int64(stmt, 0);
+
+#ifdef DB_PROFILE
+  while (db_blocking_step(stmt) == SQLITE_ROW)
+  ; /* EMPTY */
+#endif
+
+  sqlite3_finalize(stmt);
+  sqlite3_free(query);
+
+  return 0;
+
+#undef Q_TMPL
+}
+
+
 /* Remotes */
 static int
 db_pairing_delete_byremote(char *remote_id)
@@ -3281,7 +3340,7 @@ db_spotify_pl_delete(int id)
     {
       "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);",
+      "DELETE FROM files WHERE path LIKE 'spotify:%%' AND NOT path IN (SELECT filepath FROM playlistitems);",
     };
   char *query;
   int i;
@@ -3334,7 +3393,7 @@ db_admin_get(const char *key)
   ret = db_blocking_prepare_v2(query, strlen(query) + 1, &stmt, NULL);
   if (ret != SQLITE_OK)
     {
-      DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
+      DPRINTF(E_WARN, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
 
       sqlite3_free(query);
       return NULL;
@@ -3346,7 +3405,7 @@ db_admin_get(const char *key)
       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));	
+	DPRINTF(E_WARN, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));	
 
       sqlite3_finalize(stmt);
       sqlite3_free(query);
@@ -4052,6 +4111,8 @@ db_pragma_set_synchronous(int synchronous)
 #undef Q_TMPL
 }
 
+
+
 int
 db_perthread_init(void)
 {
@@ -4108,7 +4169,7 @@ db_perthread_init(void)
   sqlite3_profile(hdl, db_xprofile, NULL);
 #endif
 
-  cache_size = cfg_getint(cfg_getsec(cfg, "general"), "db_pragma_cache_size");
+  cache_size = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_cache_size_library");
   if (cache_size > -1)
     {
       db_pragma_set_cache_size(cache_size);
@@ -4116,14 +4177,14 @@ db_perthread_init(void)
       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");
+  journal_mode = cfg_getstr(cfg_getsec(cfg, "sqlite"), "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");
+  synchronous = cfg_getint(cfg_getsec(cfg, "sqlite"), "pragma_synchronous");
   if (synchronous > -1)
     {
       db_pragma_set_synchronous(synchronous);
@@ -4267,55 +4328,6 @@ db_perthread_deinit(void)
   "   path        VARCHAR(4096) NOT NULL"		\
   ");"
 
-#define I_RESCAN				\
-  "CREATE INDEX IF NOT EXISTS idx_rescan ON files(path, db_timestamp);"
-
-#define I_SONGARTISTID				\
-  "CREATE INDEX IF NOT EXISTS idx_sari ON files(songartistid);"
-
-#define I_SONGALBUMID				\
-  "CREATE INDEX IF NOT EXISTS idx_sali ON files(songalbumid);"
-
-#define I_STATEMKINDSARI				\
-  "CREATE INDEX IF NOT EXISTS idx_state_mkind_sari ON files(disabled, media_kind, songartistid);"
-
-#define I_STATEMKINDSALI				\
-  "CREATE INDEX IF NOT EXISTS idx_state_mkind_sali ON files(disabled, media_kind, songalbumid);"
-
-#define I_ARTIST				\
-  "CREATE INDEX IF NOT EXISTS idx_artist ON files(artist, artist_sort);"
-
-#define I_ALBUMARTIST				\
-  "CREATE INDEX IF NOT EXISTS idx_albumartist ON files(album_artist, album_artist_sort);"
-
-#define I_COMPOSER				\
-  "CREATE INDEX IF NOT EXISTS idx_composer ON files(composer, composer_sort);"
-
-#define I_TITLE					\
-  "CREATE INDEX IF NOT EXISTS idx_title ON files(title, title_sort);"
-
-#define I_ALBUM					\
-  "CREATE INDEX IF NOT EXISTS idx_album ON files(album, album_sort);"
-
-#define I_PL_PATH				\
-  "CREATE INDEX IF NOT EXISTS idx_pl_path ON playlists(path);"
-
-#define I_PL_DISABLED				\
-  "CREATE INDEX IF NOT EXISTS idx_pl_disabled ON playlists(disabled);"
-
-#define I_FILEPATH							\
-  "CREATE INDEX IF NOT EXISTS idx_filepath ON playlistitems(filepath ASC);"
-
-#define I_PLITEMID							\
-  "CREATE INDEX IF NOT EXISTS idx_playlistid ON playlistitems(playlistid, filepath);"
-
-#define I_GRP_TYPE_PERSIST				\
-  "CREATE INDEX IF NOT EXISTS idx_grp_type_persist ON groups(type, persistentid);"
-
-#define I_PAIRING				\
-  "CREATE INDEX IF NOT EXISTS idx_pairingguid ON pairings(guid);"
-
-
 #define TRG_GROUPS_INSERT_FILES						\
   "CREATE TRIGGER update_groups_new_file AFTER INSERT ON files FOR EACH ROW" \
   " BEGIN"								\
@@ -4360,16 +4372,22 @@ db_perthread_deinit(void)
   " VALUES(8, 'Purchased', 0, 'media_kind = 1024', 0, '', 0, 8);"
  */
 
-#define SCHEMA_VERSION 15
-#define Q_SCVER					\
+#define SCHEMA_VERSION_MAJOR 15
+#define SCHEMA_VERSION_MINOR 01
+// Q_SCVER should be deprecated/removed at v16
+#define Q_SCVER						\
   "INSERT INTO admin (key, value) VALUES ('schema_version', '15');"
+#define Q_SCVER_MAJOR					\
+  "INSERT INTO admin (key, value) VALUES ('schema_version_major', '15');"
+#define Q_SCVER_MINOR					\
+  "INSERT INTO admin (key, value) VALUES ('schema_version_minor', '01');"
 
 struct db_init_query {
   char *query;
   char *desc;
 };
 
-static const struct db_init_query db_init_queries[] =
+static const struct db_init_query db_init_table_queries[] =
   {
     { T_ADMIN,     "create table admin" },
     { T_FILES,     "create table files" },
@@ -4380,6 +4398,82 @@ static const struct db_init_query db_init_queries[] =
     { T_SPEAKERS,  "create table speakers" },
     { T_INOTIFY,   "create table inotify" },
 
+    { TRG_GROUPS_INSERT_FILES,    "create trigger update_groups_new_file" },
+    { TRG_GROUPS_UPDATE_FILES,    "create trigger update_groups_update_file" },
+
+    { Q_PL1,       "create default playlist" },
+    { Q_PL2,       "create default smart playlist 'Music'" },
+    { Q_PL3,       "create default smart playlist 'Movies'" },
+    { Q_PL4,       "create default smart playlist 'TV Shows'" },
+    { Q_PL5,       "create default smart playlist 'Podcasts'" },
+    { Q_PL6,       "create default smart playlist 'Audiobooks'" },
+
+    { Q_SCVER,       "set schema version" },
+    { Q_SCVER_MAJOR, "set schema version major" },
+    { Q_SCVER_MINOR, "set schema version minor" },
+  };
+
+
+/* Indices must be prefixed with idx_ for db_drop_indices() to id them */
+
+#define I_RESCAN				\
+  "CREATE INDEX IF NOT EXISTS idx_rescan ON files(path, db_timestamp);"
+
+#define I_SONGARTISTID				\
+  "CREATE INDEX IF NOT EXISTS idx_sari ON files(songartistid);"
+
+/* Used by Q_GROUP_ALBUMS */
+#define I_SONGALBUMID				\
+  "CREATE INDEX IF NOT EXISTS idx_sali ON files(songalbumid, disabled, media_kind, album_sort, disc, track);"
+
+/* Used by Q_GROUP_ARTISTS */
+#define I_STATEMKINDSARI				\
+  "CREATE INDEX IF NOT EXISTS idx_state_mkind_sari ON files(disabled, media_kind, songartistid);"
+
+#define I_STATEMKINDSALI				\
+  "CREATE INDEX IF NOT EXISTS idx_state_mkind_sali ON files(disabled, media_kind, songalbumid);"
+
+#define I_ARTIST				\
+  "CREATE INDEX IF NOT EXISTS idx_artist ON files(artist, artist_sort);"
+
+#define I_ALBUMARTIST				\
+  "CREATE INDEX IF NOT EXISTS idx_albumartist ON files(album_artist, album_artist_sort);"
+
+/* Used by Q_BROWSE_COMPOSERS */
+#define I_COMPOSER				\
+  "CREATE INDEX IF NOT EXISTS idx_composer ON files(disabled, media_kind, composer_sort);"
+
+/* Used by Q_BROWSE_GENRES */
+#define I_GENRE					\
+  "CREATE INDEX IF NOT EXISTS idx_genre ON files(disabled, media_kind, genre);"
+
+/* Used by Q_PLITEMS for smart playlists */
+#define I_TITLE					\
+  "CREATE INDEX IF NOT EXISTS idx_title ON files(disabled, media_kind, title_sort);"
+
+#define I_ALBUM					\
+  "CREATE INDEX IF NOT EXISTS idx_album ON files(album, album_sort);"
+
+#define I_PL_PATH				\
+  "CREATE INDEX IF NOT EXISTS idx_pl_path ON playlists(path);"
+
+#define I_PL_DISABLED				\
+  "CREATE INDEX IF NOT EXISTS idx_pl_disabled ON playlists(disabled);"
+
+#define I_FILEPATH							\
+  "CREATE INDEX IF NOT EXISTS idx_filepath ON playlistitems(filepath ASC);"
+
+#define I_PLITEMID							\
+  "CREATE INDEX IF NOT EXISTS idx_playlistid ON playlistitems(playlistid, filepath);"
+
+#define I_GRP_PERSIST				\
+  "CREATE INDEX IF NOT EXISTS idx_grp_persist ON groups(persistentid);"
+
+#define I_PAIRING				\
+  "CREATE INDEX IF NOT EXISTS idx_pairingguid ON pairings(guid);"
+
+static const struct db_init_query db_init_index_queries[] =
+  {
     { I_RESCAN,    "create rescan index" },
     { I_SONGARTISTID, "create songartistid index" },
     { I_SONGALBUMID, "create songalbumid index" },
@@ -4389,6 +4483,7 @@ static const struct db_init_query db_init_queries[] =
     { I_ARTIST,    "create artist index" },
     { I_ALBUMARTIST, "create album_artist index" },
     { I_COMPOSER,  "create composer index" },
+    { I_GENRE,     "create genre index" },
     { I_TITLE,     "create title index" },
     { I_ALBUM,     "create album index" },
 
@@ -4398,22 +4493,97 @@ static const struct db_init_query db_init_queries[] =
     { I_FILEPATH,  "create file path index" },
     { I_PLITEMID,  "create playlist id index" },
 
-    { I_GRP_TYPE_PERSIST, "create groups type/persistentid index" },
+    { I_GRP_PERSIST, "create groups persistentid index" },
 
     { I_PAIRING,   "create pairing guid index" },
+  };
 
-    { TRG_GROUPS_INSERT_FILES,    "create trigger update_groups_new_file" },
-    { TRG_GROUPS_UPDATE_FILES,    "create trigger update_groups_update_file" },
+static int
+db_create_indices(void)
+{
+  char *errmsg;
+  int i;
+  int ret;
 
-    { Q_PL1,       "create default playlist" },
-    { Q_PL2,       "create default smart playlist 'Music'" },
-    { Q_PL3,       "create default smart playlist 'Movies'" },
-    { Q_PL4,       "create default smart playlist 'TV Shows'" },
-    { Q_PL5,       "create default smart playlist 'Podcasts'" },
-    { Q_PL6,       "create default smart playlist 'Audiobooks'" },
+  for (i = 0; i < (sizeof(db_init_index_queries) / sizeof(db_init_index_queries[0])); i++)
+    {
+      DPRINTF(E_DBG, L_DB, "DB init index query: %s\n", db_init_index_queries[i].desc);
 
-    { Q_SCVER,     "set schema version" },
-  };
+      ret = sqlite3_exec(hdl, db_init_index_queries[i].query, NULL, NULL, &errmsg);
+      if (ret != SQLITE_OK)
+	{
+	  DPRINTF(E_FATAL, L_DB, "DB init error: %s\n", errmsg);
+
+	  sqlite3_free(errmsg);
+	  return -1;
+	}
+    }
+
+  return 0;
+}
+
+static int
+db_drop_indices(void)
+{
+#define Q_INDEX "SELECT name FROM sqlite_master WHERE type == 'index' AND name LIKE 'idx_%';"
+#define Q_TMPL "DROP INDEX %q;"
+  sqlite3_stmt *stmt;
+  char *errmsg;
+  char *query;
+  char *index[256];
+  int ret;
+  int i;
+  int n;
+
+  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", Q_INDEX);
+
+  ret = sqlite3_prepare_v2(hdl, Q_INDEX, strlen(Q_INDEX) + 1, &stmt, NULL);
+  if (ret != SQLITE_OK)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
+      return -1;
+    }
+
+  n = 0;
+  while ((ret = sqlite3_step(stmt)) == SQLITE_ROW)
+    {
+      index[n] = strdup((char *)sqlite3_column_text(stmt, 0));
+      n++;
+    }
+
+  if (ret != SQLITE_DONE)
+    {
+      DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
+
+      sqlite3_finalize(stmt);
+      return -1;
+    }
+
+  sqlite3_finalize(stmt);
+
+  for (i = 0; i < n; i++)
+    {
+      query = sqlite3_mprintf(Q_TMPL, index[i]);
+      free(index[i]);
+
+      DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query);
+
+      ret = sqlite3_exec(hdl, query, NULL, NULL, &errmsg);
+      if (ret != SQLITE_OK)
+	{
+	  DPRINTF(E_LOG, L_DB, "DB error while running '%s': %s\n", query, errmsg);
+
+	  sqlite3_free(errmsg);
+	  return -1;
+	}
+
+      sqlite3_free(query);
+    }
+
+  return 0;
+#undef Q_TMPL
+#undef Q_INDEX
+}
 
 static int
 db_create_tables(void)
@@ -4422,11 +4592,11 @@ db_create_tables(void)
   int i;
   int ret;
 
-  for (i = 0; i < (sizeof(db_init_queries) / sizeof(db_init_queries[0])); i++)
+  for (i = 0; i < (sizeof(db_init_table_queries) / sizeof(db_init_table_queries[0])); i++)
     {
-      DPRINTF(E_DBG, L_DB, "DB init query: %s\n", db_init_queries[i].desc);
+      DPRINTF(E_DBG, L_DB, "DB init table query: %s\n", db_init_table_queries[i].desc);
 
-      ret = sqlite3_exec(hdl, db_init_queries[i].query, NULL, NULL, &errmsg);
+      ret = sqlite3_exec(hdl, db_init_table_queries[i].query, NULL, NULL, &errmsg);
       if (ret != SQLITE_OK)
 	{
 	  DPRINTF(E_FATAL, L_DB, "DB init error: %s\n", errmsg);
@@ -4436,7 +4606,9 @@ db_create_tables(void)
 	}
     }
 
-  return 0;
+  ret = db_create_indices();
+
+  return ret;
 }
 
 static int
@@ -4893,18 +5065,6 @@ db_upgrade_v11(void)
   "   album_artist_sort  VARCHAR(1024) DEFAULT NULL COLLATE DAAP"	\
   ");"
 
-#define U_V12_IDX_PATH						\
-  "CREATE INDEX IF NOT EXISTS idx_path ON files(path, idx);"
-
-#define U_V12_IDX_TS							\
-  "CREATE INDEX IF NOT EXISTS idx_titlesort ON files(title_sort);"
-
-#define U_V12_IDX_AS							\
-  "CREATE INDEX IF NOT EXISTS idx_artistsort ON files(artist_sort);"
-
-#define U_V12_IDX_BS							\
-  "CREATE INDEX IF NOT EXISTS idx_albumsort ON files(album_sort);"
-
 #define U_V12_TRG1							\
   "CREATE TRIGGER update_groups_new_file AFTER INSERT ON files FOR EACH ROW" \
   " BEGIN"								\
@@ -4922,11 +5082,6 @@ db_upgrade_v11(void)
 
 static const struct db_init_query db_upgrade_v12_queries[] =
   {
-    { U_V12_IDX_PATH, "create index path table files" },
-    { U_V12_IDX_TS,   "create index titlesort table files" },
-    { U_V12_IDX_AS,   "create index artistsort table files" },
-    { U_V12_IDX_BS,   "create index albumsort table files" },
-
     { U_V12_TRG1,     "create trigger update_groups_new_file" },
     { U_V12_TRG2,     "create trigger update_groups_update_file" },
 
@@ -4967,51 +5122,6 @@ db_upgrade_v12(void)
 
 /* Upgrade from schema v12 to v13 */
 
-#define U_V13_DROP_IDX_PATH						\
-  "DROP INDEX idx_path;"
-
-#define U_V13_DROP_IDX_TS						\
-  "DROP INDEX idx_titlesort;"
-
-#define U_V13_DROP_IDX_AS						\
-  "DROP INDEX idx_artistsort;"
-
-#define U_V13_DROP_IDX_BS						\
-  "DROP INDEX idx_albumsort;"
-
-#define U_V13_IDX_RESCAN						\
-  "CREATE INDEX IF NOT EXISTS idx_rescan ON files(path, db_timestamp);"
-
-#define U_V13_IDX_SONGALBUMID					\
-  "CREATE INDEX IF NOT EXISTS idx_sai ON files(songalbumid);"
-
-#define U_V13_IDX_STATEMKINDSAI						\
-  "CREATE INDEX IF NOT EXISTS idx_state_mkind_sai ON files(disabled, media_kind, songalbumid);"
-
-#define U_V13_IDX_ARTIST				\
-  "CREATE INDEX IF NOT EXISTS idx_artist ON files(artist, artist_sort);"
-
-#define U_V13_IDX_ALBUMARTIST				\
-  "CREATE INDEX IF NOT EXISTS idx_albumartist ON files(album_artist, album_artist_sort);"
-
-#define U_V13_IDX_COMPOSER				\
-  "CREATE INDEX IF NOT EXISTS idx_composer ON files(composer, composer_sort);"
-
-#define U_V13_IDX_TITLE					\
-  "CREATE INDEX IF NOT EXISTS idx_title ON files(title, title_sort);"
-
-#define U_V13_IDX_ALBUM					\
-  "CREATE INDEX IF NOT EXISTS idx_album ON files(album, album_sort);"
-
-#define U_V13_IDX_GRP_TYPE_PERSIST					\
-  "CREATE INDEX IF NOT EXISTS idx_grp_type_persist ON groups(type, persistentid);"
-
-#define U_V13_IDX_PL_PATH				\
-  "CREATE INDEX IF NOT EXISTS idx_pl_path ON playlists(path);"
-
-#define U_V13_IDX_PL_DISABLED				\
-  "CREATE INDEX IF NOT EXISTS idx_pl_disabled ON playlists(disabled);"
-
 #define U_V13_PL2							\
   "UPDATE playlists SET query = 'f.media_kind = 1' where id = 2;"
 
@@ -5026,25 +5136,6 @@ db_upgrade_v12(void)
 
 static const struct db_init_query db_upgrade_v13_queries[] =
   {
-    { U_V13_DROP_IDX_PATH, "drop index path table files" },
-    { U_V13_DROP_IDX_TS,   "drop index titlesort table files" },
-    { U_V13_DROP_IDX_AS,   "drop index artistsort table files" },
-    { U_V13_DROP_IDX_BS,   "drop index albumsort table files" },
-
-    { U_V13_IDX_RESCAN,    "create rescan index" },
-    { U_V13_IDX_SONGALBUMID, "create songalbumid index" },
-    { U_V13_IDX_STATEMKINDSAI, "create state/mkind/sai index" },
-    { U_V13_IDX_ARTIST,    "create artist index" },
-    { U_V13_IDX_ALBUMARTIST, "create album_artist index" },
-    { U_V13_IDX_COMPOSER,  "create composer index" },
-    { U_V13_IDX_TITLE,     "create title index" },
-    { U_V13_IDX_ALBUM,     "create album index" },
-
-    { U_V13_IDX_GRP_TYPE_PERSIST, "create groups type/persistentid index" },
-
-    { U_V13_IDX_PL_PATH,   "create playlist path index" },
-    { U_V13_IDX_PL_DISABLED, "create playlist state index" },
-
     { U_V13_PL2,           "update default smart playlist 'Music'" },
     { U_V13_PL3,           "update default smart playlist 'Movies'" },
     { U_V13_PL4,           "update default smart playlist 'TV Shows'" },
@@ -5127,36 +5218,6 @@ static const struct db_init_query db_upgrade_v13_queries[] =
 #define U_V14_DELETE_PL6_2			\
   "DELETE FROM playlistitems WHERE playlistid=6;"
 
-#define U_V14_IDX_RESCAN				\
-  "CREATE INDEX IF NOT EXISTS idx_rescan ON files(path, db_timestamp);"
-
-#define U_V14_IDX_SONGARTISTID				\
-  "CREATE INDEX IF NOT EXISTS idx_sari ON files(songartistid);"
-
-#define U_V14_IDX_SONGALBUMID				\
-  "CREATE INDEX IF NOT EXISTS idx_sali ON files(songalbumid);"
-
-#define U_V14_IDX_STATEMKINDSARI				\
-  "CREATE INDEX IF NOT EXISTS idx_state_mkind_sari ON files(disabled, media_kind, songartistid);"
-
-#define U_V14_IDX_STATEMKINDSALI				\
-  "CREATE INDEX IF NOT EXISTS idx_state_mkind_sali ON files(disabled, media_kind, songalbumid);"
-
-#define U_V14_IDX_ARTIST				\
-  "CREATE INDEX IF NOT EXISTS idx_artist ON files(artist, artist_sort);"
-
-#define U_V14_IDX_ALBUMARTIST				\
-  "CREATE INDEX IF NOT EXISTS idx_albumartist ON files(album_artist, album_artist_sort);"
-
-#define U_V14_IDX_COMPOSER				\
-  "CREATE INDEX IF NOT EXISTS idx_composer ON files(composer, composer_sort);"
-
-#define U_V14_IDX_TITLE					\
-  "CREATE INDEX IF NOT EXISTS idx_title ON files(title, title_sort);"
-
-#define U_V14_IDX_ALBUM					\
-  "CREATE INDEX IF NOT EXISTS idx_album ON files(album, album_sort);"
-
 #define U_V14_TRG1							\
   "CREATE TRIGGER update_groups_new_file AFTER INSERT ON files FOR EACH ROW" \
   " BEGIN"								\
@@ -5189,18 +5250,6 @@ static const struct db_init_query db_upgrade_v14_queries[] =
     { U_V14_DELETE_PL6_1, "delete playlist id 6 table playlists" },
     { U_V14_DELETE_PL6_2, "delete playlist id 6 table playlistitems" },
 
-    { U_V14_IDX_RESCAN,         "create rescan index table files" },
-    { U_V14_IDX_SONGARTISTID,   "create songartistid index table files" },
-    { U_V14_IDX_SONGALBUMID,    "create songalbumid index table files" },
-    { U_V14_IDX_STATEMKINDSARI, "create state/mkind/sari index table files" },
-    { U_V14_IDX_STATEMKINDSALI, "create state/mkind/sali index table files" },
-
-    { U_V14_IDX_ARTIST,      "create artist index table files" },
-    { U_V14_IDX_ALBUMARTIST, "create album_artist index table files" },
-    { U_V14_IDX_COMPOSER,    "create composer index table files" },
-    { U_V14_IDX_TITLE,       "create title index table files" },
-    { U_V14_IDX_ALBUM,       "create album index table files" },
-
     { U_V14_TRG1,     "create trigger update_groups_new_file" },
     { U_V14_TRG2,     "create trigger update_groups_update_file" },
 
@@ -5307,36 +5356,6 @@ db_upgrade_v14(void)
   "   album_artist_sort  VARCHAR(1024) DEFAULT NULL COLLATE DAAP"	\
   ");"
 
-#define U_V15_IDX_RESCAN				\
-  "CREATE INDEX IF NOT EXISTS idx_rescan ON files(path, db_timestamp);"
-
-#define U_V15_IDX_SONGARTISTID				\
-  "CREATE INDEX IF NOT EXISTS idx_sari ON files(songartistid);"
-
-#define U_V15_IDX_SONGALBUMID				\
-  "CREATE INDEX IF NOT EXISTS idx_sali ON files(songalbumid);"
-
-#define U_V15_IDX_STATEMKINDSARI				\
-  "CREATE INDEX IF NOT EXISTS idx_state_mkind_sari ON files(disabled, media_kind, songartistid);"
-
-#define U_V15_IDX_STATEMKINDSALI				\
-  "CREATE INDEX IF NOT EXISTS idx_state_mkind_sali ON files(disabled, media_kind, songalbumid);"
-
-#define U_V15_IDX_ARTIST				\
-  "CREATE INDEX IF NOT EXISTS idx_artist ON files(artist, artist_sort);"
-
-#define U_V15_IDX_ALBUMARTIST				\
-  "CREATE INDEX IF NOT EXISTS idx_albumartist ON files(album_artist, album_artist_sort);"
-
-#define U_V15_IDX_COMPOSER				\
-  "CREATE INDEX IF NOT EXISTS idx_composer ON files(composer, composer_sort);"
-
-#define U_V15_IDX_TITLE					\
-  "CREATE INDEX IF NOT EXISTS idx_title ON files(title, title_sort);"
-
-#define U_V15_IDX_ALBUM					\
-  "CREATE INDEX IF NOT EXISTS idx_album ON files(album, album_sort);"
-
 #define U_V15_TRG1							\
   "CREATE TRIGGER update_groups_new_file AFTER INSERT ON files FOR EACH ROW" \
   " BEGIN"								\
@@ -5356,18 +5375,6 @@ db_upgrade_v14(void)
 
 static const struct db_init_query db_upgrade_v15_queries[] =
   {
-    { U_V15_IDX_RESCAN,         "create rescan index table files" },
-    { U_V15_IDX_SONGARTISTID,   "create songartistid index table files" },
-    { U_V15_IDX_SONGALBUMID,    "create songalbumid index table files" },
-    { U_V15_IDX_STATEMKINDSARI, "create state/mkind/sari index table files" },
-    { U_V15_IDX_STATEMKINDSALI, "create state/mkind/sali index table files" },
-
-    { U_V15_IDX_ARTIST,      "create artist index table files" },
-    { U_V15_IDX_ALBUMARTIST, "create album_artist index table files" },
-    { U_V15_IDX_COMPOSER,    "create composer index table files" },
-    { U_V15_IDX_TITLE,       "create title index table files" },
-    { U_V15_IDX_ALBUM,       "create album index table files" },
-
     { U_V15_TRG1,     "create trigger update_groups_new_file" },
     { U_V15_TRG2,     "create trigger update_groups_update_file" },
 
@@ -5407,51 +5414,79 @@ db_upgrade_v15(void)
 #undef Q_DUMP
 }
 
+/* Upgrade from schema v15 to v15.01 */
+/* Improved indices (will be generated by generic schema update) */
+
+#define U_V1501_SCVER_MAJOR			\
+  "INSERT INTO admin (key, value) VALUES ('schema_version_major', '15');"
+#define U_V1501_SCVER_MINOR			\
+  "INSERT INTO admin (key, value) VALUES ('schema_version_minor', '01');"
+
+static const struct db_init_query db_upgrade_v1501_queries[] =
+  {
+    { U_V1501_SCVER_MAJOR,    "set schema_version_major to 15" },
+    { U_V1501_SCVER_MINOR,    "set schema_version_minor to 01" },
+  };
+
 static int
 db_check_version(void)
 {
-#define Q_VER "SELECT value FROM admin WHERE key = 'schema_version';"
 #define Q_VACUUM "VACUUM;"
-  sqlite3_stmt *stmt;
+  char *buf;
   char *errmsg;
-  int cur_ver;
+  int db_ver_major;
+  int db_ver_minor;
+  int db_ver;
+  int vacuum;
   int ret;
 
-  DPRINTF(E_DBG, L_DB, "Running query '%s'\n", Q_VER);
+  vacuum = cfg_getbool(cfg_getsec(cfg, "sqlite"), "vacuum");
 
-  ret = sqlite3_prepare_v2(hdl, Q_VER, strlen(Q_VER) + 1, &stmt, NULL);
-  if (ret != SQLITE_OK)
+  buf = db_admin_get("schema_version_major");
+  if (!buf)
+    buf = db_admin_get("schema_version"); // Pre schema v15.1
+
+  if (!buf)
+    return 1; // Will create new database
+
+  safe_atoi32(buf, &db_ver_major);
+  free(buf);
+
+  buf = db_admin_get("schema_version_minor");
+  if (buf)
     {
-      DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl));
-      return 1;
+      safe_atoi32(buf, &db_ver_minor);
+      free(buf);
     }
+  else
+    db_ver_minor = 0;
 
-  ret = sqlite3_step(stmt);
-  if (ret != SQLITE_ROW)
+  db_ver = db_ver_major * 100 + db_ver_minor;
+
+  if (db_ver_major < 10)
     {
-      DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl));
+      DPRINTF(E_FATAL, L_DB, "Database schema v%d too old, cannot upgrade\n", db_ver_major);
 
-      sqlite3_finalize(stmt);
       return -1;
     }
-
-  cur_ver = sqlite3_column_int(stmt, 0);
-
-  sqlite3_finalize(stmt);
-
-  if (cur_ver < 10)
+  else if (db_ver_major > SCHEMA_VERSION_MAJOR)
     {
-      DPRINTF(E_FATAL, L_DB, "Database schema v%d too old, cannot upgrade\n", cur_ver);
+      DPRINTF(E_FATAL, L_DB, "Database schema v%d is newer than the supported version\n", db_ver_major);
 
       return -1;
     }
-  else if (cur_ver < SCHEMA_VERSION)
+  else if (db_ver < (SCHEMA_VERSION_MAJOR * 100 + SCHEMA_VERSION_MINOR))
     {
-      DPRINTF(E_LOG, L_DB, "Database schema outdated, schema upgrade needed v%d -> v%d\n", cur_ver, SCHEMA_VERSION);
+      DPRINTF(E_LOG, L_DB, "Database schema outdated, upgrading schema v%d.%d -> v%d.%d...\n",
+                           db_ver_major, db_ver_minor, SCHEMA_VERSION_MAJOR, SCHEMA_VERSION_MINOR);
 
-      switch (cur_ver)
+      ret = db_drop_indices();
+      if (ret < 0)
+	return -1;
+
+      switch (db_ver)
 	{
-	  case 10:
+	  case 1000:
 	    ret = db_generic_upgrade(db_upgrade_v11_queries, sizeof(db_upgrade_v11_queries) / sizeof(db_upgrade_v11_queries[0]));
 	    if (ret < 0)
 	      return -1;
@@ -5462,7 +5497,7 @@ db_check_version(void)
 
 	    /* FALLTHROUGH */
 
-	  case 11:
+	  case 1100:
 	    ret = db_upgrade_v12();
 	    if (ret < 0)
 	      return -1;
@@ -5473,14 +5508,14 @@ db_check_version(void)
 
 	    /* FALLTHROUGH */
 
-	  case 12:
+	  case 1200:
 	    ret = db_generic_upgrade(db_upgrade_v13_queries, sizeof(db_upgrade_v13_queries) / sizeof(db_upgrade_v13_queries[0]));
 	    if (ret < 0)
 	      return -1;
 
 	    /* FALLTHROUGH */
 
-	  case 13:
+	  case 1300:
 	    ret = db_upgrade_v14();
 	    if (ret < 0)
 	      return -1;
@@ -5491,7 +5526,7 @@ db_check_version(void)
 
 	    /* FALLTHROUGH */
 
-	  case 14:
+	  case 1400:
 	    ret = db_upgrade_v15();
 	    if (ret < 0)
 	      return -1;
@@ -5500,15 +5535,30 @@ db_check_version(void)
 	    if (ret < 0)
 	      return -1;
 
+	    /* FALLTHROUGH */
+
+	  case 1500:
+	    ret = db_generic_upgrade(db_upgrade_v1501_queries, sizeof(db_upgrade_v1501_queries) / sizeof(db_upgrade_v1501_queries[0]));
+	    if (ret < 0)
+	      return -1;
+
 	    break;
 
 	  default:
-	    DPRINTF(E_LOG, L_DB, "No upgrade path from DB schema v%d to v%d\n", cur_ver, SCHEMA_VERSION);
+	    DPRINTF(E_FATAL, L_DB, "No upgrade path from the current DB schema\n");
 	    return -1;
 	}
 
-      /* What about some housekeeping work, eh? */
-      DPRINTF(E_INFO, L_DB, "Now vacuuming database, this may take some time...\n");
+      vacuum = 1;
+
+      ret = db_create_indices();
+      if (ret < 0)
+	return -1;
+    }
+
+  if (vacuum)
+    {
+      DPRINTF(E_LOG, L_DB, "Now vacuuming database, this may take some time...\n");
 
       ret = sqlite3_exec(hdl, Q_VACUUM, NULL, NULL, &errmsg);
       if (ret != SQLITE_OK)
@@ -5519,15 +5569,9 @@ db_check_version(void)
 	  return -1;
 	}
     }
-  else if (cur_ver > SCHEMA_VERSION)
-    {
-      DPRINTF(E_LOG, L_DB, "Database schema is newer than the supported version\n");
-      return -1;
-    }
 
   return 0;
 
-#undef Q_VER
 #undef Q_VACUUM
 }
 
@@ -5576,7 +5620,7 @@ db_init(void)
     }
   else if (ret > 0)
     {
-      DPRINTF(E_FATAL, L_DB, "Could not check database version, trying DB init\n");
+      DPRINTF(E_LOG, L_DB, "Could not check database version, trying DB init\n");
 
       ret = db_create_tables();
       if (ret < 0)
@@ -5596,7 +5640,7 @@ db_init(void)
 
   db_perthread_deinit();
 
-  DPRINTF(E_INFO, L_DB, "Database OK with %d active files and %d active playlists\n", files, pls);
+  DPRINTF(E_LOG, L_DB, "Database OK with %d active files and %d active playlists\n", files, pls);
 
   return 0;
 }
diff --git a/src/db.h b/src/db.h
index 78be9f8..a7d72bf 100644
--- a/src/db.h
+++ b/src/db.h
@@ -54,6 +54,7 @@ struct query_params {
   enum index_type idx_type;
   enum sort_type sort;
   int id;
+  int64_t persistentid;
   int offset;
   int limit;
 
@@ -461,8 +462,8 @@ db_pl_enable_bycookie(uint32_t cookie, char *path);
 int
 db_groups_clear(void);
 
-enum group_type
-db_group_type_byid(int id);
+int
+db_group_persistentid_byid(int id, int64_t *persistentid);
 
 /* Remotes */
 int
diff --git a/src/filescanner.c b/src/filescanner.c
index 97d4365..0a5dab0 100644
--- a/src/filescanner.c
+++ b/src/filescanner.c
@@ -53,6 +53,10 @@
 # include <sys/eventfd.h>
 #endif
 
+#ifdef HAVE_REGEX_H
+# include <regex.h>
+#endif
+
 #include <event.h>
 
 #include "logger.h"
@@ -62,6 +66,8 @@
 #include "misc.h"
 #include "remote_pairing.h"
 #include "player.h"
+#include "cache.h"
+#include "artwork.h"
 
 #ifdef LASTFM
 # include "lastfm.h"
@@ -170,6 +176,43 @@ pop_dir(struct stacked_dir **s)
   return ret;
 }
 
+#ifdef HAVE_REGEX_H
+/* Checks if the file path is configured to be ignored */
+static int
+file_path_ignore(const char *path)
+{
+  cfg_t *lib;
+  regex_t regex;
+  int n;
+  int i;
+  int ret;
+
+  lib = cfg_getsec(cfg, "library");
+  n = cfg_size(lib, "filepath_ignore");
+
+  for (i = 0; i < n; i++)
+    {
+      ret = regcomp(&regex, cfg_getnstr(lib, "filepath_ignore", i), 0);
+      if (ret != 0)
+	{
+	  DPRINTF(E_LOG, L_SCAN, "Could not compile regex for matching with file path\n");
+	  return 0;
+	}
+
+      ret = regexec(&regex, path, 0, NULL, 0);
+      regfree(&regex);
+
+      if (ret == 0)
+	{
+	  DPRINTF(E_DBG, L_SCAN, "Regex match: %s\n", path);
+	  return 1;
+	}
+    }
+
+  return 0;
+}
+#endif
+
 /* Checks if the file extension is in the ignore list */
 static int
 file_type_ignore(const char *ext)
@@ -201,6 +244,11 @@ file_type_get(const char *path) {
   else
     filename++;
 
+#ifdef HAVE_REGEX_H
+  if (file_path_ignore(path))
+    return FILE_IGNORE;
+#endif
+
   ext = strrchr(path, '.');
   if (!ext || (strlen(ext) == 1))
     return FILE_REGULAR;
@@ -208,9 +256,12 @@ file_type_get(const char *path) {
   if ((strcasecmp(ext, ".m3u") == 0) || (strcasecmp(ext, ".pls") == 0))
     return FILE_PLAYLIST;
 
-  if ((strcasecmp(ext, ".png") == 0) || (strcasecmp(ext, ".jpg") == 0))
+  if (artwork_file_is_artwork(filename))
     return FILE_ARTWORK;
 
+  if ((strcasecmp(ext, ".jpg") == 0) || (strcasecmp(ext, ".png") == 0))
+    return FILE_IGNORE;
+
 #ifdef ITUNES
   if (strcasecmp(ext, ".xml") == 0)
     return FILE_ITUNES;
@@ -712,6 +763,9 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags)
       case FILE_REGULAR:
 	filescanner_process_media(file, mtime, size, type, NULL);
 
+	cache_artwork_ping(file, mtime);
+	// TODO [artworkcache] If entry in artwork cache exists for no artwork available, delete the entry if media file has embedded artwork
+
 	counter++;
 
 	/* When in bulk mode, split transaction in pieces of 200 */
@@ -731,6 +785,14 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags)
 	  process_playlist(file, mtime);
 	break;
 
+      case FILE_ARTWORK:
+	DPRINTF(E_DBG, L_SCAN, "Artwork file: %s\n", file);
+	cache_artwork_ping(file, mtime);
+
+	// TODO [artworkcache] If entry in artwork cache exists for no artwork available for a album with files in the same directory, delete the entry
+
+	break;
+
       case FILE_CTRL_REMOTE:
 	remote_pairing_read_pin(file);
 	break;
@@ -1066,6 +1128,7 @@ bulk_scan(int flags)
 
       DPRINTF(E_DBG, L_SCAN, "Purging old database content\n");
       db_purge_cruft(start);
+      cache_artwork_purge_cruft(start);
 
       DPRINTF(E_LOG, L_SCAN, "Bulk library scan completed in %.f sec\n", difftime(end, start));
 
@@ -1147,6 +1210,7 @@ filescanner(void *arg)
     DPRINTF(E_FATAL, L_SCAN, "Scan event loop terminated ahead of time!\n");
 
   db_perthread_deinit();
+  //artworkcache_perthread_deinit();
 
   pthread_exit(NULL);
 }
@@ -1335,6 +1399,7 @@ process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie
 
       db_file_delete_bypath(path);
       db_pl_delete_bypath(path);
+      cache_artwork_delete_by_path(path);
     }
 
   if (ie->mask & IN_MOVED_FROM)
@@ -1357,7 +1422,8 @@ process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie
 	{
 	  DPRINTF(E_LOG, L_SCAN, "File access to '%s' failed: %s\n", path, strerror(errno));
 
-	  db_file_delete_bypath(path);;
+	  db_file_delete_bypath(path);
+	  cache_artwork_delete_by_path(path);
 	}
       else if ((file_type_get(path) == FILE_REGULAR) && (db_file_id_bypath(path) <= 0)) // TODO Playlists
 	{
diff --git a/src/filescanner_ffmpeg.c b/src/filescanner_ffmpeg.c
index cec6b1a..c70a253 100644
--- a/src/filescanner_ffmpeg.c
+++ b/src/filescanner_ffmpeg.c
@@ -415,6 +415,15 @@ scan_metadata_ffmpeg(char *file, struct media_file_info *mfi)
   options = NULL;
 
 #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3)
+# ifndef HAVE_FFMPEG
+  // Without this, libav is slow to probe some internet streams
+  if (mfi->data_kind == 1)
+    {
+      ctx = avformat_alloc_context();
+      ctx->probesize = 64000;
+    }
+# endif
+
   if (mfi->data_kind == 1)
     av_dict_set(&options, "icy", "1", 0);
 
diff --git a/src/filescanner_itunes.c b/src/filescanner_itunes.c
index 174dfd6..2059505 100644
--- a/src/filescanner_itunes.c
+++ b/src/filescanner_itunes.c
@@ -34,7 +34,6 @@
 #include <stdint.h>
 #include <inttypes.h>
 
-#include <avl.h>
 #include <plist/plist.h>
 
 #ifdef HAVE_LIBEVENT2
@@ -43,6 +42,7 @@
 # include "evhttp/evhttp.h"
 #endif
 
+#include "avl/avl.h"
 #include "logger.h"
 #include "db.h"
 #include "filescanner.h"
@@ -281,10 +281,10 @@ find_track_file(char *location)
       return 0;
     }
 
-  plen = strlen("file://localhost/");
+  plen = strlen("file://");
 
   /* Not a local file ... */
-  if (strncmp(location, "file://localhost/", plen) != 0)
+  if (strncmp(location, "file://", plen) != 0)
     return 0;
 
   /* Now search for the library item where the path has closest match to playlist item */
diff --git a/src/httpd_daap.c b/src/httpd_daap.c
index 9506b6f..2a41945 100644
--- a/src/httpd_daap.c
+++ b/src/httpd_daap.c
@@ -40,8 +40,7 @@
 #include <uninorm.h>
 #include <unistd.h>
 
-#include <avl.h>
-
+#include "avl/avl.h"
 #include "logger.h"
 #include "db.h"
 #include "conffile.h"
@@ -52,7 +51,7 @@
 #include "httpd_daap.h"
 #include "daap_query.h"
 #include "dmap_common.h"
-#include "daap_cache.h"
+#include "cache.h"
 
 #ifdef HAVE_LIBEVENT2
 # include <event2/http_struct.h>
@@ -506,14 +505,31 @@ daap_sort_finalize(struct sort_ctx *ctx)
   dmap_add_int(ctx->headerlist, "mshn", ctx->misc_mshn); /* 12 */
 }
 
+/* Remotes are clients that will issue DACP commands. For these clients we will
+ * do the playback, and we will not stream to them. This is a crude function to
+ * identify them, so we can give them appropriate treatment.
+ */
+static int
+is_remote(const char *user_agent)
+{
+  if (!user_agent)
+    return 0;
+  if (strcasestr(user_agent, "remote"))
+    return 1;
+  if (strstr(user_agent, "Retune"))
+    return 1;
+
+  return 0;
+}
+
 /* We try not to return items that the client cannot play (like Spotify and
  * internet streams in iTunes), or which are inappropriate (like internet streams
- * in the album tab in Remote
+ * in the album tab of remotes)
  */
 static void
 user_agent_filter(const char *user_agent, struct query_params *qp)
 {
-  char *filter;
+  const char *filter;
   char *buffer;
   int len;
 
@@ -523,16 +539,10 @@ user_agent_filter(const char *user_agent, struct query_params *qp)
   // Valgrind doesn't like strlen(filter) below, so instead we allocate 128 bytes
   // to hold the string and the leading " AND ". Remember to adjust the 128 if
   // you define strings here that will be too large for the buffer.
-  if (strcasestr(user_agent, "itunes"))
-    filter = strdup("(f.data_kind = 0)"); // Only real files
-  else if (strcasestr(user_agent, "daap"))
-    filter = strdup("(f.data_kind = 0)"); // Only real files
-  else if (strcasestr(user_agent, "remote"))
-    filter = strdup("(f.data_kind <> 1)"); // No internet radio
-  else if (strcasestr(user_agent, "android"))
-    filter = strdup("(f.data_kind <> 1)"); // No internet radio
+  if (is_remote(user_agent))
+    filter = "(f.data_kind <> 1)"; // No internet radio
   else
-    return;
+    filter = "(f.data_kind = 0)"; // Only real files
 
   if (qp->filter)
     {
@@ -547,8 +557,6 @@ user_agent_filter(const char *user_agent, struct query_params *qp)
     qp->filter = strdup(filter);
 
   DPRINTF(E_DBG, L_DAAP, "SQL filter w/client mod: %s\n", qp->filter);
-
-  free(filter);
 }
 
 /* Returns eg /databases/1/containers from /databases/1/containers?meta=dmap.item... */
@@ -813,7 +821,15 @@ daap_reply_server_info(struct evhttp_request *req, struct evbuffer *evbuf, char
   dmap_add_int(content, "aeSV", apro);       // com.apple.itunes.music-sharing-version (determines if itunes shows share types)
 
   dmap_add_short(content, "ated", 7);        // daap.supportsextradata
-  dmap_add_short(content, "asgr", 3);        // daap.supportsgroups
+
+  /* Sub-optimal user-agent sniffing to solve the problem that iTunes 12.1
+   * does not work if we announce support for groups.
+   */ 
+  ua = evhttp_find_header(headers, "User-Agent");
+  if (ua && (strncmp(ua, "iTunes", strlen("iTunes")) == 0))
+    dmap_add_short(content, "asgr", 0);      // daap.supportsgroups (1=artists, 2=albums, 3=both)
+  else
+    dmap_add_short(content, "asgr", 3);      // daap.supportsgroups (1=artists, 2=albums, 3=both)
 
 //  dmap_add_long(content, "asse", 0x80000); // unknown - used by iTunes
 
@@ -848,6 +864,9 @@ daap_reply_server_info(struct evhttp_request *req, struct evbuffer *evbuf, char
 
   dmap_add_int(content, "msdc", 1);          // dmap.databasescount
 
+//  dmap_add_int(content, "mstc", );          // dmap.utctime
+//  dmap_add_int(content, "msto", );          // dmap.utcoffset
+
   // Create container
   dmap_add_container(evbuf, "msrv", EVBUFFER_LENGTH(content));
   evbuffer_add_buffer(evbuf, content);
@@ -1009,7 +1028,7 @@ daap_reply_update(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
   param = evhttp_find_header(query, "revision-number");
   if (!param)
     {
-      DPRINTF(E_LOG, L_DAAP, "Missing revision-number in client update request\n");
+      DPRINTF(E_DBG, L_DAAP, "Missing revision-number in client update request\n");
       /* Some players (Amarok, Banshee) don't supply a revision number.
 	 They get a standard update of everything. */
       param = "1";  /* Default to "1" will insure update */
@@ -1169,10 +1188,12 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
   struct sort_ctx *sctx;
   const char *param;
   const char *client_codecs;
+  char *last_codectype;
   char *tag;
   int nmeta;
   int sort_headers;
   int nsongs;
+  int remote;
   int transcode;
   int ret;
 
@@ -1299,19 +1320,40 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
       goto out_query_free;
     }
 
+  remote = is_remote(ua);
+
+  client_codecs = NULL;
+  if (!remote && req)
+    {
+      headers = evhttp_request_get_input_headers(req);
+      client_codecs = evhttp_find_header(headers, "Accept-Codecs");
+    }
+
   nsongs = 0;
+  last_codectype = NULL;
   while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id))
     {
       nsongs++;
 
-      client_codecs = NULL;
-      if (req)
+      if (!dbmfi.codectype)
+	{
+	  DPRINTF(E_LOG, L_DAAP, "Cannot transcode '%s', codec type is unknown\n", dbmfi.fname);
+
+	  transcode = 0;
+	}
+      else if (remote)
 	{
-	  headers = evhttp_request_get_input_headers(req);
-	  client_codecs = evhttp_find_header(headers, "Accept-Codecs");
+	  transcode = 1;
 	}
+      else if (!last_codectype || (strcmp(last_codectype, dbmfi.codectype) != 0))
+	{
+	  transcode = transcode_needed(ua, client_codecs, dbmfi.codectype);
+
+	  if (last_codectype)
+	    free(last_codectype);
 
-      transcode = transcode_needed(ua, client_codecs, dbmfi.codectype);
+	  last_codectype = strdup(dbmfi.codectype);
+	}
 
       ret = dmap_encode_file_metadata(songlist, song, &dbmfi, meta, nmeta, sort_headers, transcode);
       if (ret < 0)
@@ -1339,6 +1381,9 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
 
   DPRINTF(E_DBG, L_DAAP, "Done with song list, %d songs\n", nsongs);
 
+  if (last_codectype)
+    free(last_codectype);
+
   if (nmeta > 0)
     free(meta);
 
@@ -1913,7 +1958,10 @@ daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
   evbuffer_free(group);
 
   if (qp.filter)
-    free(qp.filter);
+    {
+        free(qp.filter);
+        qp.filter = NULL;
+    }
 
   if (ret < 0)
     {
@@ -1993,8 +2041,7 @@ daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
   evbuffer_free(grouplist);
 
  out_qfilter_free:
-  if (qp.filter)
-    free(qp.filter);
+  free(qp.filter);
 
   return -1;
 }
@@ -2255,9 +2302,9 @@ daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char *
     }
 
   if (strcmp(uri[2], "groups") == 0)
-    ret = artwork_get_group(id, max_w, max_h, ART_CAN_PNG | ART_CAN_JPEG, evbuf);
+    ret = artwork_get_group(id, max_w, max_h, evbuf);
   else if (strcmp(uri[2], "items") == 0)
-    ret = artwork_get_item(id, max_w, max_h, ART_CAN_PNG | ART_CAN_JPEG, evbuf);
+    ret = artwork_get_item(id, max_w, max_h, evbuf);
 
   switch (ret)
     {
@@ -2294,14 +2341,9 @@ daap_reply_extra_data(struct evhttp_request *req, struct evbuffer *evbuf, char *
 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;
   int ret;
 
-  s = daap_session_find(req, query, evbuf);
-  if (!s)
-    return -1;
-
   ret = safe_atoi32(uri[3], &id);
   if (ret < 0)
     evhttp_send_error(req, HTTP_BADREQUEST, "Bad Request");
@@ -2311,6 +2353,23 @@ daap_stream(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, stru
   return ret;
 }
 
+static char *
+uri_relative(char *uri, const char *protocol)
+{
+  char *ret;
+
+  if (strncmp(uri, protocol, strlen(protocol)) != 0)
+    return NULL;
+
+  ret = strchr(uri + strlen(protocol), '/');
+  if (!ret)
+    {
+      DPRINTF(E_LOG, L_DAAP, "Malformed DAAP Request URI '%s'\n", uri);
+      return NULL;
+    }
+
+  return ret;
+}
 
 static char *
 daap_fix_request_uri(struct evhttp_request *req, char *uri)
@@ -2319,25 +2378,21 @@ daap_fix_request_uri(struct evhttp_request *req, char *uri)
 
   /* iTunes 9 gives us an absolute request-uri like
    *  daap://10.1.1.20:3689/server-info
+   * iTunes 12.1 gives us an absolute request-uri for streaming like
+   *  http://10.1.1.20:3689/databases/1/items/1.mp3
    */
 
-  if (strncmp(uri, "daap://", strlen("daap://")) != 0)
-    return uri;
-
-  /* Clear the proxy request flag set by evhttp
-   * due to the request URI being absolute.
-   * It has side-effects on Connection: keep-alive
-   */
-  req->flags &= ~EVHTTP_PROXY_REQUEST;
-
-  ret = strchr(uri + strlen("daap://"), '/');
-  if (!ret)
+  if ( (ret = uri_relative(uri, "daap://")) || (ret = uri_relative(uri, "http://")) )
     {
-      DPRINTF(E_LOG, L_DAAP, "Malformed DAAP Request URI '%s'\n", uri);
-      return NULL;
+      /* Clear the proxy request flag set by evhttp
+       * due to the request URI being absolute.
+       * It has side-effects on Connection: keep-alive
+       */
+      req->flags &= ~EVHTTP_PROXY_REQUEST;
+      return ret;
     }
 
-  return ret;
+  return uri;
 }
 
 
@@ -2668,31 +2723,31 @@ daap_request(struct evhttp_request *req)
    */
   evhttp_add_header(headers, "Content-Type", "application/x-dmap-tagged");
 
-  // Try the cache
-  evbuf = daapcache_get(full_uri);
-  if (evbuf)
+  evbuf = evbuffer_new();
+  if (!evbuf)
     {
-      httpd_send_reply(req, HTTP_OK, "OK", evbuf); // TODO not all want this reply
+      DPRINTF(E_LOG, L_DAAP, "Could not allocate evbuffer for DAAP reply\n");
+
+      evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error");
 
-      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)
+  // Try the cache
+  ret = cache_daap_get(full_uri, evbuf);
+  if (ret == 0)
     {
-      DPRINTF(E_LOG, L_DAAP, "Could not allocate evbuffer for DAAP reply\n");
-
-      evhttp_send_error(req, HTTP_SERVUNAVAIL, "Internal Server Error");
+      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
   evhttp_parse_query(full_uri, &query);
 
   clock_gettime(CLOCK_MONOTONIC, &start);
@@ -2703,10 +2758,10 @@ daap_request(struct evhttp_request *req)
 
   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);
+  DPRINTF(E_DBG, L_DAAP, "DAAP request handled in %d milliseconds\n", msec);
 
-  if (msec > daapcache_threshold())
-    daapcache_add(full_uri, ua, msec);
+  if (msec > cache_daap_threshold())
+    cache_daap_add(full_uri, ua, msec);
 
   evhttp_clear_headers(&query);
   evbuffer_free(evbuf);
diff --git a/src/httpd_dacp.c b/src/httpd_dacp.c
index e24f61b..49d49d9 100644
--- a/src/httpd_dacp.c
+++ b/src/httpd_dacp.c
@@ -638,10 +638,13 @@ dacp_propset_userrating(const char *value, struct evkeyvalq *query)
       return;
     }
 
-  param = evhttp_find_header(query, "item-spec");
+  param = evhttp_find_header(query, "item-spec"); // Remote
+  if (!param)
+    param = evhttp_find_header(query, "song-spec"); // Retune
+
   if (!param)
     {
-      DPRINTF(E_LOG, L_DACP, "Missing item-spec parameter in dacp.userrating query\n");
+      DPRINTF(E_LOG, L_DACP, "Missing item-spec/song-spec parameter in dacp.userrating query\n");
 
       return;
     }
@@ -649,7 +652,7 @@ dacp_propset_userrating(const char *value, struct evkeyvalq *query)
   param = strchr(param, ':');
   if (!param)
     {
-      DPRINTF(E_LOG, L_DACP, "Malformed item-spec parameter in dacp.userrating query\n");
+      DPRINTF(E_LOG, L_DACP, "Malformed item-spec/song-spec parameter in dacp.userrating query\n");
 
       return;
     }
@@ -658,7 +661,7 @@ dacp_propset_userrating(const char *value, struct evkeyvalq *query)
   ret = safe_hextou32(param, &itemid);
   if (ret < 0)
     {
-      DPRINTF(E_LOG, L_DACP, "Couldn't convert item-spec to an integer in dacp.userrating (%s)\n", param);
+      DPRINTF(E_LOG, L_DACP, "Couldn't convert item-spec/song-spec to an integer in dacp.userrating (%s)\n", param);
 
       return;
     }
@@ -1403,6 +1406,8 @@ dacp_reply_playqueueedit_add(struct evhttp_request *req, struct evbuffer *evbuf,
   // -> mode=0: add to end of playqueue
   //?command=add&query='dmap.itemid:306'&queuefilter=album:6525753023700533274&sort=album&mode=1&session-id=100
   // -> mode 1: stop playblack, clear playqueue, add songs to playqueue
+  //?command=add&query='dmap.itemid:2'&query-modifier=containers&sort=name&mode=2&session-id=100
+  // -> mode 2: stop playblack, clear playqueue, add shuffled songs from playlist=itemid to playqueue
 
   struct player_source *ps;
   const char *editquery;
@@ -1802,7 +1807,7 @@ dacp_reply_nowplayingartwork(struct evhttp_request *req, struct evbuffer *evbuf,
   if (ret < 0)
     goto no_artwork;
 
-  ret = artwork_get_item(id, max_w, max_h, ART_CAN_PNG | ART_CAN_JPEG, evbuf);
+  ret = artwork_get_item(id, max_w, max_h, evbuf);
   switch (ret)
     {
       case ART_FMT_PNG:
@@ -2091,12 +2096,8 @@ dacp_reply_setspeakers(struct evhttp_request *req, struct evbuffer *evbuf, char
   do
     {
       param++;
-      /* Hyperfine Remote for Android will send in decimal, others use hex */
-      if ((strlen(param) > 1) && (param[1] != 'x'))
-	ret = safe_atou64(param, &ids[i]);
-      else
-	ret = safe_hextou64(param, &ids[i]);
 
+      ret = safe_hextou64(param, &ids[i]);
       if (ret < 0)
 	{
 	  DPRINTF(E_LOG, L_DACP, "Invalid speaker id in request: %s\n", param);
diff --git a/src/laudio_alsa.c b/src/laudio_alsa.c
index c56d7b2..6234805 100644
--- a/src/laudio_alsa.c
+++ b/src/laudio_alsa.c
@@ -281,8 +281,9 @@ laudio_write(uint8_t *buf, uint64_t rtptime)
 	}
 
       /* Don't let ALSA fill up the buffer too much */
-      if (nsamp == AIRTUNES_V2_PACKET_SAMPLES)
-	return;
+// Disabled - seems to cause buffer underruns
+//      if (nsamp == AIRTUNES_V2_PACKET_SAMPLES)
+//	return;
     }
 }
 
diff --git a/src/laudio_oss4.c b/src/laudio_oss4.c
index 7c2baea..1c21249 100644
--- a/src/laudio_oss4.c
+++ b/src/laudio_oss4.c
@@ -32,7 +32,7 @@
 #include <fcntl.h>
 #include <sys/ioctl.h>
 
-#include <soundcard.h>
+#include <sys/soundcard.h>
 
 #include "conffile.h"
 #include "logger.h"
@@ -280,6 +280,7 @@ int
 laudio_open(void)
 {
   audio_buf_info bi;
+  oss_sysinfo si;
   int scratch;
   int ret;
 
@@ -291,6 +292,14 @@ laudio_open(void)
       return -1;
     }
 
+  ret = ioctl(oss_fd, SNDCTL_SYSINFO, &si);
+  if ((ret < 0) || (si.versionnum < 0x040000))
+    {
+      DPRINTF(E_LOG, L_LAUDIO, "Your OSS version (%s) is unavailable or too old; version 4.0.0+ is required\n", si.version);
+
+      goto out_fail;
+    }
+
   scratch = 0;
   ret = ioctl(oss_fd, SNDCTL_DSP_SETTRIGGER, &scratch);
   if (ret < 0)
@@ -390,41 +399,10 @@ laudio_close(void)
 int
 laudio_init(laudio_status_cb cb)
 {
-  oss_sysinfo si;
-  int ret;
-
   status_cb = cb;
-  pcm_status = LAUDIO_CLOSED;
 
   card_name = cfg_getstr(cfg_getsec(cfg, "audio"), "card");
 
-  oss_fd = open(card_name, O_RDWR);
-  if (oss_fd < 0)
-    {
-      DPRINTF(E_FATAL, L_LAUDIO, "Could not open sound device: %s\n", strerror(errno));
-
-      return -1;
-    }
-
-  ret = ioctl(oss_fd, SNDCTL_SYSINFO, &si);
-
-  close(oss_fd);
-  oss_fd = -1;
-
-  if (ret < 0)
-    {
-      DPRINTF(E_FATAL, L_LAUDIO, "Could not check OSS version: %s\n", strerror(errno));
-
-      return -1;
-    }
-
-  if (si.versionnum < 0x040000)
-    {
-      DPRINTF(E_FATAL, L_LAUDIO, "Your OSS version (%s) is too old; version 4.0.0+ is required\n", si.version);
-
-      return -1;
-    }
-
   return 0;
 }
 
diff --git a/src/logger.c b/src/logger.c
index 00242ed..a499c95 100644
--- a/src/logger.c
+++ b/src/logger.c
@@ -43,7 +43,8 @@ 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", "lastfm", "dcache" };
+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", "cache" };
+static char *severities[] = { "FATAL", "LOG", "WARN", "INFO", "DEBUG", "SPAM" };
 
 
 static int
@@ -104,7 +105,7 @@ vlogger(int severity, int domain, const char *fmt, va_list args)
       if (ret == 0)
 	stamp[0] = '\0';
 
-      fprintf(logfile, "[%s] %8s: ", stamp, labels[domain]);
+      fprintf(logfile, "[%s] [%5s] %8s: ", stamp, severities[severity], labels[domain]);
 
       va_copy(ap, args);
       vfprintf(logfile, fmt, ap);
@@ -115,7 +116,7 @@ vlogger(int severity, int domain, const char *fmt, va_list args)
 
   if (console)
     {
-      fprintf(stderr, "%8s: ", labels[domain]);
+      fprintf(stderr, "[%5s] %8s: ", severities[severity], labels[domain]);
 
       va_copy(ap, args);
       vfprintf(stderr, fmt, ap);
diff --git a/src/logger.h b/src/logger.h
index 180f57d..a203dc8 100644
--- a/src/logger.h
+++ b/src/logger.h
@@ -28,7 +28,7 @@
 #define L_DBPERF  19
 #define L_SPOTIFY 20
 #define L_LASTFM  21
-#define L_DCACHE  22
+#define L_CACHE   22
 
 #define N_LOGDOMAINS  23
 
diff --git a/src/main.c b/src/main.c
index d7e5c63..2240c61 100644
--- a/src/main.c
+++ b/src/main.c
@@ -58,7 +58,7 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL;
 #include "db.h"
 #include "logger.h"
 #include "misc.h"
-#include "daap_cache.h"
+#include "cache.h"
 #include "filescanner.h"
 #include "httpd.h"
 #include "mdns.h"
@@ -675,11 +675,11 @@ main(int argc, char **argv)
       goto db_fail;
     }
 
-  /* Spawn DAAP cache thread */
-  ret = daapcache_init();
+  /* Spawn cache thread */
+  ret = cache_init();
   if (ret != 0)
     {
-      DPRINTF(E_FATAL, L_MAIN, "DAAP cache thread failed to start\n");
+      DPRINTF(E_FATAL, L_MAIN, "Cache thread failed to start\n");
 
       ret = EXIT_FAILURE;
       goto cache_fail;
@@ -824,8 +824,8 @@ main(int argc, char **argv)
   filescanner_deinit();
 
  filescanner_fail:
-  DPRINTF(E_LOG, L_MAIN, "DAAP cache deinit\n");
-  daapcache_deinit();
+  DPRINTF(E_LOG, L_MAIN, "Cache deinit\n");
+  cache_deinit();
 
  cache_fail:
   DPRINTF(E_LOG, L_MAIN, "Database deinit\n");
diff --git a/src/misc.c b/src/misc.c
index 24762e9..246d4a6 100644
--- a/src/misc.c
+++ b/src/misc.c
@@ -119,6 +119,10 @@ safe_hextou32(const char *str, uint32_t *val)
   char *end;
   unsigned long intval;
 
+  /* A hex shall begin with 0x */
+  if (strncmp(str, "0x", 2) != 0)
+    return safe_atou32(str, val);
+
   errno = 0;
   intval = strtoul(str, &end, 16);
 
diff --git a/src/player.c b/src/player.c
index a181ca5..4750e64 100644
--- a/src/player.c
+++ b/src/player.c
@@ -1069,7 +1069,10 @@ source_shuffle(struct player_source *head, struct player_source *tail)
 
   // Count items in queue (excluding head and tail)
   ps = head->shuffle_next;
-  nitems = 0;
+  if (!cur_streaming)
+    nitems = 1;
+  else
+    nitems = 0;
   while (ps != tail)
     {
       nitems++;
@@ -1089,7 +1092,10 @@ source_shuffle(struct player_source *head, struct player_source *tail)
     }
 
   // Fill array with items in queue (excluding head and tail)
-  ps = head->shuffle_next;
+  if (cur_streaming)
+    ps = head->shuffle_next;
+  else
+    ps = head;
   i = 0;
   do
     {
@@ -1114,10 +1120,19 @@ source_shuffle(struct player_source *head, struct player_source *tail)
     }
 
   // 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];
+  if (cur_streaming)
+    {
+      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];
+    }
+  else
+    {
+      ps_array[0]->shuffle_prev = ps_array[nitems - 1];
+      ps_array[nitems - 1]->shuffle_next = ps_array[0];
+      shuffle_head = ps_array[0];
+    }
 
   free(ps_array);
 
@@ -1132,6 +1147,8 @@ source_reshuffle(void)
 
   if (cur_streaming)
     head = cur_streaming;
+  else if (shuffle)
+    head = shuffle_head;
   else
     head = source_head;
 
@@ -2527,12 +2544,7 @@ playback_start(struct player_command *cmd)
     {
       ret = laudio_open();
       if (ret < 0)
-	{
-	  DPRINTF(E_LOG, L_PLAYER, "Could not open local audio\n");
-
-	  playback_abort();
-	  return -1;
-	}
+	DPRINTF(E_LOG, L_PLAYER, "Could not open local audio, will try AirPlay\n");
     }
 
   /* Start RAOP sessions on selected devices if needed */
diff --git a/src/raop.c b/src/raop.c
index bf77f8b..2309eb7 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(id, 600, 600, ART_CAN_PNG | ART_CAN_JPEG, rmd->artwork);
+  ret = artwork_get_item(id, 600, 600, 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);
@@ -1162,57 +1162,15 @@ raop_add_headers(struct raop_session *rs, struct evrtsp_request *req, enum evrts
   return 0;
 }
 
-static int
-raop_grab_cseq(struct evkeyvalq *headers)
-{
-  const char *param;
-  int cseq;
-  int ret;
-
-  param = evrtsp_find_header(headers, "CSeq");
-  if (!param)
-    return -1;
-
-  ret = safe_atoi32(param, &cseq);
-  if (ret < 0)
-    {
-      DPRINTF(E_LOG, L_RAOP, "Could not convert CSeq value to integer (%s)\n", param);
-
-      return -1;
-    }
-
-  return cseq;
-}
-
+/* This check should compare the reply CSeq with the request CSeq, but it has
+ * been removed because RAOP targets like Reflector and AirFoil don't return
+ * the CSeq according to the rtsp spec, and the CSeq is not really important
+ * anyway.
+ */
 static int
 raop_check_cseq(struct raop_session *rs, struct evrtsp_request *req)
 {
-  int reply_cseq;
-  int request_cseq;
-
-  reply_cseq = raop_grab_cseq(req->input_headers);
-  /* AirFoil won't return cseq, so skip the check */
-  if (reply_cseq < 0)
-    {
-      DPRINTF(E_INFO, L_RAOP, "No CSeq in reply, skipping check\n");
-
-      return 0;
-    }
-
-  request_cseq = raop_grab_cseq(req->output_headers);
-  if (request_cseq < 0)
-    {
-      DPRINTF(E_LOG, L_RAOP, "No CSeq in request\n");
-
-      return -1;
-    }
-
-  if (reply_cseq == request_cseq)
-    return 0;
-
-  DPRINTF(E_LOG, L_RAOP, "Reply CSeq does not match request CSeq: got %d expected %d\n", reply_cseq, request_cseq);
-
-  return -1;
+  return 0;
 }
 
 static int
@@ -2290,7 +2248,8 @@ raop_set_volume_internal(struct raop_session *rs, int volume, evrtsp_req_cb cb)
   raop_volume = raop_volume_convert(volume, rs->devname);
 
   /* Don't let locales get in the way here */
-  ret = evbuffer_add_printf(evbuf, "volume: %d.%06d\r\n", (int)raop_volume, -(int)(1000000.0 * (raop_volume - (int)raop_volume)));
+  /* We use -%d and -(int)raop_volume so -0.3 won't become 0.3 */
+  ret = evbuffer_add_printf(evbuf, "volume: -%d.%06d\r\n", -(int)raop_volume, -(int)(1000000.0 * (raop_volume - (int)raop_volume)));
   if (ret < 0)
     {
       DPRINTF(E_LOG, L_RAOP, "Out of memory for SET_PARAMETER payload (volume)\n");
diff --git a/src/spotify.c b/src/spotify.c
index 611975e..dc82c75 100644
--- a/src/spotify.c
+++ b/src/spotify.c
@@ -46,6 +46,9 @@
 #include "filescanner.h"
 
 
+/* How long to wait for audio (in sec) before giving up */
+#define SPOTIFY_TIMEOUT 20
+
 /* --- Types --- */
 typedef struct audio_fifo_data
 {
@@ -116,6 +119,10 @@ struct spotify_command
 // Spotify thread
 static pthread_t tid_spotify;
 
+// Used to make sure no login is attempted before the logout cb from Spotify
+static pthread_mutex_t login_lck;
+static pthread_cond_t login_cond;
+
 // Event base, pipes and events
 struct event_base *evbase_spotify;
 static int g_exit_pipe[2];
@@ -951,6 +958,7 @@ audio_get(struct spotify_command *cmd)
   struct timespec ts;
   audio_fifo_data_t *afd;
   int processed;
+  int timeout;
   int ret;
   int s;
 
@@ -975,9 +983,11 @@ audio_get(struct spotify_command *cmd)
 
       // If buffer is empty, wait for audio, but use timed wait so we don't
       // risk waiting forever (maybe the player stopped while we were waiting)
+      timeout = 0;
       while ( !(afd = TAILQ_FIRST(&g_audio_fifo->q)) && 
 	       (g_state != SPOTIFY_STATE_STOPPED) &&
-	       (g_state != SPOTIFY_STATE_STOPPING) )
+	       (g_state != SPOTIFY_STATE_STOPPING) &&
+	       (timeout < SPOTIFY_TIMEOUT) )
 	{
 	  DPRINTF(E_DBG, L_SPOTIFY, "Waiting for audio\n");
 #if _POSIX_TIMERS > 0
@@ -988,10 +998,18 @@ audio_get(struct spotify_command *cmd)
 	  TIMEVAL_TO_TIMESPEC(&tv, &ts);
 #endif
 	  ts.tv_sec += 5;
+	  timeout += 5;
 
 	  pthread_cond_timedwait(&g_audio_fifo->cond, &g_audio_fifo->mutex, &ts);
 	}
 
+      if ((!afd) && (timeout >= SPOTIFY_TIMEOUT))
+	{
+	  DPRINTF(E_LOG, L_SPOTIFY, "Timeout waiting for audio (waited %d sec)\n", timeout);
+
+	  spotify_playback_stop_nonblock();
+	}
+
       if (!afd)
 	break;
 
@@ -1058,11 +1076,11 @@ artwork_get(struct spotify_command *cmd)
       goto level2_exit;
     }
 
-  // TODO rescale
+  // Get an image at least the same size as requested
   image_size = SP_IMAGE_SIZE_SMALL; // 64x64
-  if ((cmd->arg.artwork.max_w > 200) && (cmd->arg.artwork.max_h > 200))
+  if ((cmd->arg.artwork.max_w > 64) || (cmd->arg.artwork.max_h > 64))
     image_size = SP_IMAGE_SIZE_NORMAL; // 300x300
-  if ((cmd->arg.artwork.max_w > 500) && (cmd->arg.artwork.max_h > 500))
+  if ((cmd->arg.artwork.max_w > 300) || (cmd->arg.artwork.max_h > 300))
     image_size = SP_IMAGE_SIZE_LARGE; // 640x640
 
   image_id = fptr_sp_album_cover(album, image_size);
@@ -1151,7 +1169,6 @@ logged_in(sp_session *sess, sp_error error)
   if (SP_ERROR_OK != error)
     {
       DPRINTF(E_LOG, L_SPOTIFY, "Login failed: %s\n",	fptr_sp_error_message(error));
-      thread_exit();
       return;
     }
 
@@ -1173,6 +1190,24 @@ logged_in(sp_session *sess, sp_error error)
 }
 
 /**
+ * Called when logout has been processed.
+ * Either called explicitly if you initialize a logout operation, or implicitly
+ * if there is a permanent connection error
+ *
+ * @sa sp_session_callbacks#logged_out
+ */
+static void
+logged_out(sp_session *sess)
+{
+  DPRINTF(E_INFO, L_SPOTIFY, "Logout complete\n");
+
+  pthread_mutex_lock(&login_lck);
+
+  pthread_cond_signal(&login_cond);
+  pthread_mutex_unlock(&login_lck);
+}
+
+/**
  * This callback is used from libspotify whenever there is PCM data available.
  *
  * @sa sp_session_callbacks#music_delivery
@@ -1316,6 +1351,7 @@ static void end_of_track(sp_session *sess)
  */
 static sp_session_callbacks session_callbacks = {
   .logged_in = &logged_in,
+  .logged_out = &logged_out,
   .connectionstate_updated = &connectionstate_updated,
   .notify_main_thread = &notify_main_thread,
   .music_delivery = &music_delivery,
@@ -1556,7 +1592,7 @@ spotify_playback_stop(void)
   return ret;
 }
 
-/* Thread: libspotify */
+/* Thread: player and libspotify */
 void
 spotify_playback_stop_nonblock(void)
 {
@@ -1751,45 +1787,54 @@ spotify_file_read(char *path, char **username, char **password)
 void
 spotify_login(char *path)
 {
+  sp_error err;
   char *username;
   char *password;
   int ret;
-  sp_error err;
 
   if (!g_sess)
     {
-      DPRINTF(E_LOG, L_SPOTIFY, "Can't login! No valid Spotify session.\n");
+      if (!g_libhandle)
+	DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - could not find libspotify\n");
+      else
+	DPRINTF(E_LOG, L_SPOTIFY, "Can't login! - no valid Spotify session\n");
+
       return;
     }
 
-  /* Log out if thread already running */
-  if (g_state != SPOTIFY_STATE_INACTIVE)
+  if (SP_CONNECTION_STATE_LOGGED_IN == fptr_sp_session_connectionstate(g_sess))
     {
-      DPRINTF(E_LOG, L_SPOTIFY, "Existing login terminating (state %d)\n", g_state);
+      pthread_mutex_lock(&login_lck);
 
-      thread_exit();
+      DPRINTF(E_LOG, L_SPOTIFY, "Logging out of Spotify (current state is %d)\n", g_state);
 
-      ret = pthread_join(tid_spotify, NULL);
-      if (ret != 0)
+      fptr_sp_session_player_unload(g_sess);
+      err = fptr_sp_session_logout(g_sess);
+
+      if (SP_ERROR_OK != err)
 	{
-	  DPRINTF(E_FATAL, L_SPOTIFY, "Could not join Spotify thread: %s\n", strerror(errno));
+	  DPRINTF(E_LOG, L_SPOTIFY, "Could not logout of Spotify: %s\n", fptr_sp_error_message(err));
+	  pthread_mutex_unlock(&login_lck);
 	  return;
 	}
+
+      pthread_cond_wait(&login_cond, &login_lck);
+      pthread_mutex_unlock(&login_lck);
     }
 
-  /* Log in */
+  DPRINTF(E_INFO, L_SPOTIFY, "Logging into Spotify\n");
   if (path)
     {
       ret = spotify_file_read(path, &username, &password);
       if (ret < 0)
 	return;
 
-      DPRINTF(E_INFO, L_SPOTIFY, "Logging into Spotify\n");
       err = fptr_sp_session_login(g_sess, username, password, 1, NULL);
+      free(username);
+      free(password);
     }
   else
     {
-      DPRINTF(E_INFO, L_SPOTIFY, "Logging into Spotify\n");
       err = fptr_sp_session_relogin(g_sess);
     }
 
@@ -1798,18 +1843,8 @@ spotify_login(char *path)
       DPRINTF(E_LOG, L_SPOTIFY, "Could not login into Spotify: %s\n", fptr_sp_error_message(err));
       return;
     }
-
-  /* Spawn thread */
-  ret = pthread_create(&tid_spotify, NULL, spotify, NULL);
-  if (ret < 0)
-    {
-      DPRINTF(E_LOG, L_SPOTIFY, "Could not spawn Spotify thread: %s\n", strerror(errno));
-
-      return;
-    }
 }
 
-
 /* Thread: main */
 int
 spotify_init(void)
@@ -1967,9 +2002,28 @@ spotify_init(void)
   pthread_mutex_init(&g_audio_fifo->mutex, NULL);
   pthread_cond_init(&g_audio_fifo->cond, NULL);
 
+  pthread_mutex_init(&login_lck, NULL);
+  pthread_cond_init(&login_cond, NULL);
+
+  /* Spawn thread */
+  ret = pthread_create(&tid_spotify, NULL, spotify, NULL);
+  if (ret < 0)
+    {
+      DPRINTF(E_FATAL, L_SPOTIFY, "Could not spawn Spotify thread: %s\n", strerror(errno));
+      goto thread_fail;
+    }
+
   DPRINTF(E_DBG, L_SPOTIFY, "Spotify init complete\n");
   return 0;
 
+ thread_fail:
+  pthread_cond_destroy(&login_cond);
+  pthread_mutex_destroy(&login_lck);
+
+  pthread_cond_destroy(&g_audio_fifo->cond);
+  pthread_mutex_destroy(&g_audio_fifo->mutex);
+  free(g_audio_fifo);  
+
  audio_fifo_fail:
   fptr_sp_session_release(g_sess);
   g_sess = NULL;
@@ -2035,6 +2089,10 @@ spotify_deinit(void)
   close(g_exit_pipe[0]);
   close(g_exit_pipe[1]);
 
+  /* Destroy locks */
+  pthread_cond_destroy(&login_cond);
+  pthread_mutex_destroy(&login_lck);
+
   /* Clear audio fifo */
   pthread_cond_destroy(&g_audio_fifo->cond);
   pthread_mutex_destroy(&g_audio_fifo->mutex);
diff --git a/src/transcode.c b/src/transcode.c
index 4806c36..a1269c0 100644
--- a/src/transcode.c
+++ b/src/transcode.c
@@ -498,6 +498,15 @@ transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t
   memset(ctx, 0, sizeof(struct transcode_ctx));
 
 #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3)
+# ifndef HAVE_FFMPEG
+  // Without this, libav is slow to probe some internet streams, which leads to RAOP timeouts
+  if (mfi->data_kind == 1)
+    {
+      ctx->fmtctx = avformat_alloc_context();
+      ctx->fmtctx->probesize = 64000;
+    }
+# endif
+
   ret = avformat_open_input(&ctx->fmtctx, mfi->path, NULL, NULL);
 #else
   ret = av_open_input_file(&ctx->fmtctx, mfi->path, NULL, 0, NULL);
@@ -767,7 +776,6 @@ transcode_cleanup(struct transcode_ctx *ctx)
   free(ctx);
 }
 
-
 int
 transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype)
 {
@@ -776,13 +784,9 @@ transcode_needed(const char *user_agent, const char *client_codecs, char *file_c
   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");
+      DPRINTF(E_LOG, L_XCODE, "Can't determine transcode status, codec type is unknown\n");
       return -1;
     }
 

-- 
forked-daapd packaging



More information about the pkg-multimedia-commits mailing list