Add support for Spotify (squashed commit), and:

- Try to not return items which a client can't play
    - Remove inotify subscription to IN_MODIFY and IN_CREATE
    - Fix crash on unknown codec type in transcode.c
    - Probably added some new bugs...
This commit is contained in:
ejurgensen 2014-03-11 23:20:29 +01:00
parent 190f91114e
commit 7ed6cc98c3
24 changed files with 2203 additions and 171 deletions

28
INSTALL
View File

@ -97,6 +97,8 @@ Libraries:
from <http://developer.kde.org/~wheeler/taglib.html> from <http://developer.kde.org/~wheeler/taglib.html>
- libplist 0.16+ (optional - iTunes XML support) - libplist 0.16+ (optional - iTunes XML support)
from <http://github.com/JonathanBeck/libplist/downloads> from <http://github.com/JonathanBeck/libplist/downloads>
- libspotify (optional - Spotify support)
from <https://developer.spotify.com>
If using binary packages, remember that you need the development packages to If using binary packages, remember that you need the development packages to
build forked-daapd (usually named -dev or -devel). build forked-daapd (usually named -dev or -devel).
@ -143,14 +145,22 @@ needed.
To display the configure options run ./configure --help To display the configure options run ./configure --help
FLAC and Musepack support are optional. If not enabled, metadata extraction Support for Spotify is optional. Use --enable-spotify to enable this feature.
will fail on these files. If you enable this feature libspotify/api.h is required at compile time.
Forked-daapd uses runtime dynamic linking to the libspotify library, so even
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 iTunes Music Library XML format is optional. Use --enable-itunes Support for iTunes Music Library XML format is optional. Use --enable-itunes
to enable this feature. to enable this feature.
FLAC and Musepack support are optional, and they are probably only required
if your version of libav/ffmpeg is very old (version 0.5 or 0.6). Use
--enable-flac and --enable-musepack to enable.
Recommended build settings: Recommended build settings:
./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-flac ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var
After configure run the usual make, and if that went well, sudo make install After configure run the usual make, and if that went well, sudo make install
@ -189,12 +199,14 @@ The LSB header below sums it up:
# Required-Start: $local_fs $remote_fs $network $time # Required-Start: $local_fs $remote_fs $network $time
# Required-Stop: $local_fs $remote_fs $network $time # Required-Stop: $local_fs $remote_fs $network $time
# Should-Start: avahi # Should-Start: avahi
# Should-Stop: avahi
# Default-Start: 2 3 4 5 # Default-Start: 2 3 4 5
# Default-Stop: 0 1 6 # Default-Stop: 0 1 6
# Short-Description: media server with support for RSP, DAAP, DACP and AirPlay # Short-Description: DAAP/DACP (iTunes) server, support for AirPlay and Spotify
# Description: forked-daapd is an iTunes-compatible media server for # Description: forked-daapd is an iTunes-compatible media server for
# sharing your media library over the local network with RSP # sharing your media library over the local network with DAAP
# clients like the SoundBridge from Roku and DAAP clients # clients like iTunes. Like iTunes, it can be controlled by
# like iTunes. It can also stream music to AirPlay devices, # Apple Remote (and compatibles) and stream music directly to
# and it can be controlled by Apple Remote (and compatibles). # AirPlay devices. It also supports streaming to RSP clients
# (Roku devices) and streaming from Spotify.
### END INIT INFO ### END INIT INFO

View File

@ -8,5 +8,5 @@ SUBDIRS = sqlext src
man_MANS = forked-daapd.8 man_MANS = forked-daapd.8
install-data-hook: install-data-hook:
$(MKDIR_P) $(DESTDIR)$(localstatedir)/cache/forked-daapd $(MKDIR_P) $(DESTDIR)$(localstatedir)/cache/forked-daapd/libspotify

64
README
View File

@ -1,8 +1,10 @@
forked-daapd forked-daapd
------------ ------------
forked-daapd is a DAAP (iTunes) and RSP (Roku) media server, with support for forked-daapd is a Linux/FreeBSD DAAP (iTunes) and RSP (Roku) media server.
Linux and FreeBSD.
It has support for AirPlay devices/speakers, Apple Remote (and compatibles),
internet radio and Spotify.
DAAP stands for Digital Audio Access Protocol, and is the protocol used DAAP stands for Digital Audio Access Protocol, and is the protocol used
by iTunes and friends to share/stream media libraries over the network. by iTunes and friends to share/stream media libraries over the network.
@ -21,6 +23,21 @@ The original (now unmaintained) source can be found here:
forked-daapd is a complete rewrite of mt-daapd (Firefly Media Server). forked-daapd is a complete rewrite of mt-daapd (Firefly Media Server).
Contents of this README
-----------------------
- Supported clients
- Using Remote
- AirPlay devices/speakers
- Local audio output
- Supported formats
- Streaming MPEG4
- Playlists and internet radio
- Artwork
- Library
- Spotify
Supported clients Supported clients
----------------- -----------------
@ -198,12 +215,15 @@ happily write the metadata back at the end of the file after you've modified
them. Watch out for that. them. Watch out for that.
Playlists Playlists and internet radio
--------- ----------------------------
forked-daapd supports M3U playlists. Just drop your playlist somewhere in forked-daapd supports M3U playlists. Just drop your playlist somewhere in
your library with an .m3u extension and it will pick it up. your library with an .m3u extension and it will pick it up.
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.
Support for iTunes Music Library XML format is available as a compile-time 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 option. By default, metadata from our parsers is preferred over what's in
the iTunes DB; use itunes_overrides = true if you prefer iTunes' metadata. the iTunes DB; use itunes_overrides = true if you prefer iTunes' metadata.
@ -289,3 +309,39 @@ so changes won't be noticed unless the file happens to be in a directory that
is monitored. is monitored.
Bottom line: symlinks are for directories only. Bottom line: symlinks are for directories only.
Spotify
-------
forked-daapd has *some* support for Spotify. It must be compiled with the
--enable-spotify option (see INSTALL). You must have also have libspotify
installed, otherwise the Spotify integration will not be available. You can
get libspotify here:
- Original (binary) tar.gz, see <https://developer.spotify.com>
- Debian package (libspotify12), see <https://apt.mopidy.com>
You must also have a Spotify premium account.
The procedure for logging in to Spotify is very much like the Remote pairing
procedure. You must prepare a file, which should have the ending ".spotify".
The file must have two lines: The first is your Spotify user name, and the
second is your password. Move the file to your forked-daapd library.
Forked-daapd will then log in and add all the music in your Spotify playlists
to its database.
Spotify will automatically notify forked-daapd about playlist updates, so you
should not need to restart forked-daapd to syncronize with Spotify.
For safety you should delete the ".spotify" file after first login. Forked-daapd
will not store your password, but will still be able to log you in automatically
afterwards, because libspotify saves a login token. You can configure the
location of your Spotify user data in the configuration file.
Limitations: You will only be able to play tracks from your Spotify playlists,
so you can't search and listen to music from the rest of the Spotify catalogue.
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. Finally, Spotify artwork is not currently supported.

View File

