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