From 7ed6cc98c3417b5ad41c4d8fae3b188fea4fd8eb Mon Sep 17 00:00:00 2001 From: ejurgensen Date: Tue, 11 Mar 2014 23:20:29 +0100 Subject: [PATCH] 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... --- INSTALL | 28 +- Makefile.am | 2 +- README | 64 +- configure.in | 15 + forked-daapd.conf | 8 + src/Makefile.am | 9 +- src/artwork.c | 16 + src/conffile.c | 9 + src/db.c | 132 +++- src/db.h | 14 + src/filescanner.c | 156 +++-- src/filescanner.h | 20 +- src/filescanner_m3u.c | 45 +- src/httpd.c | 4 +- src/httpd_daap.c | 81 ++- src/logger.c | 2 +- src/logger.h | 3 +- src/main.c | 16 + src/player.c | 125 +++- src/player.h | 8 + src/spotify.c | 1548 +++++++++++++++++++++++++++++++++++++++++ src/spotify.h | 32 + src/transcode.c | 33 +- src/transcode.h | 4 +- 24 files changed, 2203 insertions(+), 171 deletions(-) create mode 100644 src/spotify.c create mode 100644 src/spotify.h diff --git a/INSTALL b/INSTALL index 341b9a2f..1721b676 100644 --- a/INSTALL +++ b/INSTALL @@ -97,6 +97,8 @@ Libraries: from - libplist 0.16+ (optional - iTunes XML support) from + - libspotify (optional - Spotify support) + from If using binary packages, remember that you need the development packages to build forked-daapd (usually named -dev or -devel). @@ -143,14 +145,22 @@ needed. To display the configure options run ./configure --help -FLAC and Musepack support are optional. If not enabled, metadata extraction -will fail on these files. +Support for Spotify is optional. Use --enable-spotify to enable this feature. +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 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: - ./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 @@ -189,12 +199,14 @@ The LSB header below sums it up: # Required-Start: $local_fs $remote_fs $network $time # Required-Stop: $local_fs $remote_fs $network $time # Should-Start: avahi +# Should-Stop: avahi # Default-Start: 2 3 4 5 # 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 -# sharing your media library over the local network with RSP -# clients like the SoundBridge from Roku and DAAP clients -# like iTunes. It can also stream music to AirPlay devices, -# and it can be controlled by Apple Remote (and compatibles). +# sharing your media library over the local network with DAAP +# clients like iTunes. Like iTunes, it can be controlled by +# Apple Remote (and compatibles) and stream music directly to +# AirPlay devices. It also supports streaming to RSP clients +# (Roku devices) and streaming from Spotify. ### END INIT INFO diff --git a/Makefile.am b/Makefile.am index 4ec23c31..1d766499 100644 --- a/Makefile.am +++ b/Makefile.am @@ -8,5 +8,5 @@ SUBDIRS = sqlext src man_MANS = forked-daapd.8 install-data-hook: - $(MKDIR_P) $(DESTDIR)$(localstatedir)/cache/forked-daapd + $(MKDIR_P) $(DESTDIR)$(localstatedir)/cache/forked-daapd/libspotify diff --git a/README b/README index bd3b3e7d..b292f2c0 100644 --- a/README +++ b/README @@ -1,8 +1,10 @@ forked-daapd ------------ -forked-daapd is a DAAP (iTunes) and RSP (Roku) media server, with support for -Linux and FreeBSD. +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. DAAP stands for Digital Audio Access Protocol, and is the protocol used 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). +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 ----------------- @@ -198,12 +215,15 @@ happily write the metadata back at the end of the file after you've modified them. Watch out for that. -Playlists ---------- +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. +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 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. @@ -289,3 +309,39 @@ so changes won't be noticed unless the file happens to be in a directory that is monitored. 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 + - Debian package (libspotify12), see + +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. diff --git a/configure.in b/configure.in index 299c680b..1ca5cee8 100644 --- a/configure.in +++ b/configure.in @@ -60,9 +60,14 @@ AC_ARG_ENABLE(itunes, AC_HELP_STRING([--enable-itunes], [Enable iTunes library s use_itunes=true; 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_MUSEPACK, test x$use_musepack = 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)]), [ case "$withval" in @@ -184,6 +189,16 @@ if test x$use_itunes = xtrue; then PKG_CHECK_MODULES(LIBPLIST, [ libplist >= 0.16 ]) 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 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 *-*-linux-*) if test x$use_oss4 != xtrue; then diff --git a/forked-daapd.conf b/forked-daapd.conf index 429820cf..735e63e6 100644 --- a/forked-daapd.conf +++ b/forked-daapd.conf @@ -113,3 +113,11 @@ audio { # AirPlay password # 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" +} diff --git a/src/Makefile.am b/src/Makefile.am index 92c2ea4e..42ceec4b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -13,6 +13,10 @@ if COND_ITUNES ITUNESSRC=filescanner_itunes.c endif +if COND_SPOTIFY +SPOTIFYSRC=spotify.c spotify.h +endif + if COND_ALSA ALSASRC=laudio_alsa.c endif @@ -59,13 +63,13 @@ 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@ + @LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @SPOTIFY_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@ + @LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @SPOTIFY_LIBS@ forked_daapd_SOURCES = main.c \ db.c db.h \ @@ -95,6 +99,7 @@ forked_daapd_SOURCES = main.c \ evrtsp/rtsp.c evrtp/evrtsp.h \ evrtsp/rtsp-internal.h evrtsp/log.h \ scan-wma.c \ + $(SPOTIFYSRC) \ $(FLACSRC) $(MUSEPACKSRC) nodist_forked_daapd_SOURCES = \ diff --git a/src/artwork.c b/src/artwork.c index 20620dfd..ae790466 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -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) 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; @@ -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) 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))) { @@ -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) 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))) { @@ -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) 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))) { diff --git a/src/conffile.c b/src/conffile.c index c9928a65..3ad21589 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -100,6 +100,14 @@ static cfg_opt_t sec_airplay[] = 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 */ 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("audio", sec_audio, CFGF_NONE), CFG_SEC("airplay", sec_airplay, CFGF_MULTI | CFGF_TITLE), + CFG_SEC("spotify", sec_spotify, CFGF_NONE), CFG_END() }; diff --git a/src/db.c b/src/db.c index 56f095a0..25639542 100644 --- a/src/db.c +++ b/src/db.c @@ -273,6 +273,7 @@ static const char *sort_clause[] = "ORDER BY f.title_sort ASC", "ORDER BY f.album_sort ASC, f.disc ASC, f.track ASC", "ORDER BY f.album_artist_sort ASC", + "ORDER BY f.special_id DESC, f.title ASC", }; static char *db_path; @@ -876,6 +877,7 @@ db_build_query_pls(struct query_params *qp, char **q) { char *query; char *idx; + const char *sort; int ret; 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) return -1; + sort = sort_clause[qp->sort]; + 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) - 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) - 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 - 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) { @@ -1281,10 +1285,10 @@ db_build_query_browse(struct query_params *qp, char *field, char *sort_field, ch int ret; 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); 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); if (!count) @@ -1323,16 +1327,16 @@ db_build_query_browse(struct query_params *qp, char *field, char *sort_field, ch } 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); 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); 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); 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); free(sort); @@ -3045,6 +3049,41 @@ db_pl_add_item_byid(int plid, int fileid) #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 db_pl_clear_items(int id) { @@ -3427,6 +3466,79 @@ db_pairing_fetch_byguid(struct pairing_info *pi) #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 */ int diff --git a/src/db.h b/src/db.h index 05408075..f80dee18 100644 --- a/src/db.h +++ b/src/db.h @@ -21,6 +21,7 @@ enum sort_type { S_NAME, S_ALBUM, S_ARTIST, + S_PLAYLIST, }; #define Q_F_BROWSE (1 << 15) @@ -45,6 +46,7 @@ enum query_type { #define ARTWORK_OWN 3 #define ARTWORK_DIR 4 #define ARTWORK_PARENTDIR 5 +#define ARTWORK_SPOTIFY 6 struct query_params { /* Query parameters, filled in by caller */ @@ -430,6 +432,9 @@ db_pl_add_item_byid(int plid, int fileid); void db_pl_clear_items(int id); +int +db_pl_update(char *title, char *path, int id); + void db_pl_delete(int id); @@ -459,6 +464,15 @@ db_pairing_add(struct pairing_info *pi); int 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 */ int db_speaker_save(uint64_t id, int selected, int volume); diff --git a/src/filescanner.c b/src/filescanner.c index d0bf1b2f..b90f1cc0 100644 --- a/src/filescanner.c +++ b/src/filescanner.c @@ -60,6 +60,10 @@ #include "misc.h" #include "remote_pairing.h" +#ifdef HAVE_SPOTIFY_H +# include "spotify.h" +#endif + #define F_SCAN_BULK (1 << 0) #define F_SCAN_RESCAN (1 << 1) @@ -368,25 +372,23 @@ fixup_tags(struct media_file_info *mfi) 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 *ext; time_t stamp; int id; int ret; - filename = strrchr(file, '/'); - if (!filename) - { - DPRINTF(E_LOG, L_SCAN, "Could not determine filename for %s\n", file); - - return; - } + filename = strrchr(path, '/'); + if ((!filename) || (strlen(filename) == 1)) + filename = path; + else + filename++; /* File types which should never be processed */ - ext = strrchr(file, '.'); + ext = strrchr(path, '.'); if (ext) { 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 */ return; } - else if ((strlen(filename) > 1) && ((filename[1] == '_') || (filename[1] == '.'))) + else if ((filename[0] == '_') || (filename[0] == '.')) { /* Hidden files - don't scan */ 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) { @@ -420,78 +422,88 @@ process_media_file(char *file, time_t mtime, off_t size, int type, struct extinf return; } - memset(&mfi, 0, sizeof(struct media_file_info)); - - if (stamp) - mfi.id = db_file_id_bypath(file); - - mfi.fname = strdup(filename + 1); - if (!mfi.fname) + if (!external_mfi) { - 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; - } - - 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); + memset(mfi, 0, sizeof(struct media_file_info)); } 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 */ - if (extinf && extinf->found) - { - mfi.artist = strdup(extinf->artist); - mfi.title = strdup(extinf->artist); - mfi.album = strdup(extinf->title); - } - ret = scan_metadata_icy(file, &mfi); + DPRINTF(E_LOG, L_SCAN, "Out of memory for fname\n"); + goto out; } + 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) { - 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; } if (type & F_SCAN_TYPE_COMPILATION) - mfi.compilation = 1; + mfi->compilation = 1; if (type & F_SCAN_TYPE_PODCAST) - mfi.media_kind = 4; /* podcast */ + mfi->media_kind = 4; /* podcast */ if (type & F_SCAN_TYPE_AUDIOBOOK) - mfi.media_kind = 8; /* audiobook */ + mfi->media_kind = 8; /* audiobook */ - if (!mfi.item_kind) - mfi.item_kind = 2; /* music */ - if (!mfi.media_kind) - mfi.media_kind = 1; /* music */ + if (!mfi->item_kind) + mfi->item_kind = 2; /* music */ + if (!mfi->media_kind) + mfi->media_kind = 1; /* music */ - unicode_fixup_mfi(&mfi); + unicode_fixup_mfi(mfi); - fixup_tags(&mfi); + fixup_tags(mfi); - if (mfi.id == 0) - db_file_add(&mfi); + if (mfi->id == 0) + db_file_add(mfi); else - db_file_update(&mfi); + db_file_update(mfi); out: - free_mfi(&mfi, 1); + if (!external_mfi) + free_mfi(mfi, 0); } static void @@ -594,6 +606,14 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags) return; } +#ifdef HAVE_SPOTIFY_H + else if (strcmp(ext, ".spotify") == 0) + { + spotify_login(file); + + return; + } +#endif else if (strcmp(ext, ".force-rescan") == 0) { 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 */ - process_media_file(file, mtime, size, type, NULL); + filescanner_process_media(file, mtime, size, type, NULL); } /* 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 * playlist to update playlist items (relative items). */ - ie->mask |= IN_CREATE; + ie->mask |= IN_CLOSE_WRITE; 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); if (ret < 0) @@ -1475,6 +1495,12 @@ exit_cb(int fd, short event, void *arg) } +int +filescanner_status(void) +{ + return scan_exit; +} + /* Thread: main */ int filescanner_init(void) diff --git a/src/filescanner.h b/src/filescanner.h index 60a35f3e..cc81408c 100644 --- a/src/filescanner.h +++ b/src/filescanner.h @@ -4,10 +4,12 @@ #include "db.h" -#define F_SCAN_TYPE_PODCAST (1 << 0) -#define F_SCAN_TYPE_AUDIOBOOK (1 << 1) -#define F_SCAN_TYPE_COMPILATION (1 << 2) -#define F_SCAN_TYPE_URL (1 << 3) +#define F_SCAN_TYPE_FILE (1 << 0) +#define F_SCAN_TYPE_PODCAST (F_SCAN_TYPE_FILE | (1 << 1)) +#define F_SCAN_TYPE_AUDIOBOOK (F_SCAN_TYPE_FILE | (1 << 2)) +#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 filescanner_init(void); @@ -15,15 +17,11 @@ filescanner_init(void); void filescanner_deinit(void); -struct extinf_ctx -{ - char *artist; - char *title; - int found; -}; +int +filescanner_status(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 */ int diff --git a/src/filescanner_m3u.c b/src/filescanner_m3u.c index 0ab05888..4b71ad64 100644 --- a/src/filescanner_m3u.c +++ b/src/filescanner_m3u.c @@ -42,7 +42,7 @@ /* Get metadata from the EXTINF tag */ static int -extinf_get(char *string, struct extinf_ctx *extinf) +extinf_get(char *string, struct media_file_info *mfi, int *extinf) { char *ptr; @@ -54,20 +54,20 @@ extinf_get(char *string, struct extinf_ctx *extinf) return 0; /* New extinf found, so clear old data */ - if (extinf->found) + if (*extinf) { - free(extinf->artist); - free(extinf->title); + free(mfi->artist); + free(mfi->title); } - extinf->found = 1; - extinf->artist = strdup(ptr + 1); + *extinf = 1; + mfi->artist = strdup(ptr + 1); - ptr = strstr(extinf->artist, " -"); + ptr = strstr(mfi->artist, " -"); if (ptr && strlen(ptr) > 3) - extinf->title = strdup(ptr + 3); + mfi->title = strdup(ptr + 3); else - extinf->title = strdup(""); + mfi->title = strdup(""); if (ptr) *ptr = '\0'; @@ -75,28 +75,29 @@ extinf_get(char *string, struct extinf_ctx *extinf) } 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(extinf->title); + free(mfi->artist); + free(mfi->title); } - extinf->found = 0; + *extinf = 0; } void scan_m3u_playlist(char *file, time_t mtime) { FILE *fp; + struct media_file_info mfi; struct playlist_info *pli; struct stat sb; - struct extinf_ctx extinf; char buf[PATH_MAX]; char *entry; char *filename; char *ptr; size_t len; + int extinf; int pl_id; int mfi_id; 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); + memset(&mfi, 0, sizeof(struct media_file_info)); + ret = stat(file, &sb); 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); } - extinf.found = 0; + extinf = 0; while (fgets(buf, sizeof(buf), fp) != NULL) { @@ -189,7 +192,7 @@ scan_m3u_playlist(char *file, time_t mtime) continue; /* Saves metadata in extinf if EXTINF metadata line */ - if (extinf_get(buf, &extinf)) + if (extinf_get(buf, &mfi, &extinf)) continue; /* Check that first char is sane for a path */ @@ -209,10 +212,10 @@ scan_m3u_playlist(char *file, time_t mtime) continue; } - if (extinf.found) - DPRINTF(E_INFO, L_SCAN, "Playlist has EXTINF metadata, artist is '%s', title is '%s'\n", extinf.artist, extinf.title); + if (extinf) + 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 */ else @@ -271,7 +274,7 @@ scan_m3u_playlist(char *file, time_t mtime) if (ret < 0) DPRINTF(E_WARN, L_SCAN, "Could not add %s to playlist\n", filename); - extinf_reset(&extinf); + extinf_reset(&mfi, &extinf); free(filename); } diff --git a/src/httpd.c b/src/httpd.c index d1ba1ce3..227c07aa 100644 --- a/src/httpd.c +++ b/src/httpd.c @@ -395,8 +395,8 @@ httpd_stream_file(struct evhttp_request *req, int id) stream_cb = stream_chunk_xcode_cb; - st->xcode = transcode_setup(mfi, &st->size, 1); - if (!st->xcode) + ret = transcode_setup(&st->xcode, mfi, &st->size, 1); + if (ret < 0) { DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n"); diff --git a/src/httpd_daap.c b/src/httpd_daap.c index 3a24b94e..ac678615 100644 --- a/src/httpd_daap.c +++ b/src/httpd_daap.c @@ -73,7 +73,7 @@ struct uri_map { struct daap_session { int id; - + char *user_agent; struct event timeout; }; @@ -132,7 +132,12 @@ daap_session_free(void *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); } @@ -155,7 +160,7 @@ daap_session_timeout_cb(int fd, short what, void *arg) } static struct daap_session * -daap_session_register(void) +daap_session_register(const char *user_agent) { struct timeval tv; struct daap_session *s; @@ -175,6 +180,9 @@ daap_session_register(void) next_session_id++; + if (user_agent) + s->user_agent = strdup(user_agent); + if (DAAP_SESSION_TIMEOUT > 0) { evtimer_set(&s->timeout, daap_session_timeout_cb, s); 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)); + if (user_agent) + free(s->user_agent); + free(s); return NULL; } @@ -466,6 +477,47 @@ daap_sort_finalize(struct sort_ctx *ctx) 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 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); } - s = daap_session_register(); + s = daap_session_register(ua); if (!s) { 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 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 db_media_file_info dbmfi; struct evbuffer *song; @@ -1001,6 +1054,10 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf, int transcode; 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); 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)); get_query_params(query, &sort_headers, &qp); + if (playlist == -1) + user_agent_filter(s->user_agent, &qp); sctx = NULL; if (sort_headers) @@ -1242,26 +1301,15 @@ daap_reply_songlist_generic(struct evhttp_request *req, struct evbuffer *evbuf, static void 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); } static void daap_reply_plsonglist(struct evhttp_request *req, struct evbuffer *evbuf, char **uri, struct evkeyvalq *query) { - struct daap_session *s; int playlist; int ret; - s = daap_session_find(req, query, evbuf); - if (!s) - return; - ret = safe_atoi32(uri[3], &playlist); 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)); get_query_params(query, NULL, &qp); qp.type = Q_PL; + qp.sort = S_PLAYLIST; ret = db_query_start(&qp); 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)); get_query_params(query, &sort_headers, &qp); + user_agent_filter(s->user_agent, &qp); param = evhttp_find_header(query, "group-type"); 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)); get_query_params(query, &sort_headers, &qp); + user_agent_filter(s->user_agent, &qp); if (strcmp(uri[3], "artists") == 0) { diff --git a/src/logger.c b/src/logger.c index b5a590ec..7feba737 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" }; +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 diff --git a/src/logger.h b/src/logger.h index 21ae92e3..0498be21 100644 --- a/src/logger.h +++ b/src/logger.h @@ -26,8 +26,9 @@ #define L_LAUDIO 17 #define L_DMAP 18 #define L_DBPERF 19 +#define L_SPOTIFY 20 -#define N_LOGDOMAINS 20 +#define N_LOGDOMAINS 21 /* Severities */ #define E_FATAL 0 diff --git a/src/main.c b/src/main.c index d5eeb328..c7fcf01e 100644 --- a/src/main.c +++ b/src/main.c @@ -67,6 +67,9 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL; # include "ffmpeg_url_evbuffer.h" #endif +#ifdef HAVE_SPOTIFY_H +# include "spotify.h" +#endif #define PIDFILE STATEDIR "/run/" PACKAGE ".pid" @@ -678,6 +681,15 @@ main(int argc, char **argv) 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 */ ret = player_init(); if (ret != 0) @@ -786,6 +798,10 @@ main(int argc, char **argv) player_deinit(); 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"); filescanner_deinit(); diff --git a/src/player.c b/src/player.c index 6ae79835..631d19cf 100644 --- a/src/player.c +++ b/src/player.c @@ -59,6 +59,9 @@ #include "raop.h" #include "laudio.h" +#ifdef HAVE_SPOTIFY_H +# include "spotify.h" +#endif #ifndef MIN # define MIN(a, b) ((a < b) ? a : b) @@ -882,8 +885,19 @@ player_queue_make_pl(int plid, uint32_t *id) static void source_free(struct player_source *ps) { - if (ps->ctx) - transcode_cleanup(ps->ctx); + switch (ps->type) + { + 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); } @@ -895,11 +909,22 @@ source_stop(struct player_source *ps) while (ps) { - if (ps->ctx) + switch (ps->type) { - transcode_cleanup(ps->ctx); - ps->ctx = NULL; - } + case SOURCE_FFMPEG: + 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; ps = ps->play_next; @@ -989,7 +1014,9 @@ static int source_open(struct player_source *ps, int no_md) { struct media_file_info *mfi; + int ret; + ps->setup_done = 0; ps->stream_start = 0; ps->output_start = 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); - 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); - if (!ps->ctx) + if (ret < 0) { 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) metadata_send(ps, (player_state == PLAY_PLAYING) ? 0 : 1); + ps->setup_done = 1; + return 0; } @@ -1067,7 +1109,7 @@ source_next(int force) if (!cur_streaming) break; - if (cur_streaming->ctx) + if ((cur_streaming->type == SOURCE_FFMPEG) && cur_streaming->ctx) { ret = transcode_seek(cur_streaming->ctx, 0); @@ -1276,10 +1318,13 @@ source_check(void) { cur_playing = cur_playing->play_next; - if (ps->ctx) + if (ps->setup_done) { - transcode_cleanup(ps->ctx); - ps->ctx = NULL; + if ((ps->type == SOURCE_FFMPEG) && ps->ctx) + { + transcode_cleanup(ps->ctx); + ps->ctx = NULL; + } ps->play_next = NULL; } } @@ -1324,10 +1369,13 @@ source_check(void) cur_playing->stream_start = ps->end + 1; cur_playing->output_start = cur_playing->stream_start; - if (ps->ctx) + if (ps->setup_done) { - transcode_cleanup(ps->ctx); - ps->ctx = NULL; + if ((ps->type == SOURCE_FFMPEG) && ps->ctx) + { + transcode_cleanup(ps->ctx); + ps->ctx = 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) { - 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) { /* EOF or error */ @@ -2555,7 +2618,20 @@ playback_seek_bh(struct player_command *cmd) ps->end = 0; /* 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) { playback_abort(); @@ -2596,7 +2672,20 @@ playback_pause_bh(struct player_command *cmd) pos -= ps->stream_start; 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) { playback_abort(); diff --git a/src/player.h b/src/player.h index 7e42a3c9..879b6901 100644 --- a/src/player.h +++ b/src/player.h @@ -33,6 +33,11 @@ enum repeat_mode { REPEAT_ALL = 2, }; +enum source_type { + SOURCE_FFMPEG = 0, + SOURCE_SPOTIFY = 1, +}; + struct spk_flags { unsigned selected:1; unsigned has_password:1; @@ -60,6 +65,9 @@ struct player_source { uint32_t id; + enum source_type type; + int setup_done; + uint64_t stream_start; uint64_t output_start; uint64_t end; diff --git a/src/spotify.c b/src/spotify.c new file mode 100644 index 00000000..06d67cfb --- /dev/null +++ b/src/spotify.c @@ -0,0 +1,1548 @@ +/* + * Copyright (C) 2014 Espen Jürgensen + * + * Stiched together from libspotify examples + * + * 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 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "spotify.h" +#include "logger.h" +#include "conffile.h" +#include "filescanner.h" + + +/* --- Types --- */ +typedef struct audio_fifo_data { + TAILQ_ENTRY(audio_fifo_data) link; + int nsamples; + int16_t samples[0]; +} audio_fifo_data_t; + +typedef struct audio_fifo { + TAILQ_HEAD(, audio_fifo_data) q; + int qlen; + int fullcount; + pthread_mutex_t mutex; + pthread_cond_t cond; +} audio_fifo_t; + +enum spotify_event + { + SPOTIFY_EVENT_NONE, + SPOTIFY_EVENT_LIBCB, + SPOTIFY_EVENT_PLAY, + SPOTIFY_EVENT_PAUSE, + SPOTIFY_EVENT_STOP, + SPOTIFY_EVENT_SEEK, + SPOTIFY_EVENT_EXIT, + SPOTIFY_EVENT_RESUME, + }; + +enum spotify_state + { + SPOTIFY_STATE_INACTIVE, + SPOTIFY_STATE_WAIT, + SPOTIFY_STATE_PLAYING, + SPOTIFY_STATE_PAUSED, + SPOTIFY_STATE_STOPPED, + SPOTIFY_STATE_SEEKED, + SPOTIFY_STATE_EXITING, + }; + +/* Context for communicating with player */ +struct spotify_ctx + { + sp_link *link; + int seek_ms; + }; + +/** + * The application key is specific to forked-daapd, and allows Spotify + * to produce statistics on how their service is used. + */ +const uint8_t g_appkey[] = { + 0x01, 0xC6, 0x9D, 0x18, 0xA4, 0xF7, 0x79, 0x12, 0x43, 0x55, 0x0F, 0xAD, 0xBF, 0x23, 0x23, 0x10, + 0x2E, 0x51, 0x46, 0x8F, 0x06, 0x3D, 0xEE, 0xC3, 0xF0, 0x2A, 0x5D, 0x8E, 0x72, 0x35, 0xD1, 0x21, + 0x44, 0xE3, 0x19, 0x80, 0xED, 0xD5, 0xAD, 0xE6, 0xE1, 0xDD, 0xBE, 0xCB, 0xA9, 0x84, 0xBD, 0xC2, + 0xAF, 0xB1, 0xF2, 0xD5, 0x87, 0xFC, 0x35, 0xD6, 0x1C, 0x5F, 0x5B, 0x76, 0x38, 0x1D, 0x6E, 0x49, + 0x6D, 0x85, 0x15, 0xCD, 0x38, 0x14, 0xD6, 0xB8, 0xFE, 0x05, 0x0A, 0xAC, 0x9B, 0x31, 0xD1, 0xC0, + 0xAF, 0x16, 0x78, 0x48, 0x49, 0x27, 0x41, 0xCA, 0xAF, 0x07, 0xEC, 0x10, 0x5D, 0x19, 0x43, 0x2E, + 0x84, 0xEB, 0x43, 0x5D, 0x4B, 0xBF, 0xD0, 0x5C, 0xDF, 0x3D, 0x12, 0x6D, 0x1C, 0x76, 0x4E, 0x9F, + 0xBF, 0x14, 0xC9, 0x46, 0x95, 0x99, 0x32, 0x6A, 0xC2, 0xF1, 0x89, 0xA4, 0xB3, 0xF3, 0xA0, 0xEB, + 0xDA, 0x84, 0x67, 0x27, 0x07, 0x1F, 0xF6, 0x19, 0xAC, 0xF1, 0xB8, 0xB6, 0xCF, 0xAB, 0xF8, 0x0A, + 0xEE, 0x4D, 0xAC, 0xC2, 0x39, 0x63, 0x50, 0x13, 0x7B, 0x51, 0x3A, 0x50, 0xE0, 0x03, 0x6E, 0xB7, + 0x17, 0xEE, 0x58, 0xCE, 0xF8, 0x15, 0x3C, 0x70, 0xDE, 0xE6, 0xEB, 0xE6, 0xD4, 0x2C, 0x27, 0xB9, + 0xCA, 0x15, 0xCE, 0x2E, 0x31, 0x54, 0xF5, 0x0A, 0x98, 0x8D, 0x78, 0xE5, 0xB6, 0xF8, 0xE4, 0x62, + 0x43, 0xAA, 0x37, 0x93, 0xFF, 0xE3, 0xAB, 0x17, 0xC5, 0x81, 0x4F, 0xFD, 0xF1, 0x84, 0xE1, 0x8A, + 0x99, 0xB0, 0x1D, 0x85, 0x80, 0xA2, 0x49, 0x35, 0x8D, 0xDD, 0xBC, 0x74, 0x0B, 0xBA, 0x33, 0x5B, + 0xD5, 0x7A, 0xB9, 0x2F, 0x9B, 0x24, 0xA5, 0xAB, 0xF6, 0x1E, 0xE3, 0xA3, 0xA8, 0x0D, 0x1E, 0x48, + 0xF7, 0xDB, 0xE2, 0x54, 0x65, 0x43, 0xA6, 0xD3, 0x3F, 0x2C, 0x9B, 0x13, 0x9A, 0xBE, 0x0F, 0x4D, + 0x51, 0xC3, 0x73, 0xA5, 0xFE, 0xFC, 0x93, 0x12, 0xEF, 0x9C, 0x4D, 0x68, 0xE3, 0xDA, 0x52, 0x67, + 0x28, 0x41, 0x17, 0x22, 0x3E, 0x33, 0xB0, 0x3A, 0xFB, 0x44, 0xB0, 0x2E, 0xA6, 0xD2, 0x95, 0xC0, + 0x9A, 0xBA, 0x32, 0xA3, 0xC5, 0xFE, 0x86, 0x5D, 0xC8, 0xBB, 0xB5, 0xDE, 0x92, 0x8C, 0x7D, 0xE4, + 0x03, 0xD4, 0xF9, 0xAE, 0x41, 0xE3, 0xBD, 0x35, 0x4B, 0x94, 0x27, 0xE0, 0x12, 0x21, 0x46, 0xE9, + 0x09, +}; + +// Spotify thread +static pthread_t tid_spotify; +// Synchronization mutex for the spotify thread +static pthread_mutex_t g_notify_mutex; +// Synchronization condition variable for the spotify thread +static pthread_cond_t g_notify_cond; +// Synchronization mutex for the spotify player state +static pthread_mutex_t g_state_mutex; +// Synchronization condition variable for the spotify player state +static pthread_cond_t g_state_cond; +// Synchronization variable telling the spotify thread to process events +static enum spotify_event g_event; +// Synchronization variable telling the caller thread about the state +static enum spotify_state g_state; + +// The global session handle +static sp_session *g_sess; +// The global library handle +static void *g_libhandle; +// The global spotify context +static struct spotify_ctx g_ctx; + +// Synchronization mutex for the database (possibly useless) +static pthread_mutex_t g_db_mutex; + +// Audio fifo +static audio_fifo_t *g_audio_fifo; + + +// This section defines and assigns function pointers to the libspotify functions +// The arguments and return values must be in sync with the spotify api +// Please scroll through the ugliness which follows + +typedef const char* (*fptr_sp_error_message_t)(sp_error error); + +typedef sp_error (*fptr_sp_session_create_t)(const sp_session_config *config, sp_session **sess); +typedef sp_error (*fptr_sp_session_release_t)(sp_session *sess); +typedef sp_error (*fptr_sp_session_login_t)(sp_session *session, const char *username, const char *password, bool remember_me, const char *blob); +typedef sp_error (*fptr_sp_session_relogin_t)(sp_session *session); +typedef sp_error (*fptr_sp_session_logout_t)(sp_session *session); +typedef sp_error (*fptr_sp_session_process_events_t)(sp_session *session, int *next_timeout); +typedef sp_playlistcontainer* (*fptr_sp_session_playlistcontainer_t)(sp_session *session); +typedef sp_error (*fptr_sp_session_player_load_t)(sp_session *session, sp_track *track); +typedef sp_error (*fptr_sp_session_player_unload_t)(sp_session *session); +typedef sp_error (*fptr_sp_session_player_play_t)(sp_session *session, bool play); +typedef sp_error (*fptr_sp_session_player_seek_t)(sp_session *session, int offset); + +typedef sp_error (*fptr_sp_playlistcontainer_add_callbacks_t)(sp_playlistcontainer *pc, sp_playlistcontainer_callbacks *callbacks, void *userdata); +typedef int (*fptr_sp_playlistcontainer_num_playlists_t)(sp_playlistcontainer *pc); +typedef sp_playlist* (*fptr_sp_playlistcontainer_playlist_t)(sp_playlistcontainer *pc, int index); + +typedef sp_error (*fptr_sp_playlist_add_callbacks_t)(sp_playlist *playlist, sp_playlist_callbacks *callbacks, void *userdata); +typedef const char* (*fptr_sp_playlist_name_t)(sp_playlist *playlist); +typedef sp_error (*fptr_sp_playlist_remove_callbacks_t)(sp_playlist *playlist, sp_playlist_callbacks *callbacks, void *userdata); +typedef int (*fptr_sp_playlist_num_tracks_t)(sp_playlist *playlist); +typedef sp_track* (*fptr_sp_playlist_track_t)(sp_playlist *playlist, int index); +typedef bool (*fptr_sp_playlist_is_loaded_t)(sp_playlist *playlist); + +typedef sp_error (*fptr_sp_track_error_t)(sp_track *track); +typedef bool (*fptr_sp_track_is_loaded_t)(sp_track *track); +typedef const char* (*fptr_sp_track_name_t)(sp_track *track); +typedef int (*fptr_sp_track_duration_t)(sp_track *track); +typedef int (*fptr_sp_track_index_t)(sp_track *track); +typedef int (*fptr_sp_track_disc_t)(sp_track *track); +typedef sp_album* (*fptr_sp_track_album_t)(sp_track *track); + +typedef sp_link* (*fptr_sp_link_create_from_playlist_t)(sp_playlist *playlist); +typedef sp_link* (*fptr_sp_link_create_from_track_t)(sp_track *track, int offset); +typedef sp_link* (*fptr_sp_link_create_from_string_t)(const char *link); +typedef int (*fptr_sp_link_as_string_t)(sp_link *link, char *buffer, int buffer_size); +typedef sp_track* (*fptr_sp_link_as_track_t)(sp_link *link); +typedef sp_error (*fptr_sp_link_release_t)(sp_link *link); + +typedef const char* (*fptr_sp_album_name_t)(sp_album *album); +typedef sp_artist* (*fptr_sp_album_artist_t)(sp_album *album); +typedef int (*fptr_sp_album_year_t)(sp_album *album); +typedef sp_albumtype (*fptr_sp_album_type_t)(sp_album *album); + +typedef const char* (*fptr_sp_artist_name_t)(sp_artist *artist); + +/* Define actual function pointers */ +fptr_sp_error_message_t fptr_sp_error_message; + +fptr_sp_session_create_t fptr_sp_session_create; +fptr_sp_session_release_t fptr_sp_session_release; +fptr_sp_session_login_t fptr_sp_session_login; +fptr_sp_session_relogin_t fptr_sp_session_relogin; +fptr_sp_session_logout_t fptr_sp_session_logout; +fptr_sp_session_playlistcontainer_t fptr_sp_session_playlistcontainer; +fptr_sp_session_process_events_t fptr_sp_session_process_events; +fptr_sp_session_player_load_t fptr_sp_session_player_load; +fptr_sp_session_player_unload_t fptr_sp_session_player_unload; +fptr_sp_session_player_play_t fptr_sp_session_player_play; +fptr_sp_session_player_seek_t fptr_sp_session_player_seek; + +fptr_sp_playlistcontainer_add_callbacks_t fptr_sp_playlistcontainer_add_callbacks; +fptr_sp_playlistcontainer_num_playlists_t fptr_sp_playlistcontainer_num_playlists; +fptr_sp_playlistcontainer_playlist_t fptr_sp_playlistcontainer_playlist; + +fptr_sp_playlist_add_callbacks_t fptr_sp_playlist_add_callbacks; +fptr_sp_playlist_name_t fptr_sp_playlist_name; +fptr_sp_playlist_remove_callbacks_t fptr_sp_playlist_remove_callbacks; +fptr_sp_playlist_num_tracks_t fptr_sp_playlist_num_tracks; +fptr_sp_playlist_track_t fptr_sp_playlist_track; +fptr_sp_playlist_is_loaded_t fptr_sp_playlist_is_loaded; + +fptr_sp_track_error_t fptr_sp_track_error; +fptr_sp_track_is_loaded_t fptr_sp_track_is_loaded; +fptr_sp_track_name_t fptr_sp_track_name; +fptr_sp_track_duration_t fptr_sp_track_duration; +fptr_sp_track_index_t fptr_sp_track_index; +fptr_sp_track_disc_t fptr_sp_track_disc; +fptr_sp_track_album_t fptr_sp_track_album; + +fptr_sp_link_create_from_playlist_t fptr_sp_link_create_from_playlist; +fptr_sp_link_create_from_track_t fptr_sp_link_create_from_track; +fptr_sp_link_create_from_string_t fptr_sp_link_create_from_string; +fptr_sp_link_as_string_t fptr_sp_link_as_string; +fptr_sp_link_as_track_t fptr_sp_link_as_track; +fptr_sp_link_release_t fptr_sp_link_release; + +fptr_sp_album_name_t fptr_sp_album_name; +fptr_sp_album_artist_t fptr_sp_album_artist; +fptr_sp_album_year_t fptr_sp_album_year; +fptr_sp_album_type_t fptr_sp_album_type; + +fptr_sp_artist_name_t fptr_sp_artist_name; + +/* Assign function pointers to libspotify symbol */ +static int +fptr_assign_all() +{ + void *h; + char *err; + int ret; + + h = g_libhandle; + + // The following is non-ISO compliant + ret = (fptr_sp_error_message = dlsym(h, "sp_error_message")) + && (fptr_sp_session_create = dlsym(h, "sp_session_create")) + && (fptr_sp_session_release = dlsym(h, "sp_session_release")) + && (fptr_sp_session_login = dlsym(h, "sp_session_login")) + && (fptr_sp_session_relogin = dlsym(h, "sp_session_relogin")) + && (fptr_sp_session_logout = dlsym(h, "sp_session_logout")) + && (fptr_sp_session_playlistcontainer = dlsym(h, "sp_session_playlistcontainer")) + && (fptr_sp_session_process_events = dlsym(h, "sp_session_process_events")) + && (fptr_sp_session_player_load = dlsym(h, "sp_session_player_load")) + && (fptr_sp_session_player_unload = dlsym(h, "sp_session_player_unload")) + && (fptr_sp_session_player_play = dlsym(h, "sp_session_player_play")) + && (fptr_sp_session_player_seek = dlsym(h, "sp_session_player_seek")) + && (fptr_sp_playlistcontainer_add_callbacks = dlsym(h, "sp_playlistcontainer_add_callbacks")) + && (fptr_sp_playlistcontainer_num_playlists = dlsym(h, "sp_playlistcontainer_num_playlists")) + && (fptr_sp_playlistcontainer_playlist = dlsym(h, "sp_playlistcontainer_playlist")) + && (fptr_sp_playlist_add_callbacks = dlsym(h, "sp_playlist_add_callbacks")) + && (fptr_sp_playlist_name = dlsym(h, "sp_playlist_name")) + && (fptr_sp_playlist_remove_callbacks = dlsym(h, "sp_playlist_remove_callbacks")) + && (fptr_sp_playlist_num_tracks = dlsym(h, "sp_playlist_num_tracks")) + && (fptr_sp_playlist_track = dlsym(h, "sp_playlist_track")) + && (fptr_sp_playlist_is_loaded = dlsym(h, "sp_playlist_is_loaded")) + && (fptr_sp_track_error = dlsym(h, "sp_track_error")) + && (fptr_sp_track_is_loaded = dlsym(h, "sp_track_is_loaded")) + && (fptr_sp_track_name = dlsym(h, "sp_track_name")) + && (fptr_sp_track_duration = dlsym(h, "sp_track_duration")) + && (fptr_sp_track_index = dlsym(h, "sp_track_index")) + && (fptr_sp_track_disc = dlsym(h, "sp_track_disc")) + && (fptr_sp_track_album = dlsym(h, "sp_track_album")) + && (fptr_sp_link_create_from_playlist = dlsym(h, "sp_link_create_from_playlist")) + && (fptr_sp_link_create_from_track = dlsym(h, "sp_link_create_from_track")) + && (fptr_sp_link_create_from_string = dlsym(h, "sp_link_create_from_string")) + && (fptr_sp_link_as_string = dlsym(h, "sp_link_as_string")) + && (fptr_sp_link_as_track = dlsym(h, "sp_link_as_track")) + && (fptr_sp_link_release = dlsym(h, "sp_link_release")) + && (fptr_sp_album_name = dlsym(h, "sp_album_name")) + && (fptr_sp_album_artist = dlsym(h, "sp_album_artist")) + && (fptr_sp_album_year = dlsym(h, "sp_album_year")) + && (fptr_sp_album_type = dlsym(h, "sp_album_type")) + && (fptr_sp_artist_name = dlsym(h, "sp_artist_name")) + ; + + err = dlerror(); + + if (ret && !err) + return ret; + else if (err) + DPRINTF(E_LOG, L_SPOTIFY, "Assignment error (%d): %s\n", ret, err); + else + DPRINTF(E_LOG, L_SPOTIFY, "Unknown assignment error (%d)\n", ret); + + return -1; +} +// End of ugly part + + +/* -------------------------- PLAYLIST HELPERS ------------------------- */ + +static int +spotify_metadata_get(sp_track *track, struct media_file_info *mfi) +{ + sp_album *album; + sp_artist *artist; + sp_albumtype albumtype; + + album = fptr_sp_track_album(track); + if (!album) + return -1; + + artist = fptr_sp_album_artist(album); + if (!artist) + return -1; + + albumtype = fptr_sp_album_type(album); + + mfi->title = strdup(fptr_sp_track_name(track)); + mfi->album = strdup(fptr_sp_album_name(album)); + mfi->artist = strdup(fptr_sp_artist_name(artist)); + mfi->year = fptr_sp_album_year(album); + mfi->song_length = fptr_sp_track_duration(track); + mfi->track = fptr_sp_track_index(track); + mfi->disc = fptr_sp_track_disc(track); + mfi->compilation = (albumtype == SP_ALBUMTYPE_COMPILATION); + mfi->artwork = ARTWORK_SPOTIFY; + mfi->type = strdup("spotify"); + mfi->codectype = strdup("wav"); + mfi->description = strdup("Spotify audio"); + + return 0; +} + +static int +spotify_track_save(int plid, sp_track *track) +{ + struct media_file_info mfi; + sp_link *link; + char url[1024]; + int ret; + + if (!fptr_sp_track_is_loaded(track)) + { + DPRINTF(E_INFO, L_SPOTIFY, "Metadata for track not ready yet\n"); + return 0; + } + + link = fptr_sp_link_create_from_track(track, 0); + if (!link) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not create link for track\n"); + return -1; + } + + ret = fptr_sp_link_as_string(link, url, sizeof(url)); + if (ret == sizeof(url)) + { + DPRINTF(E_DBG, L_SPOTIFY, "Spotify link truncated: %s\n", url); + } + fptr_sp_link_release(link); + + /* Add to playlistitems table */ + ret = db_pl_add_item_bypath(plid, url); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not save playlist item\n"); + return -1; + } + + memset(&mfi, 0, sizeof(struct media_file_info)); + + ret = spotify_metadata_get(track, &mfi); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Metadata missing (but track should be loaded?)\n"); + free_mfi(&mfi, 1); + return -1; + } + + filescanner_process_media(url, time(NULL), 0, F_SCAN_TYPE_SPOTIFY, &mfi); + + free_mfi(&mfi, 1); + + return 0; +} + +static int +spotify_playlist_save(sp_playlist *pl) +{ + struct playlist_info *pli; + sp_track *track; + sp_link *link; + char url[1024]; + const char *name; + char title[512]; + int plid; + int num_tracks; + int ret; + int i; + + if (!fptr_sp_playlist_is_loaded(pl)) + { + DPRINTF(E_DBG, L_SPOTIFY, "Playlist still not loaded - wait for rename callback\n"); + return 0; + } + + name = fptr_sp_playlist_name(pl); + + DPRINTF(E_DBG, L_SPOTIFY, "Saving playlist: %s\n", name); + + /* Save playlist (playlists table) */ + link = fptr_sp_link_create_from_playlist(pl); + if (!link) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not create link for playlist (wait)\n"); + return -1; + } + + ret = fptr_sp_link_as_string(link, url, sizeof(url)); + if (ret == sizeof(url)) + { + DPRINTF(E_DBG, L_SPOTIFY, "Spotify link truncated: %s\n", url); + } + fptr_sp_link_release(link); + + sleep(1); // Primitive way of preventing database locking (the mutex wasn't working) + + pli = db_pl_fetch_bypath(url); + snprintf(title, sizeof(title), "[s] %s", name); + + if (pli) + { + DPRINTF(E_DBG, L_SPOTIFY, "Playlist found (%s, link %s), updating\n", name, url); + + plid = pli->id; + + free_pli(pli, 0); + + ret = db_pl_update(title, url, plid); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error updating playlist (%s, link %s)\n", name, url); + return -1; + } + + db_pl_ping(plid); + db_pl_clear_items(plid); + } + else + { + DPRINTF(E_DBG, L_SPOTIFY, "Adding playlist (%s, link %s)\n", name, url); + + ret = db_pl_add(title, url, &plid); + if ((ret < 0) || (plid < 1)) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist (%s, link %s, ret %d, plid %d)\n", name, url, ret, plid); + return -1; + } + } + + /* Save tracks and playlistitems (files and playlistitems table) */ + num_tracks = fptr_sp_playlist_num_tracks(pl); + for (i = 0; i < num_tracks; i++) + { + track = fptr_sp_playlist_track(pl, i); + if (!track) + { + DPRINTF(E_LOG, L_SPOTIFY, "Track %d in playlist %s (id %d) is invalid\n", i, name, plid); + continue; + } + + ret = spotify_track_save(plid, track); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error saving track %d to playlist %s (id %d)\n", i, name, plid); + continue; + } + } + + return plid; +} + +/* -------------------------- AUDIO HELPER ------------------------------- */ + +static void +spotify_audio_fifo_flush(void) +{ + audio_fifo_data_t *afd; + + DPRINTF(E_DBG, L_SPOTIFY, "Flushing fifo\n"); + + pthread_mutex_lock(&g_audio_fifo->mutex); + + while((afd = TAILQ_FIRST(&g_audio_fifo->q))) { + TAILQ_REMOVE(&g_audio_fifo->q, afd, link); + free(afd); + } + + g_audio_fifo->qlen = 0; + g_audio_fifo->fullcount = 0; + pthread_mutex_unlock(&g_audio_fifo->mutex); + + DPRINTF(E_DBG, L_SPOTIFY, "fifo flushed\n"); +} + +/* -------------------------- PLAYLIST CALLBACKS ------------------------- */ +/** + * Called when a playlist is updating or is done updating + * + * This is called before and after a series of changes are applied to the + * playlist. It allows e.g. the user interface to defer updating until the + * entire operation is complete. + * + * @param[in] pl Playlist object + * @param[in] done True iff the update is completed + * @param[in] userdata Userdata passed to sp_playlist_add_callbacks() + */ +static void playlist_update_in_progress(sp_playlist *pl, bool done, void *userdata) +{ + DPRINTF(E_DBG, L_SPOTIFY, "Playlist update in progress (status %d): %s\n", done, fptr_sp_playlist_name(pl)); + + if (done) + { + pthread_mutex_lock(&g_db_mutex); + spotify_playlist_save(pl); + pthread_mutex_unlock(&g_db_mutex); + } +} + +static void playlist_metadata_updated(sp_playlist *pl, void *userdata) +{ + DPRINTF(E_DBG, L_SPOTIFY, "Playlist metadata updated: %s\n", fptr_sp_playlist_name(pl)); + + pthread_mutex_lock(&g_db_mutex); + spotify_playlist_save(pl); + pthread_mutex_unlock(&g_db_mutex); +} + +/** + * The callbacks we are interested in for individual playlists. + */ +static sp_playlist_callbacks pl_callbacks = { + .playlist_update_in_progress = &playlist_update_in_progress, + .playlist_metadata_updated = &playlist_metadata_updated, +}; + + +/* -------------------- PLAYLIST CONTAINER CALLBACKS --------------------- */ +/** + * Callback from libspotify, telling us a playlist was added to the playlist container. + * + * We add our playlist callbacks to the newly added playlist. + * + * @param pc The playlist container handle + * @param pl The playlist handle + * @param position Index of the added playlist + * @param userdata The opaque pointer + */ +static void playlist_added(sp_playlistcontainer *pc, sp_playlist *pl, + int position, void *userdata) +{ + DPRINTF(E_INFO, L_SPOTIFY, "Playlist added: %s (%d tracks)\n", fptr_sp_playlist_name(pl), fptr_sp_playlist_num_tracks(pl)); + + fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); + + pthread_mutex_lock(&g_db_mutex); + spotify_playlist_save(pl); + pthread_mutex_unlock(&g_db_mutex); +} + +/** + * Callback from libspotify, telling us a playlist was removed from the playlist container. + * + * This is the place to remove our playlist callbacks. + * + * @param pc The playlist container handle + * @param pl The playlist handle + * @param position Index of the removed playlist + * @param userdata The opaque pointer + */ +static void +playlist_removed(sp_playlistcontainer *pc, sp_playlist *pl, int position, void *userdata) +{ + struct playlist_info *pli; + sp_link *link; + char url[1024]; + int plid; + int ret; + + DPRINTF(E_INFO, L_SPOTIFY, "Playlist removed: %s\n", fptr_sp_playlist_name(pl)); + + fptr_sp_playlist_remove_callbacks(pl, &pl_callbacks, NULL); + + link = fptr_sp_link_create_from_playlist(pl); + if (!link) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not find link for deleted playlist\n"); + return; + } + + ret = fptr_sp_link_as_string(link, url, sizeof(url)); + if (ret == sizeof(url)) + { + DPRINTF(E_DBG, L_SPOTIFY, "Spotify link truncated: %s\n", url); + } + fptr_sp_link_release(link); + + pthread_mutex_lock(&g_db_mutex); + + pli = db_pl_fetch_bypath(url); + + if (!pli) + { + DPRINTF(E_DBG, L_SPOTIFY, "Playlist %s not found, can't delete\n", url); + pthread_mutex_unlock(&g_db_mutex); + return; + } + + plid = pli->id; + + free_pli(pli, 0); + + db_spotify_pl_delete(plid); + + pthread_mutex_unlock(&g_db_mutex); +} + +/** + * Callback from libspotify, telling us the rootlist is fully synchronized + * + * @param pc The playlist container handle + * @param userdata The opaque pointer + */ +static void +container_loaded(sp_playlistcontainer *pc, void *userdata) +{ + int num; + + num = fptr_sp_playlistcontainer_num_playlists(pc); + + DPRINTF(E_INFO, L_SPOTIFY, "Rootlist synchronized (%d playlists)\n", num); +} + + +/** + * The playlist container callbacks + */ +static sp_playlistcontainer_callbacks pc_callbacks = { + .playlist_added = &playlist_added, + .playlist_removed = &playlist_removed, + .container_loaded = &container_loaded, +}; + + +/* --------------------------- SESSION CALLBACKS ------------------------- */ +/** + * This callback is called when an attempt to login has succeeded or failed. + * + * @sa sp_session_callbacks#logged_in + */ +static void +logged_in(sp_session *sess, sp_error error) +{ + sp_playlist *pl; + sp_playlistcontainer *pc; + int i; + + if (SP_ERROR_OK != error) + { + DPRINTF(E_LOG, L_SPOTIFY, "Login failed: %s\n", fptr_sp_error_message(error)); + pthread_exit(NULL); + } + + DPRINTF(E_LOG, L_SPOTIFY, "Login to Spotify succeeded. Reloading playlists.\n"); + + pthread_mutex_lock(&g_db_mutex); + db_spotify_purge(); + pthread_mutex_unlock(&g_db_mutex); + + pc = fptr_sp_session_playlistcontainer(sess); + + fptr_sp_playlistcontainer_add_callbacks(pc, &pc_callbacks, NULL); + + DPRINTF(E_DBG, L_SPOTIFY, "Found %d playlists\n", fptr_sp_playlistcontainer_num_playlists(pc)); + + for (i = 0; i < fptr_sp_playlistcontainer_num_playlists(pc); i++) + { + pl = fptr_sp_playlistcontainer_playlist(pc, i); + fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); + } +} + +/** + * This callback is used from libspotify whenever there is PCM data available. + * + * @sa sp_session_callbacks#music_delivery + */ +static int music_delivery(sp_session *sess, const sp_audioformat *format, + const void *frames, int num_frames) +{ + audio_fifo_data_t *afd; + size_t s; + + /* No support for resampling right now */ + if ((format->sample_rate != 44100) || (format->channels != 2)) + { + DPRINTF(E_LOG, L_SPOTIFY, "Got music with unsupported samplerate or channels, stopping playback\n"); + pthread_mutex_lock(&g_notify_mutex); + g_event = SPOTIFY_EVENT_STOP; + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + return num_frames; + } + + if (num_frames == 0) + return 0; // Audio discontinuity, do nothing + + pthread_mutex_lock(&g_audio_fifo->mutex); + + /* Buffer three seconds of audio */ + if (g_audio_fifo->qlen > (3 * format->sample_rate)) + { + // If the buffer has been full the last 300 times (~about a minute) we + // assume the player thread paused/died without telling us, so we signal pause + if (g_audio_fifo->fullcount < 300) + g_audio_fifo->fullcount++; + else + { + DPRINTF(E_WARN, L_SPOTIFY, "Buffer full more than 300 times, signaling pause\n"); + pthread_mutex_lock(&g_notify_mutex); + g_event = SPOTIFY_EVENT_PAUSE; + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + g_audio_fifo->fullcount = 0; + } + + pthread_mutex_unlock(&g_audio_fifo->mutex); + + return 0; + } + else + g_audio_fifo->fullcount = 0; + + s = num_frames * sizeof(int16_t) * format->channels; + + afd = malloc(sizeof(*afd) + s); + memcpy(afd->samples, frames, s); + + afd->nsamples = num_frames; + + TAILQ_INSERT_TAIL(&g_audio_fifo->q, afd, link); + g_audio_fifo->qlen += num_frames; + + pthread_cond_signal(&g_audio_fifo->cond); + pthread_mutex_unlock(&g_audio_fifo->mutex); + + return num_frames; +} + +/** + * This callback is called from an internal libspotify thread to ask us to + * reiterate the main loop. + * + * We notify the main thread using a condition variable and a protected variable. + * + * @sa sp_session_callbacks#notify_main_thread + */ +static void +notify_main_thread(sp_session *sess) +{ + DPRINTF(E_SPAM, L_SPOTIFY, "Notify main thread - init\n"); + pthread_mutex_lock(&g_notify_mutex); + g_event = SPOTIFY_EVENT_LIBCB; + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + DPRINTF(E_SPAM, L_SPOTIFY, "Notify main thread - done\n"); +} + +/** + * Called whenever metadata has been updated + * + * If you have metadata cached outside of libspotify, you should purge + * your caches and fetch new versions. + * + * @param[in] session Session + */ +static void metadata_updated(sp_session *session) +{ + DPRINTF(E_DBG, L_SPOTIFY, "Session metadata updated.\n"); +} + +/** + * Notification that some other connection has started playing on this account. + * Playback has been stopped. + * + * @sa sp_session_callbacks#play_token_lost + */ +static void play_token_lost(sp_session *sess) +{ + DPRINTF(E_DBG, L_SPOTIFY, "Play token lost - init\n"); + pthread_mutex_lock(&g_notify_mutex); + g_event = SPOTIFY_EVENT_STOP; + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + DPRINTF(E_DBG, L_SPOTIFY, "Play token lost - done\n"); +} + +/** + * This callback is used from libspotify when the current track has ended + * + * @sa sp_session_callbacks#end_of_track + */ +static void end_of_track(sp_session *sess) +{ + DPRINTF(E_DBG, L_SPOTIFY, "End of track - init\n"); + pthread_mutex_lock(&g_notify_mutex); + g_event = SPOTIFY_EVENT_STOP; + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + DPRINTF(E_DBG, L_SPOTIFY, "End of track - done\n"); +} + +/** + * The session callbacks + */ +static sp_session_callbacks session_callbacks = { + .logged_in = &logged_in, + .notify_main_thread = ¬ify_main_thread, + .music_delivery = &music_delivery, + .metadata_updated = &metadata_updated, + .play_token_lost = &play_token_lost, + .log_message = NULL, + .end_of_track = &end_of_track, +}; + +/** + * The session configuration. + */ +static sp_session_config spconfig = { + .api_version = SPOTIFY_API_VERSION, + .cache_location = NULL, + .settings_location = NULL, + .application_key = g_appkey, + .application_key_size = sizeof(g_appkey), + .user_agent = "forked-daapd", + .callbacks = &session_callbacks, + NULL, +}; +/* ------------------------- END SESSION CALLBACKS ----------------------- */ + + +/* Thread: spotify */ +static int +playback_play(void) +{ + sp_track *track; + sp_error err; + + DPRINTF(E_DBG, L_SPOTIFY, "Starting playback\n"); + + if (!g_ctx.link) + { + DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed, no Spotify link"); + return -1; + } + + track = fptr_sp_link_as_track(g_ctx.link); + if (!track) + { + DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed, invalid Spotify track"); + return -1; + } + + err = fptr_sp_session_player_load(g_sess, track); + if (SP_ERROR_OK != err) + { + DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed: %s\n", fptr_sp_error_message(err)); + return -1; + } + + spotify_audio_fifo_flush(); + + err = fptr_sp_session_player_play(g_sess, 1); + if (SP_ERROR_OK != err) + { + DPRINTF(E_LOG, L_SPOTIFY, "Playback failed: %s\n", fptr_sp_error_message(err)); + return -1; + } + + DPRINTF(E_DBG, L_SPOTIFY, "Playback started\n"); + + return 0; +} + +/* Thread: spotify */ +static int +playback_pause(void) +{ + sp_error err; + + DPRINTF(E_DBG, L_SPOTIFY, "Pausing playback\n"); + + err = fptr_sp_session_player_play(g_sess, 0); + if (SP_ERROR_OK != err) + { + DPRINTF(E_LOG, L_SPOTIFY, "Playback pause failed: %s\n", fptr_sp_error_message(err)); + return -1; + } + + DPRINTF(E_DBG, L_SPOTIFY, "Playback paused\n"); + + return 0; +} + +/* Thread: spotify */ +static int +playback_resume(void) +{ + sp_error err; + + DPRINTF(E_DBG, L_SPOTIFY, "Resuming playback\n"); + + err = fptr_sp_session_player_play(g_sess, 1); + if (SP_ERROR_OK != err) + { + DPRINTF(E_LOG, L_SPOTIFY, "Playback resume failed: %s\n", fptr_sp_error_message(err)); + return -1; + } + + DPRINTF(E_DBG, L_SPOTIFY, "Playback resumed\n"); + + return 0; +} + +/* Thread: spotify */ +static int +playback_stop(void) +{ + sp_error err; + + DPRINTF(E_DBG, L_SPOTIFY, "Stopping playback\n"); + + err = fptr_sp_session_player_unload(g_sess); + if (SP_ERROR_OK != err) + { + DPRINTF(E_LOG, L_SPOTIFY, "Playback stop failed: %s\n", fptr_sp_error_message(err)); + return -1; + } + + DPRINTF(E_DBG, L_SPOTIFY, "Playback stopped\n"); + + return 0; +} + +/* Thread: spotify */ +static int +playback_seek(void) +{ + sp_error err; + + DPRINTF(E_DBG, L_SPOTIFY, "Playback seek\n"); + + err = fptr_sp_session_player_seek(g_sess, g_ctx.seek_ms); + if (SP_ERROR_OK != err) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not seek: %s\n", fptr_sp_error_message(err)); + return -1; + } + + spotify_audio_fifo_flush(); + + return 0; +} + +/* Thread: spotify */ +static void * +spotify(void *arg) +{ + struct timespec ts; + enum spotify_event this_event; + enum spotify_state state; + int ret; + int next_timeout; + + DPRINTF(E_DBG, L_SPOTIFY, "Main loop begin\n"); + + ret = db_perthread_init(); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error: DB init failed\n"); + + pthread_exit(NULL); + } + + state = SPOTIFY_STATE_WAIT; + next_timeout = 0; + for (;;) + { + pthread_mutex_lock(&g_notify_mutex); + + if (next_timeout == 0) + { + while(g_event == SPOTIFY_EVENT_NONE) + pthread_cond_wait(&g_notify_cond, &g_notify_mutex); + } + else + { +#if _POSIX_TIMERS > 0 + clock_gettime(CLOCK_REALTIME, &ts); +#else + struct timeval tv; + gettimeofday(&tv, NULL); + TIMEVAL_TO_TIMESPEC(&tv, &ts); +#endif + ts.tv_sec += next_timeout / 1000; + ts.tv_nsec += (next_timeout % 1000) * 1000000; + + while (g_event == SPOTIFY_EVENT_NONE) + if (pthread_cond_timedwait(&g_notify_cond, &g_notify_mutex, &ts)) + break; + } + + this_event = g_event; + g_event = SPOTIFY_EVENT_NONE; + pthread_mutex_unlock(&g_notify_mutex); + + switch (this_event) + { + case SPOTIFY_EVENT_PLAY: + if ((ret = playback_play()) == 0) + state = SPOTIFY_STATE_PLAYING; + else + state = SPOTIFY_STATE_STOPPED; + break; + + case SPOTIFY_EVENT_PAUSE: + if ((ret = playback_pause()) == 0) + state = SPOTIFY_STATE_PAUSED; + else + state = SPOTIFY_STATE_PLAYING; + break; + + case SPOTIFY_EVENT_RESUME: + if ((ret = playback_resume()) == 0) + state = SPOTIFY_STATE_PLAYING; + else + state = SPOTIFY_STATE_PAUSED; + break; + + case SPOTIFY_EVENT_STOP: + if ((ret = playback_stop()) == 0) + state = SPOTIFY_STATE_STOPPED; + else + state = SPOTIFY_STATE_PLAYING; + break; + + case SPOTIFY_EVENT_SEEK: + if ((ret = playback_seek()) == 0) + state = SPOTIFY_STATE_SEEKED; + break; + + case SPOTIFY_EVENT_EXIT: + ret = playback_stop(); + fptr_sp_session_logout(g_sess); + state = SPOTIFY_STATE_EXITING; + break; + + default: + ret = 0; + state = 0; + } + + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Playback action failed (event code %d)\n", this_event); + } + + if (state == SPOTIFY_STATE_EXITING) + break; + + do + { + fptr_sp_session_process_events(g_sess, &next_timeout); + } + while (next_timeout == 0); + + pthread_mutex_lock(&g_state_mutex); + if (state) + { + g_state = state; + DPRINTF(E_LOG, L_SPOTIFY, "Event was %d, new state is %d\n", this_event, g_state); + pthread_cond_signal(&g_state_cond); + } + pthread_mutex_unlock(&g_state_mutex); + } + + db_perthread_deinit(); + + DPRINTF(E_DBG, L_SPOTIFY, "Main loop end\n"); + + pthread_exit(NULL); +} + + +/* ------------------------- PLAYER API ----------------------- */ + +/* Thread: player */ +int +spotify_playback_play(struct media_file_info *mfi) +{ + sp_link *link; + + DPRINTF(E_DBG, L_SPOTIFY, "Playback request\n"); + + link = fptr_sp_link_create_from_string(mfi->path); + if (!link) + { + DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed, invalid Spotify link: %s\n", mfi->path); + return -1; + } + + pthread_mutex_lock(&g_notify_mutex); + + g_state = SPOTIFY_STATE_WAIT; + g_event = SPOTIFY_EVENT_PLAY; + + if (g_ctx.link) + fptr_sp_link_release(g_ctx.link); + g_ctx.link = link; + + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + + // Wait until state changed so we know the event was processed + pthread_mutex_lock(&g_state_mutex); + while (g_state == SPOTIFY_STATE_WAIT) + pthread_cond_wait(&g_state_cond, &g_state_mutex); + pthread_mutex_unlock(&g_state_mutex); + + DPRINTF(E_DBG, L_SPOTIFY, "Playback reply\n"); + + if (g_state == SPOTIFY_STATE_PLAYING) + return 0; + else + return -1; +} + +/* Thread: player */ +//TODO This is not currently used by player.c - should it? +int +spotify_playback_pause(void) +{ + pthread_mutex_lock(&g_notify_mutex); + + g_state = SPOTIFY_STATE_WAIT; + g_event = SPOTIFY_EVENT_PAUSE; + + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + + // Wait until state changed so we know the event was processed + pthread_mutex_lock(&g_state_mutex); + while (g_state == SPOTIFY_STATE_WAIT) + pthread_cond_wait(&g_state_cond, &g_state_mutex); + pthread_mutex_unlock(&g_state_mutex); + + if (g_state == SPOTIFY_STATE_PAUSED) + return 0; + else + return -1; +} + +/* Thread: player */ +int +spotify_playback_stop(void) +{ + DPRINTF(E_DBG, L_SPOTIFY, "Stop request\n"); + + pthread_mutex_lock(&g_notify_mutex); + + g_state = SPOTIFY_STATE_WAIT; + g_event = SPOTIFY_EVENT_STOP; + + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + + // Wait until state changed so we know the event was processed + pthread_mutex_lock(&g_state_mutex); + while (g_state == SPOTIFY_STATE_WAIT) + pthread_cond_wait(&g_state_cond, &g_state_mutex); + pthread_mutex_unlock(&g_state_mutex); + + DPRINTF(E_DBG, L_SPOTIFY, "Stop reply\n"); + + if (g_state == SPOTIFY_STATE_STOPPED) + return 0; + else + return -1; +} + +/* Thread: player */ +int +spotify_playback_seek(int ms) +{ + pthread_mutex_lock(&g_notify_mutex); + + g_state = SPOTIFY_STATE_WAIT; + g_event = SPOTIFY_EVENT_SEEK; + g_ctx.seek_ms = ms; + + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + + // Wait until state changed so we know the event was processed + pthread_mutex_lock(&g_state_mutex); + while (g_state == SPOTIFY_STATE_WAIT) + pthread_cond_wait(&g_state_cond, &g_state_mutex); + pthread_mutex_unlock(&g_state_mutex); + + return ms; +} + +/* Thread: player */ +int +spotify_audio_get(struct evbuffer *evbuf, int wanted) +{ + audio_fifo_data_t *afd; + int processed; + int ret; + int s; + + afd = NULL; + processed = 0; + + // If spotify was paused begin by resuming playback + if (g_state == SPOTIFY_STATE_PAUSED) + { + pthread_mutex_lock(&g_notify_mutex); + g_event = SPOTIFY_EVENT_RESUME; + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + } + + pthread_mutex_lock(&g_audio_fifo->mutex); + + while ((processed < wanted) && (g_state != SPOTIFY_STATE_STOPPED)) + { + while ((g_state != SPOTIFY_STATE_STOPPED) && !(afd = TAILQ_FIRST(&g_audio_fifo->q))) + { + DPRINTF(E_DBG, L_SPOTIFY, "Audio get is blocking now\n"); + pthread_cond_wait(&g_audio_fifo->cond, &g_audio_fifo->mutex); // TODO protect against indefinite wait + DPRINTF(E_DBG, L_SPOTIFY, "Audio get is released now\n"); + } + + TAILQ_REMOVE(&g_audio_fifo->q, afd, link); + g_audio_fifo->qlen -= afd->nsamples; + + s = afd->nsamples * sizeof(int16_t) * 2; + + ret = evbuffer_add(evbuf, afd->samples, s); + free(afd); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for evbuffer (tried to add %d bytes)\n", s); + pthread_mutex_unlock(&g_audio_fifo->mutex); + return -1; + } + + processed += s; + } + + pthread_mutex_unlock(&g_audio_fifo->mutex); + + return processed; +} + +/* Thread: filescanner */ +void +spotify_login(char *path) +{ + char buf[256]; + FILE *fp; + char *username; + char *password; + int len; + int ret; + sp_error err; + + if (!g_sess) + { + DPRINTF(E_LOG, L_SPOTIFY, "Can't login! No valid Spotify session.\n"); + return; + } + + fp = fopen(path, "rb"); + if (!fp) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not open Spotify credentials file %s: %s\n", path, strerror(errno)); + return; + } + + username = fgets(buf, sizeof(buf), fp); + if (!username) + { + DPRINTF(E_LOG, L_SPOTIFY, "Empty Spotify credentials file %s\n", path); + + fclose(fp); + return; + } + + len = strlen(username); + if (buf[len - 1] != '\n') + { + DPRINTF(E_LOG, L_SPOTIFY, "Invalid Spotify credentials file %s: username name too long or missing password\n", path); + + fclose(fp); + return; + } + + while (len) + { + if ((buf[len - 1] == '\r') || (buf[len - 1] == '\n')) + { + buf[len - 1] = '\0'; + len--; + } + else + break; + } + + if (!len) + { + DPRINTF(E_LOG, L_SPOTIFY, "Invalid Spotify credentials file %s: empty line where username expected\n", path); + + fclose(fp); + return; + } + + username = strdup(buf); + if (!username) + { + DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for username while reading %s\n", path); + + fclose(fp); + return; + } + + password = fgets(buf, sizeof(buf), fp); + fclose(fp); + if (!password) + { + DPRINTF(E_LOG, L_SPOTIFY, "Invalid Spotify credentials file %s: no password\n", path); + + free(username); + return; + } + + len = strlen(password); + + while (len) + { + if ((buf[len - 1] == '\r') || (buf[len - 1] == '\n')) + { + buf[len - 1] = '\0'; + len--; + } + else + break; + } + + password = strdup(buf); + if (!password) + { + DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for password while reading %s\n", path); + + free(username); + return; + } + + if (g_state != SPOTIFY_STATE_INACTIVE) + { + DPRINTF(E_DBG, L_SPOTIFY, "Killing previous Spotify thread\n"); + pthread_mutex_lock(&g_notify_mutex); + g_event = SPOTIFY_EVENT_EXIT; + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + + pthread_join(tid_spotify, NULL); + g_state = SPOTIFY_STATE_INACTIVE; + } + + DPRINTF(E_DBG, L_SPOTIFY, "Spotify credentials file OK, logging in with %s/%s\n", username, password); + + err = fptr_sp_session_login(g_sess, username, password, 1, NULL); + if (SP_ERROR_OK != err) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not login into Spotify: %s\n", fptr_sp_error_message(err)); + return; + } + + ret = pthread_create(&tid_spotify, NULL, spotify, NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not spawn Spotify thread: %s\n", strerror(errno)); + + return; + } +} + +/* Thread: main */ +int +spotify_init(void) +{ + cfg_t *lib; + sp_session *sp; + sp_error err; + int ret; + + /* Initialize libspotify */ + g_libhandle = dlopen("libspotify.so", RTLD_LAZY); + if (!g_libhandle) + { + DPRINTF(E_INFO, L_SPOTIFY, "libspotify.so not installed or not found\n"); + return -1; + } + + DPRINTF(E_INFO, L_SPOTIFY, "Spotify session init\n"); + ret = fptr_assign_all(); + if (ret < 0) + return -1; + + /* Initialize session */ + g_event = SPOTIFY_EVENT_NONE; + g_state = SPOTIFY_STATE_INACTIVE; + + lib = cfg_getsec(cfg, "spotify"); + spconfig.settings_location = cfg_getstr(lib, "settings_dir"); + spconfig.cache_location = cfg_getstr(lib, "cache_dir"); + + DPRINTF(E_DBG, L_SPOTIFY, "Creating Spotify session\n"); + err = fptr_sp_session_create(&spconfig, &sp); + if (SP_ERROR_OK != err) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not create Spotify session: %s\n", fptr_sp_error_message(err)); + return -1; + } + + g_sess = sp; + + /* Prepare thread and audio buffer */ + pthread_mutex_init(&g_notify_mutex, NULL); + pthread_cond_init(&g_notify_cond, NULL); + pthread_mutex_init(&g_state_mutex, NULL); + pthread_cond_init(&g_state_cond, NULL); + pthread_mutex_init(&g_db_mutex, NULL); + + g_audio_fifo = (audio_fifo_t *)malloc(sizeof(audio_fifo_t)); + TAILQ_INIT(&g_audio_fifo->q); + g_audio_fifo->qlen = 0; + + pthread_mutex_init(&g_audio_fifo->mutex, NULL); + pthread_cond_init(&g_audio_fifo->cond, NULL); + + /* Log in and spawn thread */ + DPRINTF(E_DBG, L_SPOTIFY, "Logging into Spotify\n"); + err = fptr_sp_session_relogin(sp); + if (SP_ERROR_OK != err) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not login into Spotify: %s\n", fptr_sp_error_message(err)); + return -1; + } + + ret = pthread_create(&tid_spotify, NULL, spotify, NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Could not spawn Spotify thread: %s\n", strerror(errno)); + + return -1; + } + + DPRINTF(E_DBG, L_SPOTIFY, "Spotify init complete\n"); + return 0; +} + +void +spotify_deinit(void) +{ + int ret; + + /* libspotify not installed or no session - just exit */ + if (!g_libhandle || !g_sess) + return; + + /* Send exit signal to thread (if active) */ + if (g_state != SPOTIFY_STATE_INACTIVE) + { + pthread_mutex_lock(&g_notify_mutex); + g_event = SPOTIFY_EVENT_EXIT; + pthread_cond_signal(&g_notify_cond); + pthread_mutex_unlock(&g_notify_mutex); + + ret = pthread_join(tid_spotify, NULL); + if (ret != 0) + { + DPRINTF(E_FATAL, L_SPOTIFY, "Could not join Spotify thread: %s\n", strerror(errno)); + return; + } + } + + /* Release session and destroy pthread mutex/cond */ + fptr_sp_session_release(g_sess); + + DPRINTF(E_SPAM, L_SPOTIFY, "Destroy pthread mutex and cond\n"); + pthread_cond_destroy(&g_notify_cond); + pthread_mutex_destroy(&g_notify_mutex); + + pthread_cond_destroy(&g_state_cond); + pthread_mutex_destroy(&g_state_mutex); + + pthread_mutex_destroy(&g_db_mutex); + + pthread_cond_destroy(&g_audio_fifo->cond); + pthread_mutex_destroy(&g_audio_fifo->mutex); + + /* Free audio buffer */ + DPRINTF(E_SPAM, L_SPOTIFY, "Free audio fifo\n"); + free(g_audio_fifo); + + /* Release libspotify handle */ + if (g_libhandle) + dlclose(g_libhandle); +} diff --git a/src/spotify.h b/src/spotify.h new file mode 100644 index 00000000..a6d68cab --- /dev/null +++ b/src/spotify.h @@ -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__ */ diff --git a/src/transcode.c b/src/transcode.c index e4f06f2b..4f4d545b 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -54,6 +54,9 @@ #include "db.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) # define XCODE_BUFFER_SIZE ((192000 * 3) / 2) @@ -462,9 +465,8 @@ transcode_seek(struct transcode_ctx *ctx, int ms) return got_ms; } - -struct transcode_ctx * -transcode_setup(struct media_file_info *mfi, off_t *est_size, int wavhdr) +int +transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t *est_size, int wavhdr) { struct transcode_ctx *ctx; 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"); - return NULL; + return -1; } 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))); free(ctx); - return NULL; + return -1; } #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) make_wav_header(ctx, est_size); - return ctx; + *nctx = ctx; + + return 0; setup_fail_codec: avcodec_close(ctx->acodec); @@ -675,7 +679,7 @@ transcode_setup(struct media_file_info *mfi, off_t *est_size, int wavhdr) #endif free(ctx); - return NULL; + return -1; } void @@ -684,11 +688,14 @@ transcode_cleanup(struct transcode_ctx *ctx) if (ctx->apacket.data) 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) - avformat_close_input(&ctx->fmtctx); + if (ctx->fmtctx) + avformat_close_input(&ctx->fmtctx); #else - av_close_input_file(ctx->fmtctx); + if (ctx->fmtctx) + av_close_input_file(ctx->fmtctx); #endif av_free(ctx->abuffer); @@ -717,6 +724,12 @@ transcode_needed(struct evkeyvalq *headers, char *file_codectype) int size; 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); lib = cfg_getsec(cfg, "library"); diff --git a/src/transcode.h b/src/transcode.h index 4c79cedd..df0066ba 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -12,8 +12,8 @@ transcode(struct transcode_ctx *ctx, struct evbuffer *evbuf, int wanted); int transcode_seek(struct transcode_ctx *ctx, int ms); -struct transcode_ctx * -transcode_setup(struct media_file_info *mfi, off_t *est_size, int wavhdr); +int +transcode_setup(struct transcode_ctx **nctx, struct media_file_info *mfi, off_t *est_size, int wavhdr); void transcode_cleanup(struct transcode_ctx *ctx);