@ -60,9 +60,14 @@ AC_ARG_ENABLE(itunes, AC_HELP_STRING([--enable-itunes], [Enable iTunes library s
use_itunes=true; use_itunes=true;
CPPFLAGS="${CPPFLAGS} -DITUNES") CPPFLAGS="${CPPFLAGS} -DITUNES")
AC_ARG_ENABLE(spotify, AC_HELP_STRING([--enable-spotify], [Enable Spotify library support]),
use_spotify=true;
CPPFLAGS="${CPPFLAGS} -DSPOTIFY")
AM_CONDITIONAL(COND_FLAC, test x$use_flac = xtrue) AM_CONDITIONAL(COND_FLAC, test x$use_flac = xtrue)
AM_CONDITIONAL(COND_MUSEPACK, test x$use_musepack = xtrue) AM_CONDITIONAL(COND_MUSEPACK, test x$use_musepack = xtrue)
AM_CONDITIONAL(COND_ITUNES, test x$use_itunes = xtrue) AM_CONDITIONAL(COND_ITUNES, test x$use_itunes = xtrue)
AM_CONDITIONAL(COND_SPOTIFY, test x$use_spotify = xtrue)
AC_ARG_WITH(oss4, AC_HELP_STRING([--with-oss4=includedir], [Use OSS4 with soundcard.h in includedir (default /usr/lib/oss/include/sys)]), AC_ARG_WITH(oss4, AC_HELP_STRING([--with-oss4=includedir], [Use OSS4 with soundcard.h in includedir (default /usr/lib/oss/include/sys)]),
[ case "$withval" in [ case "$withval" in
@ -184,6 +189,16 @@ if test x$use_itunes = xtrue; then
PKG_CHECK_MODULES(LIBPLIST, [ libplist >= 0.16 ]) PKG_CHECK_MODULES(LIBPLIST, [ libplist >= 0.16 ])
fi fi
if test x$use_spotify = xtrue; then
AC_CHECK_HEADER(libspotify/api.h, , AC_MSG_ERROR([libspotify/api.h not found]))
AC_DEFINE(HAVE_SPOTIFY_H, 1, [Define to 1 if you have the <libspotify/api.h> header file.])
dnl Don't link to libspotify, but instead enable dynamic linking
SPOTIFY_CFLAGS="-rdynamic"
SPOTIFY_LIBS="-ldl"
AC_SUBST(SPOTIFY_CFLAGS)
AC_SUBST(SPOTIFY_LIBS)
fi
case "$host" in case "$host" in
*-*-linux-*) *-*-linux-*)
if test x$use_oss4 != xtrue; then if test x$use_oss4 != xtrue; then

View File

@ -113,3 +113,11 @@ audio {
# AirPlay password # AirPlay password
# password = "s1kr3t" # password = "s1kr3t"
#} #}
# Spotify settings (only have effect if Spotify enabled - see README/INSTALL)
spotify {
# Directory where user settings should be stored (credentials)
# settings_dir = "/var/cache/forked-daapd/libspotify"
# Cache directory
# cache_dir = "/tmp"
}

View File

@ -13,6 +13,10 @@ if COND_ITUNES
ITUNESSRC=filescanner_itunes.c ITUNESSRC=filescanner_itunes.c
endif endif
if COND_SPOTIFY
SPOTIFYSRC=spotify.c spotify.h
endif
if COND_ALSA if COND_ALSA
ALSASRC=laudio_alsa.c ALSASRC=laudio_alsa.c
endif endif
@ -59,13 +63,13 @@ forked_daapd_CPPFLAGS = -D_GNU_SOURCE \
forked_daapd_CFLAGS = \ forked_daapd_CFLAGS = \
@ZLIB_CFLAGS@ @AVAHI_CFLAGS@ @SQLITE3_CFLAGS@ @LIBAV_CFLAGS@ \ @ZLIB_CFLAGS@ @AVAHI_CFLAGS@ @SQLITE3_CFLAGS@ @LIBAV_CFLAGS@ \
@CONFUSE_CFLAGS@ @TAGLIB_CFLAGS@ @MINIXML_CFLAGS@ @LIBPLIST_CFLAGS@ \ @CONFUSE_CFLAGS@ @TAGLIB_CFLAGS@ @MINIXML_CFLAGS@ @LIBPLIST_CFLAGS@ \
@LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @SPOTIFY_CFLAGS@
forked_daapd_LDADD = -lrt \ forked_daapd_LDADD = -lrt \
@ZLIB_LIBS@ @AVAHI_LIBS@ @SQLITE3_LIBS@ @LIBAV_LIBS@ \ @ZLIB_LIBS@ @AVAHI_LIBS@ @SQLITE3_LIBS@ @LIBAV_LIBS@ \
@CONFUSE_LIBS@ @FLAC_LIBS@ @TAGLIB_LIBS@ @LIBEVENT_LIBS@ \ @CONFUSE_LIBS@ @FLAC_LIBS@ @TAGLIB_LIBS@ @LIBEVENT_LIBS@ \
@LIBAVL_LIBS@ @MINIXML_LIBS@ @ANTLR3C_LIBS@ @LIBPLIST_LIBS@ \ @LIBAVL_LIBS@ @MINIXML_LIBS@ @ANTLR3C_LIBS@ @LIBPLIST_LIBS@ \
@LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @SPOTIFY_LIBS@
forked_daapd_SOURCES = main.c \ forked_daapd_SOURCES = main.c \
db.c db.h \ db.c db.h \
@ -95,6 +99,7 @@ forked_daapd_SOURCES = main.c \
evrtsp/rtsp.c evrtp/evrtsp.h \ evrtsp/rtsp.c evrtp/evrtsp.h \
evrtsp/rtsp-internal.h evrtsp/log.h \ evrtsp/rtsp-internal.h evrtsp/log.h \
scan-wma.c \ scan-wma.c \
$(SPOTIFYSRC) \
$(FLACSRC) $(MUSEPACKSRC) $(FLACSRC) $(MUSEPACKSRC)
nodist_forked_daapd_SOURCES = \ nodist_forked_daapd_SOURCES = \

View File

@ -739,6 +739,10 @@ artwork_get_embedded_image(char *filename, int max_w, int max_h, int format, str
if (strncmp(filename, "http://", strlen("http://")) == 0) if (strncmp(filename, "http://", strlen("http://")) == 0)
return -1; 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); DPRINTF(E_SPAM, L_ART, "Trying embedded artwork in %s\n", filename);
src_ctx = NULL; src_ctx = NULL;
@ -854,6 +858,10 @@ artwork_get_own_image(char *path, int max_w, int max_h, int format, struct evbuf
if (strncmp(path, "http://", strlen("http://")) == 0) if (strncmp(path, "http://", strlen("http://")) == 0)
return -1; 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); ret = snprintf(artwork, sizeof(artwork), "%s", path);
if ((ret < 0) || (ret >= sizeof(artwork))) if ((ret < 0) || (ret >= sizeof(artwork)))
{ {
@ -911,6 +919,10 @@ artwork_get_dir_image(char *path, int isdir, int max_w, int max_h, int format, s
if (strncmp(path, "http://", strlen("http://")) == 0) if (strncmp(path, "http://", strlen("http://")) == 0)
return -1; 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); ret = snprintf(artwork, sizeof(artwork), "%s", path);
if ((ret < 0) || (ret >= sizeof(artwork))) if ((ret < 0) || (ret >= sizeof(artwork)))
{ {
@ -981,6 +993,10 @@ artwork_get_parentdir_image(char *path, int isdir, int max_w, int max_h, int for
if (strncmp(path, "http://", strlen("http://")) == 0) if (strncmp(path, "http://", strlen("http://")) == 0)
return -1; 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); ret = snprintf(artwork, sizeof(artwork), "%s", path);
if ((ret < 0) || (ret >= sizeof(artwork))) if ((ret < 0) || (ret >= sizeof(artwork)))
{ {

View File

@ -100,6 +100,14 @@ static cfg_opt_t sec_airplay[] =
CFG_END() CFG_END()
}; };
/* Spotify section structure */
static cfg_opt_t sec_spotify[] =
{
CFG_STR("settings_dir", STATEDIR "/cache/" PACKAGE "/libspotify", CFGF_NONE),
CFG_STR("cache_dir", "/tmp", CFGF_NONE),
CFG_END()
};
/* Config file structure */ /* Config file structure */
static cfg_opt_t toplvl_cfg[] = static cfg_opt_t toplvl_cfg[] =
{ {
@ -107,6 +115,7 @@ static cfg_opt_t toplvl_cfg[] =
CFG_SEC("library", sec_library, CFGF_NONE), CFG_SEC("library", sec_library, CFGF_NONE),
CFG_SEC("audio", sec_audio, CFGF_NONE), CFG_SEC("audio", sec_audio, CFGF_NONE),
CFG_SEC("airplay", sec_airplay, CFGF_MULTI | CFGF_TITLE), CFG_SEC("airplay", sec_airplay, CFGF_MULTI | CFGF_TITLE),
CFG_SEC("spotify", sec_spotify, CFGF_NONE),
CFG_END() CFG_END()
}; };

132
src/db.c
View File

@ -273,6 +273,7 @@ static const char *sort_clause[] =
"ORDER BY f.title_sort ASC", "ORDER BY f.title_sort ASC",
"ORDER BY f.album_sort ASC, f.disc ASC, f.track ASC", "ORDER BY f.album_sort ASC, f.disc ASC, f.track ASC",
"ORDER BY f.album_artist_sort ASC", "ORDER BY f.album_artist_sort ASC",
"ORDER BY f.special_id DESC, f.title ASC",
}; };
static char *db_path; static char *db_path;
@ -876,6 +877,7 @@ db_build_query_pls(struct query_params *qp, char **q)
{ {
char *query; char *query;
char *idx; char *idx;
const char *sort;
int ret; int ret;
qp->results = db_get_count("SELECT COUNT(*) FROM playlists p WHERE p.disabled = 0;"); qp->results = db_get_count("SELECT COUNT(*) FROM playlists p WHERE p.disabled = 0;");
@ -887,14 +889,16 @@ db_build_query_pls(struct query_params *qp, char **q)
if (ret < 0) if (ret < 0)
return -1; return -1;
sort = sort_clause[qp->sort];
if (idx && qp->filter) if (idx && qp->filter)
query = sqlite3_mprintf("SELECT f.* FROM playlists f WHERE f.disabled = 0 AND %s %s;", qp->filter, idx); query = sqlite3_mprintf("SELECT f.* FROM playlists f WHERE f.disabled = 0 AND %s %s %s;", qp->filter, sort, idx);
else if (idx) else if (idx)
query = sqlite3_mprintf("SELECT f.* FROM playlists f WHERE f.disabled = 0 %s;", idx); query = sqlite3_mprintf("SELECT f.* FROM playlists f WHERE f.disabled = 0 %s %s;", sort, idx);
else if (qp->filter) else if (qp->filter)
query = sqlite3_mprintf("SELECT f.* FROM playlists f WHERE f.disabled = 0 AND %s;", qp->filter); query = sqlite3_mprintf("SELECT f.* FROM playlists f WHERE f.disabled = 0 AND %s %s;", qp->filter, sort);
else else
query = sqlite3_mprintf("SELECT f.* FROM playlists f WHERE f.disabled = 0;"); query = sqlite3_mprintf("SELECT f.* FROM playlists f WHERE f.disabled = 0 %s;", sort);
if (!query) if (!query)
{ {
@ -1281,10 +1285,10 @@ db_build_query_browse(struct query_params *qp, char *field, char *sort_field, ch
int ret; int ret;
if (qp->filter) if (qp->filter)
count = sqlite3_mprintf("SELECT COUNT(DISTINCT f.%s) FROM files f WHERE f.data_kind = 0 AND f.disabled = 0 AND f.%s != '' AND %s;", count = sqlite3_mprintf("SELECT COUNT(DISTINCT f.%s) FROM files f WHERE f.disabled = 0 AND f.%s != '' AND %s;",
field, field, qp->filter); field, field, qp->filter);
else else
count = sqlite3_mprintf("SELECT COUNT(DISTINCT f.%s) FROM files f WHERE f.data_kind = 0 AND f.disabled = 0 AND f.%s != '';", count = sqlite3_mprintf("SELECT COUNT(DISTINCT f.%s) FROM files f WHERE f.disabled = 0 AND f.%s != '';",
field, field); field, field);
if (!count) if (!count)
@ -1323,16 +1327,16 @@ db_build_query_browse(struct query_params *qp, char *field, char *sort_field, ch
} }
if (idx && qp->filter) if (idx && qp->filter)
query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.data_kind = 0 AND f.disabled = 0 AND f.%s != ''" query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
" AND %s %s %s;", field, sort_field, field, qp->filter, sort, idx); " AND %s %s %s;", field, sort_field, field, qp->filter, sort, idx);
else if (idx) else if (idx)
query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.data_kind = 0 AND f.disabled = 0 AND f.%s != ''" query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
" %s %s;", field, sort_field, field, sort, idx); " %s %s;", field, sort_field, field, sort, idx);
else if (qp->filter) else if (qp->filter)
query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.data_kind = 0 AND f.disabled = 0 AND f.%s != ''" query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != ''"
" AND %s %s;", field, sort_field, field, qp->filter, sort); " AND %s %s;", field, sort_field, field, qp->filter, sort);
else else
query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.data_kind = 0 AND f.disabled = 0 AND f.%s != '' %s", query = sqlite3_mprintf("SELECT DISTINCT f.%s, f.%s FROM files f WHERE f.disabled = 0 AND f.%s != '' %s",
field, sort_field, field, sort); field, sort_field, field, sort);
free(sort); free(sort);
@ -3045,6 +3049,41 @@ db_pl_add_item_byid(int plid, int fileid)
#undef Q_TMPL #undef Q_TMPL
} }
int
db_pl_update(char *title, char *path, int id)
{
#define Q_TMPL "UPDATE playlists SET title = '%q', db_timestamp = %" PRIi64 ", 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;
#undef Q_TMPL
}
void void
db_pl_clear_items(int id) db_pl_clear_items(int id)
{ {
@ -3427,6 +3466,79 @@ db_pairing_fetch_byguid(struct pairing_info *pi)
#undef Q_TMPL #undef Q_TMPL
} }
#ifdef HAVE_SPOTIFY_H
/* Spotify */
void
db_spotify_purge(void)
{
char *queries[3] =
{
"DELETE FROM files WHERE path LIKE 'spotify:%%';",
"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_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
DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl));
}
}
/* Spotify */
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;
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]);
ret = db_exec(queries[i], &errmsg);
if (ret != SQLITE_OK)
{
DPRINTF(E_LOG, L_DB, "Spotify playlist delete %d error: %s\n", i, errmsg);
sqlite3_free(errmsg);
}
else
DPRINTF(E_DBG, L_DB, "Deleted %d rows\n", sqlite3_changes(hdl));
}
}
#endif
/* Speakers */ /* Speakers */
int int

View File

@ -21,6 +21,7 @@ enum sort_type {
S_NAME, S_NAME,
S_ALBUM, S_ALBUM,
S_ARTIST, S_ARTIST,
S_PLAYLIST,
}; };
#define Q_F_BROWSE (1 << 15) #define Q_F_BROWSE (1 << 15)
@ -45,6 +46,7 @@ enum query_type {
#define ARTWORK_OWN 3 #define ARTWORK_OWN 3
#define ARTWORK_DIR 4 #define ARTWORK_DIR 4
#define ARTWORK_PARENTDIR 5 #define ARTWORK_PARENTDIR 5
#define ARTWORK_SPOTIFY 6
struct query_params { struct query_params {
/* Query parameters, filled in by caller */ /* Query parameters, filled in by caller */
@ -430,6 +432,9 @@ db_pl_add_item_byid(int plid, int fileid);
void void
db_pl_clear_items(int id); db_pl_clear_items(int id);
int
db_pl_update(char *title, char *path, int id);
void void
db_pl_delete(int id); db_pl_delete(int id);
@ -459,6 +464,15 @@ db_pairing_add(struct pairing_info *pi);
int int
db_pairing_fetch_byguid(struct pairing_info *pi); db_pairing_fetch_byguid(struct pairing_info *pi);
#ifdef HAVE_SPOTIFY_H
/* Spotify */
void
db_spotify_purge(void);
void
db_spotify_pl_delete(int id);
#endif
/* Speakers */ /* Speakers */
int int
db_speaker_save(uint64_t id, int selected, int volume); db_speaker_save(uint64_t id, int selected, int volume);

View File

@ -60,6 +60,10 @@
#include "misc.h" #include "misc.h"
#include "remote_pairing.h" #include "remote_pairing.h"
#ifdef HAVE_SPOTIFY_H
# include "spotify.h"
#endif
#define F_SCAN_BULK (1 << 0) #define F_SCAN_BULK (1 << 0)
#define F_SCAN_RESCAN (1 << 1) #define F_SCAN_RESCAN (1 << 1)
@ -368,25 +372,23 @@ fixup_tags(struct media_file_info *mfi)
void void
process_media_file(char *file, time_t mtime, off_t size, int type, struct extinf_ctx *extinf) filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct media_file_info *external_mfi)
{ {
struct media_file_info mfi; struct media_file_info *mfi;
char *filename; char *filename;
char *ext; char *ext;
time_t stamp; time_t stamp;
int id; int id;
int ret; int ret;
filename = strrchr(file, '/'); filename = strrchr(path, '/');
if (!filename) if ((!filename) || (strlen(filename) == 1))
{ filename = path;
DPRINTF(E_LOG, L_SCAN, "Could not determine filename for %s\n", file); else
filename++;
return;
}
/* File types which should never be processed */ /* File types which should never be processed */
ext = strrchr(file, '.'); ext = strrchr(path, '.');
if (ext) if (ext)
{ {
if ((strcasecmp(ext, ".pls") == 0) || (strcasecmp(ext, ".url") == 0)) if ((strcasecmp(ext, ".pls") == 0) || (strcasecmp(ext, ".url") == 0))
@ -400,7 +402,7 @@ process_media_file(char *file, time_t mtime, off_t size, int type, struct extinf
/* Artwork files - don't scan */ /* Artwork files - don't scan */
return; return;
} }
else if ((strlen(filename) > 1) && ((filename[1] == '_') || (filename[1] == '.'))) else if ((filename[0] == '_') || (filename[0] == '.'))
{ {
/* Hidden files - don't scan */ /* Hidden files - don't scan */
return; return;
@ -412,7 +414,7 @@ process_media_file(char *file, time_t mtime, off_t size, int type, struct extinf
} }
} }
db_file_stamp_bypath(file, &stamp, &id); db_file_stamp_bypath(path, &stamp, &id);
if (stamp >= mtime) if (stamp >= mtime)
{ {
@ -420,78 +422,88 @@ process_media_file(char *file, time_t mtime, off_t size, int type, struct extinf
return; return;
} }
memset(&mfi, 0, sizeof(struct media_file_info)); if (!external_mfi)
if (stamp)
mfi.id = db_file_id_bypath(file);
mfi.fname = strdup(filename + 1);
if (!mfi.fname)
{ {
DPRINTF(E_WARN, L_SCAN, "Out of memory for fname\n"); mfi = (struct media_file_info*)malloc(sizeof(struct media_file_info));
if (!mfi)
{
DPRINTF(E_LOG, L_SCAN, "Out of memory for mfi\n");
return;
}
return; memset(mfi, 0, sizeof(struct media_file_info));
}
mfi.path = strdup(file);
if (!mfi.path)
{
DPRINTF(E_WARN, L_SCAN, "Out of memory for path\n");
free(mfi.fname);
return;
}
mfi.time_modified = mtime;
mfi.file_size = size;
if (!(type & F_SCAN_TYPE_URL))
{
mfi.data_kind = 0; /* real file */
ret = scan_metadata_ffmpeg(file, &mfi);
} }
else else
mfi = external_mfi;
if (stamp)
mfi->id = db_file_id_bypath(path);
mfi->fname = strdup(filename);
if (!mfi->fname)
{ {
mfi.data_kind = 1; /* url/stream */ DPRINTF(E_LOG, L_SCAN, "Out of memory for fname\n");
if (extinf && extinf->found) goto out;
{
mfi.artist = strdup(extinf->artist);
mfi.title = strdup(extinf->artist);
mfi.album = strdup(extinf->title);
}
ret = scan_metadata_icy(file, &mfi);
} }
mfi->path = strdup(path);
if (!mfi->path)
{
DPRINTF(E_LOG, L_SCAN, "Out of memory for path\n");
goto out;
}
mfi->time_modified = mtime;
mfi->file_size = size;
if (type & F_SCAN_TYPE_FILE)
{
mfi->data_kind = 0; /* real file */
ret = scan_metadata_ffmpeg(path, mfi);
}
else if (type & F_SCAN_TYPE_URL)
{
mfi->data_kind = 1; /* url/stream */
ret = scan_metadata_icy(path, mfi);
}
else if (type & F_SCAN_TYPE_SPOTIFY)
{
mfi->data_kind = 2; /* iTunes has no spotify data kind, but we use 2 */
ret = mfi->artist && mfi->album && mfi->title;
}
else
ret = -1;
if (ret < 0) if (ret < 0)
{ {
DPRINTF(E_INFO, L_SCAN, "Could not extract metadata for %s\n", file); DPRINTF(E_INFO, L_SCAN, "Could not extract metadata for %s\n", path);
goto out; goto out;
} }
if (type & F_SCAN_TYPE_COMPILATION) if (type & F_SCAN_TYPE_COMPILATION)
mfi.compilation = 1; mfi->compilation = 1;
if (type & F_SCAN_TYPE_PODCAST) if (type & F_SCAN_TYPE_PODCAST)
mfi.media_kind = 4; /* podcast */ mfi->media_kind = 4; /* podcast */
if (type & F_SCAN_TYPE_AUDIOBOOK) if (type & F_SCAN_TYPE_AUDIOBOOK)
mfi.media_kind = 8; /* audiobook */ mfi->media_kind = 8; /* audiobook */
if (!mfi.item_kind) if (!mfi->item_kind)
mfi.item_kind = 2; /* music */ mfi->item_kind = 2; /* music */
if (!mfi.media_kind) if (!mfi->media_kind)
mfi.media_kind = 1; /* music */ mfi->media_kind = 1; /* music */
unicode_fixup_mfi(&mfi); unicode_fixup_mfi(mfi);
fixup_tags(&mfi); fixup_tags(mfi);
if (mfi.id == 0) if (mfi->id == 0)
db_file_add(&mfi); db_file_add(mfi);
else else
db_file_update(&mfi); db_file_update(mfi);
out: out:
free_mfi(&mfi, 1); if (!external_mfi)
free_mfi(mfi, 0);
} }
static void static void
@ -594,6 +606,14 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags)
return; return;
} }
#ifdef HAVE_SPOTIFY_H
else if (strcmp(ext, ".spotify") == 0)
{
spotify_login(file);
return;
}
#endif
else if (strcmp(ext, ".force-rescan") == 0) else if (strcmp(ext, ".force-rescan") == 0)
{ {
if (flags & F_SCAN_BULK) if (flags & F_SCAN_BULK)
@ -610,7 +630,7 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags)
} }
/* Not any kind of special file, so let's see if it's a media file */ /* Not any kind of special file, so let's see if it's a media file */
process_media_file(file, mtime, size, type, NULL); filescanner_process_media(file, mtime, size, type, NULL);
} }
/* Thread: scan */ /* Thread: scan */
@ -1109,12 +1129,12 @@ process_inotify_file(struct watch_info *wi, char *path, struct inotify_event *ie
* We want to scan the new file and we want to rescan the * We want to scan the new file and we want to rescan the
* playlist to update playlist items (relative items). * playlist to update playlist items (relative items).
*/ */
ie->mask |= IN_CREATE; ie->mask |= IN_CLOSE_WRITE;
db_pl_enable_bycookie(ie->cookie, wi->path); db_pl_enable_bycookie(ie->cookie, wi->path);
} }
} }
if (ie->mask & (IN_MODIFY | IN_CREATE | IN_CLOSE_WRITE)) if (ie->mask & IN_CLOSE_WRITE)
{ {
ret = lstat(path, &sb); ret = lstat(path, &sb);
if (ret < 0) if (ret < 0)
@ -1475,6 +1495,12 @@ exit_cb(int fd, short event, void *arg)
} }
int
filescanner_status(void)
{
return scan_exit;
}
/* Thread: main */ /* Thread: main */
int int
filescanner_init(void) filescanner_init(void)

View File

@ -4,10 +4,12 @@
#include "db.h" #include "db.h"
#define F_SCAN_TYPE_PODCAST (1 << 0) #define F_SCAN_TYPE_FILE (1 << 0)
#define F_SCAN_TYPE_AUDIOBOOK (1 << 1) #define F_SCAN_TYPE_PODCAST (F_SCAN_TYPE_FILE | (1 << 1))
#define F_SCAN_TYPE_COMPILATION (1 << 2) #define F_SCAN_TYPE_AUDIOBOOK (F_SCAN_TYPE_FILE | (1 << 2))
#define F_SCAN_TYPE_URL (1 << 3) #define F_SCAN_TYPE_COMPILATION (F_SCAN_TYPE_FILE | (1 << 3))
#define F_SCAN_TYPE_URL (1 << 4)
#define F_SCAN_TYPE_SPOTIFY (1 << 5)
int int
filescanner_init(void); filescanner_init(void);
@ -15,15 +17,11 @@ filescanner_init(void);
void void
filescanner_deinit(void); filescanner_deinit(void);
struct extinf_ctx int
{ filescanner_status(void);
char *artist;
char *title;
int found;
};
void void
process_media_file(char *file, time_t mtime, off_t size, int type, struct extinf_ctx *extinf); filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct media_file_info *external_mfi);
/* Actual scanners */ /* Actual scanners */
int int

View File

@ -42,7 +42,7 @@
/* Get metadata from the EXTINF tag */ /* Get metadata from the EXTINF tag */
static int static int
extinf_get(char *string, struct extinf_ctx *extinf) extinf_get(char *string, struct media_file_info *mfi, int *extinf)
{ {
char *ptr; char *ptr;
@ -54,20 +54,20 @@ extinf_get(char *string, struct extinf_ctx *extinf)
return 0; return 0;
/* New extinf found, so clear old data */ /* New extinf found, so clear old data */
if (extinf->found) if (*extinf)
{ {
free(extinf->artist); free(mfi->artist);
free(extinf->title); free(mfi->title);
} }
extinf->found = 1; *extinf = 1;
extinf->artist = strdup(ptr + 1); mfi->artist = strdup(ptr + 1);
ptr = strstr(extinf->artist, " -"); ptr = strstr(mfi->artist, " -");
if (ptr && strlen(ptr) > 3) if (ptr && strlen(ptr) > 3)
extinf->title = strdup(ptr + 3); mfi->title = strdup(ptr + 3);
else else
extinf->title = strdup(""); mfi->title = strdup("");
if (ptr) if (ptr)
*ptr = '\0'; *ptr = '\0';
@ -75,28 +75,29 @@ extinf_get(char *string, struct extinf_ctx *extinf)
} }
static void static void
extinf_reset(struct extinf_ctx *extinf) extinf_reset(struct media_file_info *mfi, int *extinf)
{ {
if (extinf->found) if (*extinf)
{ {
free(extinf->artist); free(mfi->artist);
free(extinf->title); free(mfi->title);
} }
extinf->found = 0; *extinf = 0;
} }
void void
scan_m3u_playlist(char *file, time_t mtime) scan_m3u_playlist(char *file, time_t mtime)
{ {
FILE *fp; FILE *fp;
struct media_file_info mfi;
struct playlist_info *pli; struct playlist_info *pli;
struct stat sb; struct stat sb;
struct extinf_ctx extinf;
char buf[PATH_MAX]; char buf[PATH_MAX];
char *entry; char *entry;
char *filename; char *filename;
char *ptr; char *ptr;
size_t len; size_t len;
int extinf;
int pl_id; int pl_id;
int mfi_id; int mfi_id;
int ret; int ret;
@ -104,6 +105,8 @@ scan_m3u_playlist(char *file, time_t mtime)
DPRINTF(E_INFO, L_SCAN, "Processing static playlist: %s\n", file); DPRINTF(E_INFO, L_SCAN, "Processing static playlist: %s\n", file);
memset(&mfi, 0, sizeof(struct media_file_info));
ret = stat(file, &sb); ret = stat(file, &sb);
if (ret < 0) if (ret < 0)
{ {
@ -167,7 +170,7 @@ scan_m3u_playlist(char *file, time_t mtime)
DPRINTF(E_INFO, L_SCAN, "Added playlist as id %d\n", pl_id); DPRINTF(E_INFO, L_SCAN, "Added playlist as id %d\n", pl_id);
} }
extinf.found = 0; extinf = 0;
while (fgets(buf, sizeof(buf), fp) != NULL) while (fgets(buf, sizeof(buf), fp) != NULL)
{ {
@ -189,7 +192,7 @@ scan_m3u_playlist(char *file, time_t mtime)
continue; continue;
/* Saves metadata in extinf if EXTINF metadata line */ /* Saves metadata in extinf if EXTINF metadata line */
if (extinf_get(buf, &extinf)) if (extinf_get(buf, &mfi, &extinf))
continue; continue;
/* Check that first char is sane for a path */ /* Check that first char is sane for a path */
@ -209,10 +212,10 @@ scan_m3u_playlist(char *file, time_t mtime)
continue; continue;
} }
if (extinf.found) if (extinf)
DPRINTF(E_INFO, L_SCAN, "Playlist has EXTINF metadata, artist is '%s', title is '%s'\n", extinf.artist, extinf.title); DPRINTF(E_INFO, L_SCAN, "Playlist has EXTINF metadata, artist is '%s', title is '%s'\n", mfi.artist, mfi.title);
process_media_file(filename, mtime, 0, F_SCAN_TYPE_URL, &extinf); filescanner_process_media(filename, mtime, 0, F_SCAN_TYPE_URL, &mfi);
} }
/* Regular file */ /* Regular file */
else else
@ -271,7 +274,7 @@ scan_m3u_playlist(char *file, time_t mtime)
if (ret < 0) if (ret < 0)
DPRINTF(E_WARN, L_SCAN, "Could not add %s to playlist\n", filename); DPRINTF(E_WARN, L_SCAN, "Could not add %s to playlist\n", filename);
extinf_reset(&extinf); extinf_reset(&mfi, &extinf);
free(filename); free(filename);
} }

View File

@ -395,8 +395,8 @@ httpd_stream_file(struct evhttp_request *req, int id)
stream_cb = stream_chunk_xcode_cb; stream_cb = stream_chunk_xcode_cb;
st->xcode = transcode_setup(mfi, &st->size, 1); ret = transcode_setup(&st->xcode, mfi, &st->size, 1);
if (!st->xcode) if (ret < 0)
{ {
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n"); DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n");

View File

@ -73,7 +73,7 @@ struct uri_map {
struct daap_session { struct daap_session {
int id; int id;
char *user_agent;
struct event timeout; struct event timeout;
}; };
@ -132,7 +132,12 @@ daap_session_free(void *item)
s = (struct daap_session *)item; s = (struct daap_session *)item;
if (event_initialized(&s->timeout)) evtimer_del(&s->timeout); if (event_initialized(&s->timeout))
evtimer_del(&s->timeout);
if (s->user_agent)
free(s->user_agent);
free(s); free(s);
} }
@ -155,7 +160,7 @@ daap_session_timeout_cb(int fd, short what, void *arg)
} }
static struct daap_session * static struct daap_session *
daap_session_register(void) daap_session_register(const char *user_agent)
{ {
struct timeval tv; struct timeval tv;
struct daap_session *s; struct daap_session *s;
@ -175,6 +180,9 @@ daap_session_register(void)
next_session_id++; next_session_id++;
if (user_agent)
s->user_agent = strdup(user_agent);
if (DAAP_SESSION_TIMEOUT > 0) { if (DAAP_SESSION_TIMEOUT > 0) {
evtimer_set(&s->timeout, daap_session_timeout_cb, s); evtimer_set(&s->timeout, daap_session_timeout_cb, s);
event_base_set(evbase_httpd, &s->timeout); event_base_set(evbase_httpd, &s->timeout);
@ -185,6 +193,9 @@ daap_session_register(void)
{ {
DPRINTF(E_LOG, L_DAAP, "Could not register DAAP session: %s\n", strerror(errno)); DPRINTF(E_LOG, L_DAAP, "Could not register DAAP session: %s\n", strerror(errno));
if (user_agent)
free(s->user_agent);
free(s); free(s);
return NULL; return NULL;
} }
@ -466,6 +477,47 @@ daap_sort_finalize(struct sort_ctx *ctx)
dmap_add_int(ctx->headerlist, "mshn", ctx->misc_mshn); /* 12 */ dmap_add_int(ctx->headerlist, "mshn", ctx->misc_mshn); /* 12 */
} }
/* We try not to return items that the client cannot play (like Spotify and
* internet streams in iTunes), or which are inappropriate (like internet streams
* in the album tab in Remote
*/
static void
user_agent_filter(const char *user_agent, struct query_params *qp)
{
char *filter;
char *buffer;
int len;
if (!user_agent)
return;
if (strcasestr(user_agent, "itunes"))
filter = strdup("(f.data_kind = 0)"); // Only real files
else if (strcasestr(user_agent, "daap"))
filter = strdup("(f.data_kind = 0)"); // Only real files
else if (strcasestr(user_agent, "remote"))
filter = strdup("(f.data_kind <> 1)"); // No internet radio
else if (strcasestr(user_agent, "android"))
filter = strdup("(f.data_kind <> 1)"); // No internet radio
else
return;
if (qp->filter)
{
len = strlen(qp->filter) + strlen(" AND ") + strlen(filter);
buffer = (char *)malloc(len + 1);
snprintf(buffer, len + 1, "%s AND %s", qp->filter, filter);
free(qp->filter);
qp->filter = strdup(buffer);
free(buffer);
}
else
qp->filter = strdup(filter);
DPRINTF(E_DBG, L_DAAP, "SQL filter w/client mod: %s\n", qp->filter);
free(filter);
}
static void static void
get_query_params(struct evkeyvalq *query, int *sort_headers, struct query_params *qp) get_query_params(struct evkeyvalq *query, int *sort_headers, struct query_params *qp)
@ -807,7 +859,7 @@ daap_reply_login(struct evhttp_request *req, struct evbuffer *evbuf, char **uri,
free_pi(&pi, 1); free_pi(&pi, 1);
} }
s = daap_session_register(); s = daap_session_register(ua);
if (!s) if (!s)
{ {
dmap_send_error(req, "mlog", "Could not start session"); dmap_send_error(req, "mlog", "Could not start session");
@ -987,6 +1039,7 @@ daap_reply_dblist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
static void static void
daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf, int playlist, struct evkeyvalq *query) daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf, int playlist, struct evkeyvalq *query)
{ {
struct daap_session *s;
struct query_params qp; struct query_params qp;
struct db_media_file_info dbmfi; struct db_media_file_info dbmfi;
struct evbuffer *song; struct evbuffer *song;
@ -1001,6 +1054,10 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
int transcode; int transcode;
int ret; int ret;
s = daap_session_find(req, query, evbuf);
if (!s)
return;
DPRINTF(E_DBG, L_DAAP, "Fetching song list for playlist %d\n", playlist); DPRINTF(E_DBG, L_DAAP, "Fetching song list for playlist %d\n", playlist);
if (playlist != -1) if (playlist != -1)
@ -1082,6 +1139,8 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
memset(&qp, 0, sizeof(struct query_params)); memset(&qp, 0, sizeof(struct query_params));
get_query_params(query, &sort_headers, &qp); get_query_params(query, &sort_headers, &qp);
if (playlist == -1)
user_agent_filter(s->user_agent, &qp);
sctx = NULL; sctx = NULL;
if (sort_headers) if (sort_headers)
@ -1242,26 +1301,15 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf,
static void static void
daap_reply_dbsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) daap_reply_dbsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
{ {
struct daap_session *s;
s = daap_session_find(req, query, evbuf);
if (!s)
return;
daap_reply_songlist_generic(req, evbuf, -1, query); daap_reply_songlist_generic(req, evbuf, -1, query);
} }
static void static void
daap_reply_plsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) daap_reply_plsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query)
{ {
struct daap_session *s;
int playlist; int playlist;
int ret; int ret;
s = daap_session_find(req, query, evbuf);
if (!s)
return;
ret = safe_atoi32(uri[3], &playlist); ret = safe_atoi32(uri[3], &playlist);
if (ret < 0) if (ret < 0)
{ {
@ -1364,6 +1412,7 @@ daap_reply_playlists(struct evhttp_request *req, struct evbuffer *evbuf, char **
memset(&qp, 0, sizeof(struct query_params)); memset(&qp, 0, sizeof(struct query_params));
get_query_params(query, NULL, &qp); get_query_params(query, NULL, &qp);
qp.type = Q_PL; qp.type = Q_PL;
qp.sort = S_PLAYLIST;
ret = db_query_start(&qp); ret = db_query_start(&qp);
if (ret < 0) if (ret < 0)
@ -1545,6 +1594,7 @@ daap_reply_groups(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
memset(&qp, 0, sizeof(struct query_params)); memset(&qp, 0, sizeof(struct query_params));
get_query_params(query, &sort_headers, &qp); get_query_params(query, &sort_headers, &qp);
user_agent_filter(s->user_agent, &qp);
param = evhttp_find_header(query, "group-type"); param = evhttp_find_header(query, "group-type");
if (strcmp(param, "artists") == 0) if (strcmp(param, "artists") == 0)
@ -1833,6 +1883,7 @@ daap_reply_browse(struct evhttp_request *req, struct evbuffer *evbuf, char **uri
memset(&qp, 0, sizeof(struct query_params)); memset(&qp, 0, sizeof(struct query_params));
get_query_params(query, &sort_headers, &qp); get_query_params(query, &sort_headers, &qp);
user_agent_filter(s->user_agent, &qp);
if (strcmp(uri[3], "artists") == 0) if (strcmp(uri[3], "artists") == 0)
{ {

View File

@ -43,7 +43,7 @@ static int threshold;
static int console; static int console;
static char *logfilename; static char *logfilename;
static FILE *logfile; 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" }; 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 int static int

View File

@ -26,8 +26,9 @@
#define L_LAUDIO 17 #define L_LAUDIO 17
#define L_DMAP 18 #define L_DMAP 18
#define L_DBPERF 19 #define L_DBPERF 19
#define L_SPOTIFY 20
#define N_LOGDOMAINS 20 #define N_LOGDOMAINS 21
/* Severities */ /* Severities */
#define E_FATAL 0 #define E_FATAL 0

View File

@ -67,6 +67,9 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL;
# include "ffmpeg_url_evbuffer.h" # include "ffmpeg_url_evbuffer.h"
#endif #endif
#ifdef HAVE_SPOTIFY_H
# include "spotify.h"
#endif
#define PIDFILE STATEDIR "/run/" PACKAGE ".pid" #define PIDFILE STATEDIR "/run/" PACKAGE ".pid"
@ -678,6 +681,15 @@ main(int argc, char **argv)
goto filescanner_fail; goto filescanner_fail;
} }
#ifdef HAVE_SPOTIFY_H
/* Spawn Spotify thread */
ret = spotify_init();
if (ret < 0)
{
DPRINTF(E_INFO, L_MAIN, "Spotify thread not started\n");;
}
#endif
/* Spawn player thread */ /* Spawn player thread */
ret = player_init(); ret = player_init();
if (ret != 0) if (ret != 0)
@ -786,6 +798,10 @@ main(int argc, char **argv)
player_deinit(); player_deinit();
player_fail: player_fail:
#ifdef HAVE_SPOTIFY_H
DPRINTF(E_LOG, L_MAIN, "Spotify deinit\n");
spotify_deinit();
#endif
DPRINTF(E_LOG, L_MAIN, "File scanner deinit\n"); DPRINTF(E_LOG, L_MAIN, "File scanner deinit\n");
filescanner_deinit(); filescanner_deinit();

View File

@ -59,6 +59,9 @@
#include "raop.h" #include "raop.h"
#include "laudio.h" #include "laudio.h"
#ifdef HAVE_SPOTIFY_H
# include "spotify.h"
#endif
#ifndef MIN #ifndef MIN
# define MIN(a, b) ((a < b) ? a : b) # define MIN(a, b) ((a < b) ? a : b)
@ -882,8 +885,19 @@ player_queue_make_pl(int plid, uint32_t *id)
static void static void
source_free(struct player_source *ps) source_free(struct player_source *ps)
{ {
if (ps->ctx) switch (ps->type)
transcode_cleanup(ps->ctx); {
case SOURCE_FFMPEG:
if (ps->ctx)
transcode_cleanup(ps->ctx);
break;
case SOURCE_SPOTIFY:
#ifdef HAVE_SPOTIFY_H
spotify_playback_stop();
#endif
break;
}
free(ps); free(ps);
} }
@ -895,11 +909,22 @@ source_stop(struct player_source *ps)
while (ps) while (ps)
{ {
if (ps->ctx) switch (ps->type)
{ {
transcode_cleanup(ps->ctx); case SOURCE_FFMPEG:
ps->ctx = NULL; if (ps->ctx)
} {
transcode_cleanup(ps->ctx);
ps->ctx = NULL;
}
break;
case SOURCE_SPOTIFY:
#ifdef HAVE_SPOTIFY_H
spotify_playback_stop();
#endif
break;
}
tmp = ps; tmp = ps;
ps = ps->play_next; ps = ps->play_next;
@ -989,7 +1014,9 @@ static int
source_open(struct player_source *ps, int no_md) source_open(struct player_source *ps, int no_md)
{ {
struct media_file_info *mfi; struct media_file_info *mfi;
int ret;
ps->setup_done = 0;
ps->stream_start = 0; ps->stream_start = 0;
ps->output_start = 0; ps->output_start = 0;
ps->end = 0; ps->end = 0;
@ -1013,11 +1040,24 @@ source_open(struct player_source *ps, int no_md)
DPRINTF(E_DBG, L_PLAYER, "Opening %s\n", mfi->path); DPRINTF(E_DBG, L_PLAYER, "Opening %s\n", mfi->path);
ps->ctx = transcode_setup(mfi, NULL, 0); if (strncmp(mfi->path, "spotify:", strlen("spotify:")) == 0)
{
ps->type = SOURCE_SPOTIFY;
#ifdef HAVE_SPOTIFY_H
ret = spotify_playback_play(mfi);
#else
ret = -1;
#endif
}
else
{
ps->type = SOURCE_FFMPEG;
ret = transcode_setup(&ps->ctx, mfi, NULL, 0);
}
free_mfi(mfi, 0); free_mfi(mfi, 0);
if (!ps->ctx) if (ret < 0)
{ {
DPRINTF(E_LOG, L_PLAYER, "Could not open file id %d\n", ps->id); DPRINTF(E_LOG, L_PLAYER, "Could not open file id %d\n", ps->id);
@ -1027,6 +1067,8 @@ source_open(struct player_source *ps, int no_md)
if (!no_md) if (!no_md)
metadata_send(ps, (player_state == PLAY_PLAYING) ? 0 : 1); metadata_send(ps, (player_state == PLAY_PLAYING) ? 0 : 1);
ps->setup_done = 1;
return 0; return 0;
} }
@ -1067,7 +1109,7 @@ source_next(int force)
if (!cur_streaming) if (!cur_streaming)
break; break;
if (cur_streaming->ctx) if ((cur_streaming->type == SOURCE_FFMPEG) && cur_streaming->ctx)
{ {
ret = transcode_seek(cur_streaming->ctx, 0); ret = transcode_seek(cur_streaming->ctx, 0);
@ -1276,10 +1318,13 @@ source_check(void)
{ {
cur_playing = cur_playing->play_next; cur_playing = cur_playing->play_next;
if (ps->ctx) if (ps->setup_done)
{ {
transcode_cleanup(ps->ctx); if ((ps->type == SOURCE_FFMPEG) && ps->ctx)
ps->ctx = NULL; {
transcode_cleanup(ps->ctx);
ps->ctx = NULL;
}
ps->play_next = NULL; ps->play_next = NULL;
} }
} }
@ -1324,10 +1369,13 @@ source_check(void)
cur_playing->stream_start = ps->end + 1; cur_playing->stream_start = ps->end + 1;
cur_playing->output_start = cur_playing->stream_start; cur_playing->output_start = cur_playing->stream_start;
if (ps->ctx) if (ps->setup_done)
{ {
transcode_cleanup(ps->ctx); if ((ps->type == SOURCE_FFMPEG) && ps->ctx)
ps->ctx = NULL; {
transcode_cleanup(ps->ctx);
ps->ctx = NULL;
}
ps->play_next = NULL; ps->play_next = NULL;
} }
} }
@ -1371,7 +1419,22 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
if (EVBUFFER_LENGTH(audio_buf) == 0) if (EVBUFFER_LENGTH(audio_buf) == 0)
{ {
ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes); switch (cur_streaming->type)
{
case SOURCE_FFMPEG:
ret = transcode(cur_streaming->ctx, audio_buf, len - nbytes);
break;
#ifdef HAVE_SPOTIFY_H
case SOURCE_SPOTIFY:
ret = spotify_audio_get(audio_buf, len - nbytes);
break;
#endif
default:
ret = -1;
}
if (ret <= 0) if (ret <= 0)
{ {
/* EOF or error */ /* EOF or error */
@ -2555,7 +2618,20 @@ playback_seek_bh(struct player_command *cmd)
ps->end = 0; ps->end = 0;
/* Seek to commanded position */ /* Seek to commanded position */
ret = transcode_seek(ps->ctx, ms); switch (ps->type)
{
case SOURCE_FFMPEG:
ret = transcode_seek(ps->ctx, ms);
break;
#ifdef HAVE_SPOTIFY_H
case SOURCE_SPOTIFY:
ret = spotify_playback_seek(ms);
break;
#endif
default:
ret = -1;
}
if (ret < 0) if (ret < 0)
{ {
playback_abort(); playback_abort();
@ -2596,7 +2672,20 @@ playback_pause_bh(struct player_command *cmd)
pos -= ps->stream_start; pos -= ps->stream_start;
ms = (int)((pos * 1000) / 44100); ms = (int)((pos * 1000) / 44100);
ret = transcode_seek(ps->ctx, ms); switch (ps->type)
{
case SOURCE_FFMPEG:
ret = transcode_seek(ps->ctx, ms);
break;
#ifdef HAVE_SPOTIFY_H
case SOURCE_SPOTIFY:
ret = spotify_playback_seek(ms);
break;
#endif
default:
ret = -1;
}
if (ret < 0) if (ret < 0)
{ {
playback_abort(); playback_abort();

View File

@ -33,6 +33,11 @@ enum repeat_mode {
REPEAT_ALL = 2, REPEAT_ALL = 2,
}; };
enum source_type {
SOURCE_FFMPEG = 0,
SOURCE_SPOTIFY = 1,
};
struct spk_flags { struct spk_flags {
unsigned selected:1; unsigned selected:1;
unsigned has_password:1; unsigned has_password:1;
@ -60,6 +65,9 @@ struct player_source
{ {
uint32_t id; uint32_t id;
enum source_type type;
int setup_done;
uint64_t stream_start; uint64_t stream_start;
uint64_t output_start; uint64_t output_start;
uint64_t end; uint64_t end;

1548
src/spotify.c Normal file

File diff suppressed because it is too large Load Diff

32
src/spotify.h Normal file
View File

@ -0,0 +1,32 @@
#ifndef __SPOTIFY_H__
#define __SPOTIFY_H__
#include "db.h"
#include "evhttp/evhttp.h"
int
spotify_playback_play(struct media_file_info *mfi);
int
spotify_playback_pause(void);
int
spotify_playback_stop(void);
int
spotify_playback_seek(int ms);
int
spotify_audio_get(struct evbuffer *evbuf, int wanted);
void
spotify_login(char *path);
int
spotify_init(void);
void
spotify_deinit(void);
#endif /* !__SPOTIFY_H__ */

View File

@ -54,6 +54,9 @@
#include "db.h" #include "db.h"
#include "transcode.h" #include "transcode.h"
#ifdef HAVE_SPOTIFY_H
# include "spotify.h"
#endif
#if LIBAVCODEC_VERSION_MAJOR >= 56 || (LIBAVCODEC_VERSION_MAJOR == 55 && LIBAVCODEC_VERSION_MINOR >= 18) #if LIBAVCODEC_VERSION_MAJOR >= 56 || (LIBAVCODEC_VERSION_MAJOR == 55 && LIBAVCODEC_VERSION_MINOR >= 18)
# define XCODE_BUFFER_SIZE ((192000 * 3) / 2) # define XCODE_BUFFER_SIZE ((192000 * 3) / 2)
@ -462,9 +465,8 @@ transcode_seek(struct transcode_ctx *ctx, int ms)
return got_ms; return got_ms;
} }
int
struct transcode_ctx * transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t *est_size, int wavhdr)
transcode_setup(struct media_file_info *mfi, off_t *est_size, int wavhdr)
{ {
struct transcode_ctx *ctx; struct transcode_ctx *ctx;
int ret; int ret;
@ -474,7 +476,7 @@ transcode_setup(struct media_file_info *mfi, off_t *est_size, int wavhdr)
{ {
DPRINTF(E_WARN, L_XCODE, "Could not allocate transcode context\n"); DPRINTF(E_WARN, L_XCODE, "Could not allocate transcode context\n");
return NULL; return -1;
} }
memset(ctx, 0, sizeof(struct transcode_ctx)); memset(ctx, 0, sizeof(struct transcode_ctx));
@ -488,7 +490,7 @@ transcode_setup(struct media_file_info *mfi, off_t *est_size, int wavhdr)
DPRINTF(E_WARN, L_XCODE, "Could not open file %s: %s\n", mfi->fname, strerror(AVUNERROR(ret))); DPRINTF(E_WARN, L_XCODE, "Could not open file %s: %s\n", mfi->fname, strerror(AVUNERROR(ret)));
free(ctx); free(ctx);
return NULL; return -1;
} }
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3) #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3)
@ -662,7 +664,9 @@ transcode_setup(struct media_file_info *mfi, off_t *est_size, int wavhdr)
if (wavhdr) if (wavhdr)
make_wav_header(ctx, est_size); make_wav_header(ctx, est_size);
return ctx; *nctx = ctx;
return 0;
setup_fail_codec: setup_fail_codec:
avcodec_close(ctx->acodec); avcodec_close(ctx->acodec);
@ -675,7 +679,7 @@ transcode_setup(struct media_file_info *mfi, off_t *est_size, int wavhdr)
#endif #endif
free(ctx); free(ctx);
return NULL; return -1;
} }
void void
@ -684,11 +688,14 @@ transcode_cleanup(struct transcode_ctx *ctx)
if (ctx->apacket.data) if (ctx->apacket.data)
av_free_packet(&ctx->apacket); av_free_packet(&ctx->apacket);
avcodec_close(ctx->acodec); if (ctx->acodec)
avcodec_close(ctx->acodec);
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21) #if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21)
avformat_close_input(&ctx->fmtctx); if (ctx->fmtctx)
avformat_close_input(&ctx->fmtctx);
#else #else
av_close_input_file(ctx->fmtctx); if (ctx->fmtctx)
av_close_input_file(ctx->fmtctx);
#endif #endif
av_free(ctx->abuffer); av_free(ctx->abuffer);
@ -717,6 +724,12 @@ transcode_needed(struct evkeyvalq *headers, char *file_codectype)
int size; int size;
int i; int i;
if (!file_codectype)
{
DPRINTF(E_LOG, L_XCODE, "Can't proceed, codectype is unknown (null)\n");
return -1;
}
DPRINTF(E_DBG, L_XCODE, "Determining transcoding status for codectype %s\n", file_codectype); DPRINTF(E_DBG, L_XCODE, "Determining transcoding status for codectype %s\n", file_codectype);
lib = cfg_getsec(cfg, "library"); lib = cfg_getsec(cfg, "library");

View File

@ -12,8 +12,8 @@ transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted);
int int
transcode_seek(struct transcode_ctx *ctx, int ms); transcode_seek(struct transcode_ctx *ctx, int ms);
struct transcode_ctx * int
transcode_setup(struct media_file_info *mfi, off_t *est_size, int wavhdr); transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t *est_size, int wavhdr);
void void
transcode_cleanup(struct transcode_ctx *ctx); transcode_cleanup(struct transcode_ctx *ctx);