diff --git a/INSTALL b/INSTALL index 0998da50..c6181156 100644 --- a/INSTALL +++ b/INSTALL @@ -202,6 +202,9 @@ 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. +Support for the MPD protocol is optional. Use --enable-mpd to enable this +feature. + Recommended build settings: ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var diff --git a/README.md b/README.md index a72dc25a..a3a987ba 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ forked-daapd is a complete rewrite of mt-daapd (Firefly Media Server). - [Command line and web interface](#command-line-and-web-interface) - [Spotify](#spotify) - [LastFM](#lastfm) +- [MPD clients](#mpd-clients) ## Getting started @@ -374,9 +375,9 @@ not necessary during normal operation. forked-daapd is meant to be used with the clients mentioned above, so it does not have a command line interface nor does it have a web interface. You can, -however, to some extent control forked-daapd from the command line by issuing -DAAP/DACP commands with a program like curl. Here is an example of how to do -that. +however, to some extent control forked-daapd with [MPD clients](#mpd-clients) or +from the command line by issuing DAAP/DACP commands with a program like curl. Here +is an example of how to do that. Say you have a playlist with a radio station, and you want to make a script that starts playback of that station: @@ -396,6 +397,7 @@ curl "http://localhost:3689/logout?session-id=50" ``` + ## Spotify forked-daapd has *some* support for Spotify. It must be compiled with the @@ -449,3 +451,38 @@ session key. The session key does not expire. To stop scrobbling from forked-daapd, add an empty ".lastfm" file to your library. + +## MPD clients +If forked-daapd was build with support for the [Music Player Deamon](http://musicpd.org/) +protocol (see the [INSTALL](INSTALL) file) you can - to some extent - use clients for MPD +to control forked-daapd. +By default forked-daapd listens on port 6600 for MPD clients. You can change this by +adding a section "mpd" to the forked-daapd.conf file: + +``` +# MPD configuration (only have effect if MPD enabled - see README/INSTALL) +mpd { + port = 8800 +} +``` + +Currently only a subset of the commands offered by MPD (see [MPD protocol documentation](http://www.musicpd.org/doc/protocol/)) +are supported by forked-daapd. + +Due to some differences between forked-daapd and MPD not all commands will act the same way they would running MPD: + +- consume, crossfade, mixrampdb, mixrampdelay and replaygain will have no effect +- single, repeat: unlike MPD forked-daapd does not support setting single and repeat separately + on/off, instead repeat off, repeat all and repeat single are supported. Thus setting single on + will result in repeat single, repeat on results in repeat all. + +Following table shows what is working for a selection of MPD clients: + +| Client | Type | Status | +| --------------------------------------------- | ------ | --------------- | +| [mpc](http://www.musicpd.org/clients/mpc/) | CLI | Working commands: mpc, add, crop, current, del (ranges are not yet supported), play, next, prev (behaves like cdprev), pause, toggle, cdprev, seek, clear, playlist, ls, load, volume, repeat, random, single, update (initiates an init-rescan, the path argument is not supported) | +| [ympd](http://www.ympd.org/) | Web | Everything except "search" should work | + + + + diff --git a/configure.ac b/configure.ac index e378edb2..fedb4509 100644 --- a/configure.ac +++ b/configure.ac @@ -88,6 +88,10 @@ AC_ARG_WITH(alsa, AS_HELP_STRING([--with-alsa], [use ALSA (default Linux=yes, Fr use_oss4=false; ) +AC_ARG_ENABLE(mpd, AS_HELP_STRING([--enable-mpd], [enable MPD client protocol support (default=no)]), + use_mpd=true; + CPPFLAGS="${CPPFLAGS} -DMPD") + 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) @@ -95,6 +99,7 @@ AM_CONDITIONAL(COND_SPOTIFY, test x$use_spotify = xtrue) AM_CONDITIONAL(COND_LASTFM, test x$use_lastfm = xtrue) AM_CONDITIONAL(COND_OSS4, test x$use_oss4 = xtrue) AM_CONDITIONAL(COND_ALSA, test x$use_oss4 != xtrue) +AM_CONDITIONAL(COND_MPD, test x$use_mpd = xtrue) dnl Checks for libraries. gl_LIBUNISTRING diff --git a/sqlext/sqlext.c b/sqlext/sqlext.c index da993b24..4c3759e9 100644 --- a/sqlext/sqlext.c +++ b/sqlext/sqlext.c @@ -244,6 +244,112 @@ sqlext_daap_unicode_xcollation(void *notused, int llen, const void *left, int rl return rpp; } +static void +sqlext_daap_substring_xfunc(sqlite3_context *pv, int n, sqlite3_value **ppv) +{ + const unsigned char *s1; + const unsigned char *s2; + int index; + + char *start; + char *end; + char *result; + + if (n < 2) + { + sqlite3_result_error(pv, "daap_substring() requires at least 2 parameters", -1); + return; + } + + if (SQLITE_TEXT != sqlite3_value_type(ppv[0]) || SQLITE_TEXT != sqlite3_value_type(ppv[1])) + { + sqlite3_result_null(pv); + return; + } + + s1 = sqlite3_value_text(ppv[0]); + s2 = sqlite3_value_text(ppv[1]); + + if (n > 2) + index = sqlite3_value_int(ppv[2]); + else + index = 0; + + if (strlen((char *) s1) < index) + { + sqlite3_result_null(pv); + return; + } + + start = (char *) s1 + index; + end = strstr(start, (char *) s2); + + if (!end) + { + sqlite3_result_null(pv); + return; + } + + result = sqlite3_malloc(end - (char *) s1 + 1); + if (!result) + { + sqlite3_result_error_nomem(pv); + return; + } + + strncpy((char*) result, (char*) s1, end - (char *) s1); + *(result + (end - (char *) s1)) = '\0'; + sqlite3_result_text(pv, (char*) result, -1, SQLITE_TRANSIENT); + sqlite3_free(result); +} + +static void +sqlext_daap_charindex_xfunc(sqlite3_context *pv, int n, sqlite3_value **ppv) +{ + const unsigned char *s1; + const unsigned char *s2; + int index; + + char *start; + char *end; + + if (n < 2) + { + sqlite3_result_error(pv, "daap_charindex() requires at least 2 parameters", -1); + return; + } + + if (SQLITE_TEXT != sqlite3_value_type(ppv[0]) || SQLITE_TEXT != sqlite3_value_type(ppv[1])) + { + sqlite3_result_int(pv, -1); + return; + } + + s1 = sqlite3_value_text(ppv[0]); + s2 = sqlite3_value_text(ppv[1]); + + if (n > 2) + index = sqlite3_value_int(ppv[2]); + else + index = 0; + + if (strlen((char *) s1) < index) + { + sqlite3_result_int(pv, -1); + return; + } + + start = (char *) s1 + index; + end = strstr(start, (char *) s2); + + if (!end) + { + sqlite3_result_int(pv, -1); + return; + } + + sqlite3_result_int(pv, end - (char *) s1); +} int sqlite3_extension_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) @@ -269,5 +375,23 @@ sqlite3_extension_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines return -1; } + ret = sqlite3_create_function(db, "daap_substring", 3, SQLITE_UTF8, NULL, sqlext_daap_substring_xfunc, NULL, NULL); + if (ret != SQLITE_OK) + { + if (pzErrMsg) + *pzErrMsg = sqlite3_mprintf("Could not create daap_substring function: %s\n", sqlite3_errmsg(db)); + + return -1; + } + + ret = sqlite3_create_function(db, "daap_charindex", 3, SQLITE_UTF8, NULL, sqlext_daap_charindex_xfunc, NULL, NULL); + if (ret != SQLITE_OK) + { + if (pzErrMsg) + *pzErrMsg = sqlite3_mprintf("Could not create daap_charindex function: %s\n", sqlite3_errmsg(db)); + + return -1; + } + return 0; } diff --git a/src/Makefile.am b/src/Makefile.am index 24681684..66f35a2f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -21,6 +21,10 @@ if COND_LASTFM LASTFM_SRC=lastfm.c lastfm.h endif +if COND_MPD +MPD_SRC=mpd.c mpd.h +endif + if COND_ALSA ALSA_SRC=laudio_alsa.c endif @@ -123,6 +127,7 @@ forked_daapd_SOURCES = main.c \ $(RTSP_SRC) \ scan-wma.c \ $(SPOTIFY_SRC) $(LASTFM_SRC) \ + $(MPD_SRC) \ $(FLAC_SRC) $(MUSEPACK_SRC) nodist_forked_daapd_SOURCES = \ diff --git a/src/conffile.c b/src/conffile.c index b9155deb..03716167 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -127,6 +127,13 @@ static cfg_opt_t sec_sqlite[] = CFG_END() }; +/* MPD section structure */ +static cfg_opt_t sec_mpd[] = + { + CFG_INT("port", 6600, CFGF_NONE), + CFG_END() + }; + /* Config file structure */ static cfg_opt_t toplvl_cfg[] = { @@ -136,6 +143,7 @@ static cfg_opt_t toplvl_cfg[] = CFG_SEC("airplay", sec_airplay, CFGF_MULTI | CFGF_TITLE), CFG_SEC("spotify", sec_spotify, CFGF_NONE), CFG_SEC("sqlite", sec_sqlite, CFGF_NONE), + CFG_SEC("mpd", sec_mpd, CFGF_NONE), CFG_END() }; diff --git a/src/db.c b/src/db.c index b94d77c2..d8d2a0af 100644 --- a/src/db.c +++ b/src/db.c @@ -33,6 +33,7 @@ #include #include #include +#include #include @@ -134,6 +135,7 @@ static const struct col_type_map mfi_cols_map[] = { mfi_offsetof(album_sort), DB_TYPE_STRING }, { mfi_offsetof(composer_sort), DB_TYPE_STRING }, { mfi_offsetof(album_artist_sort), DB_TYPE_STRING }, + { mfi_offsetof(virtual_path), DB_TYPE_STRING }, }; /* This list must be kept in sync with @@ -151,6 +153,7 @@ static const struct col_type_map pli_cols_map[] = { pli_offsetof(path), DB_TYPE_STRING }, { pli_offsetof(index), DB_TYPE_INT }, { pli_offsetof(special_id), DB_TYPE_INT }, + { pli_offsetof(virtual_path), DB_TYPE_STRING }, /* items is computed on the fly */ }; @@ -218,6 +221,7 @@ static const ssize_t dbmfi_cols_map[] = dbmfi_offsetof(album_sort), dbmfi_offsetof(composer_sort), dbmfi_offsetof(album_artist_sort), + dbmfi_offsetof(virtual_path), }; /* This list must be kept in sync with @@ -235,6 +239,7 @@ static const ssize_t dbpli_cols_map[] = dbpli_offsetof(path), dbpli_offsetof(index), dbpli_offsetof(special_id), + dbmfi_offsetof(virtual_path), /* items is computed on the fly */ }; @@ -316,6 +321,18 @@ db_escape_string(const char *str) return ret; } +void +free_fi(struct filelist_info *fi, int content_only) +{ + if (fi->virtual_path) + free(fi->virtual_path); + + if (!content_only) + free(fi); + else + memset(fi, 0, sizeof(struct filelist_info)); +} + void free_pi(struct pairing_info *pi, int content_only) { @@ -406,6 +423,9 @@ free_mfi(struct media_file_info *mfi, int content_only) if (mfi->album_artist_sort) free(mfi->album_artist_sort); + if (mfi->virtual_path) + free(mfi->virtual_path); + if (!content_only) free(mfi); else @@ -458,6 +478,9 @@ free_pli(struct playlist_info *pli, int content_only) if (pli->path) free(pli->path); + if (pli->virtual_path) + free(pli->virtual_path); + if (!content_only) free(pli); else @@ -792,7 +815,7 @@ db_purge_cruft(time_t ref) void db_purge_all(void) { - char *queries[5] = + char *queries[6] = { "DELETE FROM inotify;", "DELETE FROM playlistitems;", @@ -1852,6 +1875,111 @@ db_query_fetch_string_sort(struct query_params *qp, char **string, char **sortst return 0; } +/* Filelist */ + +int +db_mpd_start_query_filelist(struct query_params *qp, char *parentpath) +{ + char *query; + int ret; + + /* + query = sqlite3_mprintf( + "SELECT " + " CASE WHEN INSTR(SUBSTR(virtual_path, LENGTH(%Q)+1), '/') = 0 " + " THEN " + " virtual_path " + " ELSE " + " SUBSTR(virtual_path, 1, LENGTH(%Q)+INSTR(SUBSTR(virtual_path, LENGTH(%Q)+1), '/')-1) " + " END AS path, " + " MAX(time_modified), " + " CASE WHEN INSTR(SUBSTR(virtual_path, LENGTH(%Q)+1), '/') = 0 " + " THEN " + " type " + " ELSE " + " 2 " + " END AS ftype, " + " disabled " + "FROM filelist " + "WHERE virtual_path LIKE '%q%%' " + "GROUP BY ftype, path " + "ORDER BY ftype, path;", parentpath, parentpath, parentpath, parentpath, parentpath); + */ + query = sqlite3_mprintf( + "SELECT " + " CASE WHEN daap_charindex(virtual_path, '/', LENGTH(%Q)) = -1 " + " THEN " + " virtual_path " + " ELSE " + " daap_substring(virtual_path, '/', LENGTH(%Q)) " + " END AS path, " + " MAX(time_modified), " + " CASE WHEN daap_charindex(virtual_path, '/', LENGTH(%Q)) = -1 " + " THEN " + " type " + " ELSE " + " 2 " + " END AS ftype " + "FROM filelist " + "WHERE virtual_path LIKE '%q%%' " + "GROUP BY ftype, path " + "ORDER BY ftype, path;", parentpath, parentpath, parentpath, parentpath); + + if (!query) + { + DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); + return -1; + } + + DPRINTF(E_DBG, L_DB, "Starting query '%s'\n", query); + + ret = db_blocking_prepare_v2(query, -1, &qp->stmt, NULL); + if (ret != SQLITE_OK) + { + DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); + + sqlite3_free(query); + return -1; + } + + sqlite3_free(query); + + return 0; +} + +int +db_mpd_query_fetch_filelist(struct query_params *qp, struct filelist_info *fi) +{ + int ret; + + memset(fi, 0, sizeof(struct filelist_info)); + + if (!qp->stmt) + { + DPRINTF(E_LOG, L_DB, "Query not started!\n"); + return -1; + } + + ret = db_blocking_step(qp->stmt); + if (ret == SQLITE_DONE) + { + DPRINTF(E_DBG, L_DB, "End of query results\n"); + fi->virtual_path = NULL; + return 0; + } + else if (ret != SQLITE_ROW) + { + DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); + return -1; + } + + fi->virtual_path = strdup((char *)sqlite3_column_text(qp->stmt, 0)); + fi->time_modified = sqlite3_column_int(qp->stmt, 1); + fi->type = sqlite3_column_int(qp->stmt, 2); + + return 0; +} + /* Files */ int @@ -2359,6 +2487,30 @@ db_file_fetch_byid(int id) #undef Q_TMPL } +struct media_file_info * +db_file_fetch_byvirtualpath(char *virtual_path) +{ +#define Q_TMPL "SELECT f.* FROM files f WHERE f.virtual_path = %Q;" + struct media_file_info *mfi; + char *query; + + query = sqlite3_mprintf(Q_TMPL, virtual_path); + if (!query) + { + DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); + + return NULL; + } + + mfi = db_file_fetch_byquery(query); + + sqlite3_free(query); + + return mfi; + +#undef Q_TMPL +} + int db_file_add(struct media_file_info *mfi) { @@ -2369,7 +2521,7 @@ db_file_add(struct media_file_info *mfi) " codectype, idx, has_video, contentrating, bits_per_sample, album_artist," \ " media_kind, tv_series_name, tv_episode_num_str, tv_network_name, tv_episode_sort, tv_season_num, " \ " songartistid, songalbumid, " \ - " title_sort, artist_sort, album_sort, composer_sort, album_artist_sort" \ + " title_sort, artist_sort, album_sort, composer_sort, album_artist_sort, virtual_path" \ " ) " \ " VALUES (NULL, '%q', '%q', TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), %Q, TRIM(%Q)," \ " TRIM(%Q), TRIM(%Q), TRIM(%Q), %Q, %d, %d, %d, %" PRIi64 ", %d, %d," \ @@ -2378,7 +2530,7 @@ db_file_add(struct media_file_info *mfi) " %Q, %d, %d, %d, %d, TRIM(%Q)," \ " %d, TRIM(%Q), TRIM(%Q), TRIM(%Q), %d, %d," \ " daap_songalbumid(LOWER(TRIM(%Q)), ''), daap_songalbumid(LOWER(TRIM(%Q)), LOWER(TRIM(%Q))), " \ - " TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q));" + " TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q), TRIM(%Q));" char *query; char *errmsg; @@ -2408,10 +2560,10 @@ db_file_add(struct media_file_info *mfi) (int64_t)mfi->time_played, (int64_t)mfi->db_timestamp, mfi->disabled, mfi->sample_count, mfi->codectype, mfi->index, mfi->has_video, mfi->contentrating, mfi->bits_per_sample, mfi->album_artist, - mfi->media_kind, mfi->tv_series_name, mfi->tv_episode_num_str, - mfi->tv_network_name, mfi->tv_episode_sort, mfi->tv_season_num, + mfi->media_kind, mfi->tv_series_name, mfi->tv_episode_num_str, + mfi->tv_network_name, mfi->tv_episode_sort, mfi->tv_season_num, mfi->album_artist, mfi->album_artist, mfi->album, mfi->title_sort, mfi->artist_sort, mfi->album_sort, - mfi->composer_sort, mfi->album_artist_sort); + mfi->composer_sort, mfi->album_artist_sort, mfi->virtual_path); if (!query) { @@ -2444,19 +2596,22 @@ int db_file_update(struct media_file_info *mfi) { #define Q_TMPL "UPDATE files SET path = '%q', fname = '%q', title = TRIM(%Q), artist = TRIM(%Q), album = TRIM(%Q), genre = TRIM(%Q)," \ - " comment = TRIM(%Q), type = %Q, composer = TRIM(%Q), orchestra = TRIM(%Q), conductor = TRIM(%Q), grouping = TRIM(%Q)," \ - " url = %Q, bitrate = %d, samplerate = %d, song_length = %d, file_size = %" PRIi64 "," \ - " year = %d, track = %d, total_tracks = %d, disc = %d, total_discs = %d, bpm = %d," \ - " compilation = %d, artwork = %d, rating = %d, seek = %d, data_kind = %d, item_kind = %d," \ - " description = %Q, time_modified = %" PRIi64 "," \ - " db_timestamp = %" PRIi64 ", disabled = %" PRIi64 ", sample_count = %" PRIi64 "," \ - " codectype = %Q, idx = %d, has_video = %d," \ - " bits_per_sample = %d, album_artist = TRIM(%Q)," \ - " media_kind = %d, tv_series_name = TRIM(%Q), tv_episode_num_str = TRIM(%Q)," \ - " tv_network_name = TRIM(%Q), tv_episode_sort = %d, tv_season_num = %d," \ - " songartistid = daap_songalbumid(LOWER(TRIM(%Q)), ''), songalbumid = daap_songalbumid(LOWER(TRIM(%Q)), LOWER(TRIM(%Q)))," \ - " title_sort = TRIM(%Q), artist_sort = TRIM(%Q), album_sort = TRIM(%Q), composer_sort = TRIM(%Q), album_artist_sort = TRIM(%Q)" \ - " WHERE id = %d;" + " comment = TRIM(%Q), type = %Q, composer = TRIM(%Q), orchestra = TRIM(%Q), conductor = TRIM(%Q), grouping = TRIM(%Q)," \ + " url = %Q, bitrate = %d, samplerate = %d, song_length = %d, file_size = %" PRIi64 "," \ + " year = %d, track = %d, total_tracks = %d, disc = %d, total_discs = %d, bpm = %d," \ + " compilation = %d, artwork = %d, rating = %d, seek = %d, data_kind = %d, item_kind = %d," \ + " description = %Q, time_modified = %" PRIi64 "," \ + " db_timestamp = %" PRIi64 ", disabled = %" PRIi64 ", sample_count = %" PRIi64 "," \ + " codectype = %Q, idx = %d, has_video = %d," \ + " bits_per_sample = %d, album_artist = TRIM(%Q)," \ + " media_kind = %d, tv_series_name = TRIM(%Q), tv_episode_num_str = TRIM(%Q)," \ + " tv_network_name = TRIM(%Q), tv_episode_sort = %d, tv_season_num = %d," \ + " songartistid = daap_songalbumid(LOWER(TRIM(%Q)), ''), songalbumid = daap_songalbumid(LOWER(TRIM(%Q)), LOWER(TRIM(%Q)))," \ + " title_sort = TRIM(%Q), artist_sort = TRIM(%Q), album_sort = TRIM(%Q), composer_sort = TRIM(%Q), album_artist_sort = TRIM(%Q)," \ + " virtual_path = TRIM(%Q)" \ + " WHERE id = %d;" + +// struct media_file_info *oldmfi; char *query; char *errmsg; int ret; @@ -2467,6 +2622,18 @@ db_file_update(struct media_file_info *mfi) return -1; } + /* + oldmfi = db_file_fetch_byid(mfi->id); + + if (!oldmfi) + { + DPRINTF(E_WARN, L_DB, "File with id '%d' does not exist\n", mfi->id); + return -1; + } + + free_mfi(oldmfi, 0); + */ + mfi->db_timestamp = (uint64_t)time(NULL); if (mfi->time_modified == 0) @@ -2486,7 +2653,7 @@ db_file_update(struct media_file_info *mfi) mfi->tv_network_name, mfi->tv_episode_sort, mfi->tv_season_num, mfi->album_artist, mfi->album_artist, mfi->album, mfi->title_sort, mfi->artist_sort, mfi->album_sort, - mfi->composer_sort, mfi->album_artist_sort, + mfi->composer_sort, mfi->album_artist_sort, mfi->virtual_path, mfi->id); if (!query) @@ -2865,6 +3032,30 @@ db_pl_fetch_bypath(char *path) #undef Q_TMPL } +struct playlist_info * +db_pl_fetch_byvirtualpath(char *virtual_path) +{ +#define Q_TMPL "SELECT p.* FROM playlists p WHERE p.virtual_path = '%q';" + struct playlist_info *pli; + char *query; + + query = sqlite3_mprintf(Q_TMPL, virtual_path); + if (!query) + { + DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); + + return NULL; + } + + pli = db_pl_fetch_byquery(query); + + sqlite3_free(query); + + return pli; + +#undef Q_TMPL +} + struct playlist_info * db_pl_fetch_byid(int id) { @@ -2914,11 +3105,11 @@ db_pl_fetch_bytitlepath(char *title, char *path) } int -db_pl_add(char *title, char *path, int *id) +db_pl_add(char *title, char *path, char *virtual_path, int *id) { #define QDUP_TMPL "SELECT COUNT(*) FROM playlists p WHERE p.title = '%q' AND p.path = '%q';" -#define QADD_TMPL "INSERT INTO playlists (title, type, query, db_timestamp, disabled, path, idx, special_id)" \ - " VALUES ('%q', 0, NULL, %" PRIi64 ", 0, '%q', 0, 0);" +#define QADD_TMPL "INSERT INTO playlists (title, type, query, db_timestamp, disabled, path, idx, special_id, virtual_path)" \ + " VALUES ('%q', 0, NULL, %" PRIi64 ", 0, '%q', 0, 0, '%q');" char *query; char *errmsg; int ret; @@ -2942,7 +3133,7 @@ db_pl_add(char *title, char *path, int *id) } /* Add */ - query = sqlite3_mprintf(QADD_TMPL, title, (int64_t)time(NULL), path); + query = sqlite3_mprintf(QADD_TMPL, title, (int64_t)time(NULL), path, virtual_path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); @@ -3003,14 +3194,17 @@ db_pl_add_item_byid(int plid, int fileid) } int -db_pl_update(char *title, char *path, int id) +db_pl_update(char *title, char *path, char *virtual_path, int id) { -#define Q_TMPL "UPDATE playlists SET title = '%q', db_timestamp = %" PRIi64 ", disabled = 0, path = '%q' WHERE id = %d;" +#define Q_TMPL "UPDATE playlists SET title = '%q', db_timestamp = %" PRIi64 ", disabled = 0, path = '%q', virtual_path = '%q' WHERE id = %d;" char *query; + int ret; - query = sqlite3_mprintf(Q_TMPL, title, (int64_t)time(NULL), path, id); + query = sqlite3_mprintf(Q_TMPL, title, (int64_t)time(NULL), path, virtual_path, id); - return db_query_run(query, 1, 0); + ret = db_query_run(query, 1, 0); + + return ret; #undef Q_TMPL } @@ -3108,6 +3302,7 @@ db_pl_enable_bycookie(uint32_t cookie, char *path) } + /* Groups */ int db_groups_clear(void) @@ -4275,7 +4470,8 @@ db_perthread_deinit(void) " artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " album_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ " composer_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ - " album_artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP" \ + " album_artist_sort VARCHAR(1024) DEFAULT NULL COLLATE DAAP," \ + " virtual_path VARCHAR(4096) DEFAULT NULL" \ ");" #define T_PL \ @@ -4288,7 +4484,8 @@ db_perthread_deinit(void) " disabled INTEGER DEFAULT 0," \ " path VARCHAR(4096)," \ " idx INTEGER NOT NULL," \ - " special_id INTEGER DEFAULT 0" \ + " special_id INTEGER DEFAULT 0," \ + " virtual_path VARCHAR(4096)" \ ");" #define T_PLITEMS \ @@ -4328,6 +4525,17 @@ db_perthread_deinit(void) " path VARCHAR(4096) NOT NULL" \ ");" +#define V_FILELIST \ + "CREATE VIEW IF NOT EXISTS filelist as" \ + " SELECT " \ + " virtual_path, time_modified, 3 as type " \ + " FROM files WHERE disabled = 0" \ + " UNION " \ + " SELECT " \ + " virtual_path, db_timestamp, 1 as type " \ + " FROM playlists where disabled = 0 AND type = 0" \ + ";" + #define TRG_GROUPS_INSERT_FILES \ "CREATE TRIGGER update_groups_new_file AFTER INSERT ON files FOR EACH ROW" \ " BEGIN" \ @@ -4372,15 +4580,15 @@ db_perthread_deinit(void) " VALUES(8, 'Purchased', 0, 'media_kind = 1024', 0, '', 0, 8);" */ -#define SCHEMA_VERSION_MAJOR 15 -#define SCHEMA_VERSION_MINOR 01 +#define SCHEMA_VERSION_MAJOR 16 +#define SCHEMA_VERSION_MINOR 00 // Q_SCVER should be deprecated/removed at v16 #define Q_SCVER \ - "INSERT INTO admin (key, value) VALUES ('schema_version', '15');" + "INSERT INTO admin (key, value) VALUES ('schema_version', '16');" #define Q_SCVER_MAJOR \ - "INSERT INTO admin (key, value) VALUES ('schema_version_major', '15');" + "INSERT INTO admin (key, value) VALUES ('schema_version_major', '16');" #define Q_SCVER_MINOR \ - "INSERT INTO admin (key, value) VALUES ('schema_version_minor', '01');" + "INSERT INTO admin (key, value) VALUES ('schema_version_minor', '00');" struct db_init_query { char *query; @@ -4398,6 +4606,8 @@ static const struct db_init_query db_init_table_queries[] = { T_SPEAKERS, "create table speakers" }, { T_INOTIFY, "create table inotify" }, + { V_FILELIST, "create view filelist" }, + { TRG_GROUPS_INSERT_FILES, "create trigger update_groups_new_file" }, { TRG_GROUPS_UPDATE_FILES, "create trigger update_groups_update_file" }, @@ -4454,11 +4664,14 @@ static const struct db_init_query db_init_table_queries[] = #define I_ALBUM \ "CREATE INDEX IF NOT EXISTS idx_album ON files(album, album_sort);" +#define I_FILELIST \ + "CREATE INDEX IF NOT EXISTS idx_filelist ON files(disabled, virtual_path, time_modified);" + #define I_PL_PATH \ "CREATE INDEX IF NOT EXISTS idx_pl_path ON playlists(path);" #define I_PL_DISABLED \ - "CREATE INDEX IF NOT EXISTS idx_pl_disabled ON playlists(disabled);" + "CREATE INDEX IF NOT EXISTS idx_pl_disabled ON playlists(disabled, type, virtual_path, db_timestamp);" #define I_FILEPATH \ "CREATE INDEX IF NOT EXISTS idx_filepath ON playlistitems(filepath ASC);" @@ -4486,6 +4699,7 @@ static const struct db_init_query db_init_index_queries[] = { I_GENRE, "create genre index" }, { I_TITLE, "create title index" }, { I_ALBUM, "create album index" }, + { I_FILELIST, "create filelist index" }, { I_PL_PATH, "create playlist path index" }, { I_PL_DISABLED, "create playlist state index" }, @@ -5428,6 +5642,153 @@ static const struct db_init_query db_upgrade_v1501_queries[] = { U_V1501_SCVER_MINOR, "set schema_version_minor to 01" }, }; +/* Upgrade from schema v15.01 to v16 */ + +#define U_V16_CREATE_VIEW_FILELIST \ + "CREATE VIEW IF NOT EXISTS filelist as" \ + " SELECT " \ + " virtual_path, time_modified, 3 as type " \ + " FROM files WHERE disabled = 0" \ + " UNION " \ + " SELECT " \ + " virtual_path, db_timestamp, 1 as type " \ + " FROM playlists WHERE disabled = 0 AND type = 0" \ + ";" + +#define U_V16_ALTER_TBL_FILES_ADD_COL \ + "ALTER TABLE files ADD COLUMN virtual_path VARCHAR(4096) DEFAULT NULL;" + +#define U_V16_ALTER_TBL_PL_ADD_COL \ + "ALTER TABLE playlists ADD COLUMN virtual_path VARCHAR(4096) DEFAULT NULL;" + +#define U_V16_SCVER \ + "UPDATE admin SET value = '16' WHERE key = 'schema_version';" +#define U_V1600_SCVER_MAJOR \ + "UPDATE admin SET value = '16' WHERE key = 'schema_version_major';" +#define U_V1600_SCVER_MINOR \ + "UPDATE admin SET value = '00' WHERE key = 'schema_version_minor';" + +static const struct db_init_query db_upgrade_v16_queries[] = + { + { U_V16_ALTER_TBL_FILES_ADD_COL, "alter table files add column virtual_path" }, + { U_V16_ALTER_TBL_PL_ADD_COL, "alter table playlists add column virtual_path" }, + { U_V16_CREATE_VIEW_FILELIST, "create new view filelist" }, + + { U_V16_SCVER, "set schema_version to 16" }, + { U_V1600_SCVER_MAJOR, "set schema_version_major to 16" }, + { U_V1600_SCVER_MINOR, "set schema_version_minor to 00" }, + }; + +static int +db_upgrade_v16(void) +{ + sqlite3_stmt *stmt; + char *query; + char *uquery; + char *errmsg; + char *artist; + char *album; + char *title; + int id; + char *path; + int data_kind; + char virtual_path[PATH_MAX]; + int ret; + + query = "SELECT id, album_artist, album, title, path, data_kind FROM files;"; + + DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); + + ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); + if (ret != SQLITE_OK) + { + DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); + return -1; + } + + while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) + { + id = sqlite3_column_int(stmt, 0); + artist = (char *)sqlite3_column_text(stmt, 1); + album = (char *)sqlite3_column_text(stmt, 2); + title = (char *)sqlite3_column_text(stmt, 3); + path = (char *)sqlite3_column_text(stmt, 4); + data_kind = sqlite3_column_int(stmt, 5); + + if (strncmp(path, "http:", strlen("http:")) == 0) + { + snprintf(virtual_path, PATH_MAX, "/http:/%s", title); + } + else if (strncmp(path, "spotify:", strlen("spotify:")) == 0) + { + snprintf(virtual_path, PATH_MAX, "/spotify:/%s/%s/%s", artist, album, title); + } + else + { + snprintf(virtual_path, PATH_MAX, "/file:%s", path); + } + + uquery = sqlite3_mprintf("UPDATE files SET virtual_path = '%q' WHERE id = %d;", virtual_path, id); + ret = sqlite3_exec(hdl, uquery, NULL, NULL, &errmsg); + if (ret != SQLITE_OK) + { + DPRINTF(E_LOG, L_DB, "Error updating files: %s\n", errmsg); + } + + sqlite3_free(uquery); + sqlite3_free(errmsg); + } + + sqlite3_finalize(stmt); + + + query = "SELECT id, title, path, type FROM playlists;"; + + DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); + + ret = sqlite3_prepare_v2(hdl, query, -1, &stmt, NULL); + if (ret != SQLITE_OK) + { + DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); + return -1; + } + + while ((ret = sqlite3_step(stmt)) == SQLITE_ROW) + { + id = sqlite3_column_int(stmt, 0); + title = (char *)sqlite3_column_text(stmt, 1); + path = (char *)sqlite3_column_text(stmt, 2); + data_kind = sqlite3_column_int(stmt, 3); + + if (data_kind == 0) /* Excludes default playlists */ + { + if (strncmp(path, "spotify:", strlen("spotify:")) == 0) + { + snprintf(virtual_path, PATH_MAX, "/spotify:/%s", title); + } + else + { + snprintf(virtual_path, PATH_MAX, "/file:%s", path); + } + + uquery = sqlite3_mprintf("UPDATE playlists SET virtual_path = '%q' WHERE id = %d;", virtual_path, id); + ret = sqlite3_exec(hdl, uquery, NULL, NULL, &errmsg); + if (ret != SQLITE_OK) + { + DPRINTF(E_LOG, L_DB, "Error updating playlists: %s\n", errmsg); + } + + sqlite3_free(uquery); + sqlite3_free(errmsg); + } + } + + sqlite3_free(errmsg); + sqlite3_finalize(stmt); + + return 0; +} + static int db_check_version(void) { @@ -5539,6 +5900,15 @@ db_check_version(void) case 1500: ret = db_generic_upgrade(db_upgrade_v1501_queries, sizeof(db_upgrade_v1501_queries) / sizeof(db_upgrade_v1501_queries[0])); + + /* FALLTHROUGH */ + + case 1501: + ret = db_generic_upgrade(db_upgrade_v16_queries, sizeof(db_upgrade_v16_queries) / sizeof(db_upgrade_v16_queries[0])); + if (ret < 0) + return -1; + + ret = db_upgrade_v16(); if (ret < 0) return -1; diff --git a/src/db.h b/src/db.h index a7d72bf4..633c5bfd 100644 --- a/src/db.h +++ b/src/db.h @@ -48,6 +48,12 @@ enum query_type { #define ARTWORK_PARENTDIR 5 #define ARTWORK_SPOTIFY 6 +enum filelistitem_type { + F_PLAYLIST = 1, + F_DIR = 2, + F_FILE = 3, +}; + struct query_params { /* Query parameters, filled in by caller */ enum query_type type; @@ -149,6 +155,8 @@ struct media_file_info { char *album_sort; char *composer_sort; char *album_artist_sort; + + char *virtual_path; }; #define mfi_offsetof(field) offsetof(struct media_file_info, field) @@ -170,6 +178,7 @@ struct playlist_info { char *path; /* path of underlying playlist */ uint32_t index; /* index of playlist for paths with multiple playlists */ uint32_t special_id; /* iTunes identifies certain 'special' playlists with special meaning */ + char *virtual_path; /* virtual path of underlying playlist */ }; #define pli_offsetof(field) offsetof(struct playlist_info, field) @@ -273,10 +282,17 @@ struct db_media_file_info { char *album_sort; char *composer_sort; char *album_artist_sort; + char *virtual_path; }; #define dbmfi_offsetof(field) offsetof(struct db_media_file_info, field) +struct filelist_info { + char *virtual_path; + uint32_t time_modified; + enum filelistitem_type type; +}; + struct watch_info { int wd; char *path; @@ -300,6 +316,9 @@ db_escape_string(const char *str); void free_pi(struct pairing_info *pi, int content_only); +void +free_fi(struct filelist_info *fi, int content_only); + void free_mfi(struct media_file_info *mfi, int content_only); @@ -394,6 +413,9 @@ db_file_stamp_bypath(char *path, time_t *stamp, int *id); struct media_file_info * db_file_fetch_byid(int id); +struct media_file_info * +db_file_fetch_byvirtualpath(char *path); + int db_file_add(struct media_file_info *mfi); @@ -425,11 +447,14 @@ db_pl_ping_bymatch(char *path, int isdir); struct playlist_info * db_pl_fetch_bypath(char *path); +struct playlist_info * +db_pl_fetch_byvirtualpath(char *virtual_path); + struct playlist_info * db_pl_fetch_bytitlepath(char *title, char *path); int -db_pl_add(char *title, char *path, int *id); +db_pl_add(char *title, char *path, char *virtual_path, int *id); int db_pl_add_item_bypath(int plid, char *path); @@ -441,7 +466,7 @@ void db_pl_clear_items(int id); int -db_pl_update(char *title, char *path, int id); +db_pl_update(char *title, char *path, char *virtual_path, int id); void db_pl_delete(int id); @@ -465,6 +490,13 @@ db_groups_clear(void); int db_group_persistentid_byid(int id, int64_t *persistentid); +/* Filelist */ +int +db_mpd_start_query_filelist(struct query_params *qp, char *path); + +int +db_mpd_query_fetch_filelist(struct query_params *qp, struct filelist_info *fi); + /* Remotes */ int db_pairing_add(struct pairing_info *pi); diff --git a/src/filescanner.c b/src/filescanner.c index 0a5dab05..f7e8a0a2 100644 --- a/src/filescanner.c +++ b/src/filescanner.c @@ -77,6 +77,22 @@ #endif +struct filescanner_command; + +typedef int (*cmd_func)(struct filescanner_command *cmd); + +struct filescanner_command +{ + pthread_mutex_t lck; + pthread_cond_t cond; + + cmd_func func; + + int nonblock; + + int ret; +}; + #define F_SCAN_BULK (1 << 0) #define F_SCAN_RESCAN (1 << 1) #define F_SCAN_FAST (1 << 2) @@ -108,6 +124,7 @@ struct stacked_dir { }; +static int cmd_pipe[2]; #ifdef USE_EVENTFD static int exit_efd; #else @@ -118,6 +135,7 @@ static int inofd; static struct event_base *evbase_scan; static struct event inoev; static struct event exitev; +static struct event cmdev; static pthread_t tid_scan; static struct deferred_pl *playlists; static struct stacked_dir *dirstack; @@ -125,6 +143,9 @@ static struct stacked_dir *dirstack; /* Count of files scanned during a bulk scan */ static int counter; +/* Flag for scan in progress */ +static int scanning; + /* Forward */ static void bulk_scan(int flags); @@ -132,6 +153,46 @@ static int inofd_event_set(void); static void inofd_event_unset(void); +static int +filescanner_initscan(struct filescanner_command *cmd); +static int +filescanner_fullrescan(struct filescanner_command *cmd); + + +/* ---------------------------- COMMAND EXECUTION -------------------------- */ + +static int +send_command(struct filescanner_command *cmd) +{ + int ret; + + if (!cmd->func) + { + DPRINTF(E_LOG, L_SCAN, "BUG: cmd->func is NULL!\n"); + return -1; + } + + ret = write(cmd_pipe[1], &cmd, sizeof(cmd)); + if (ret != sizeof(cmd)) + { + DPRINTF(E_LOG, L_SCAN, "Could not send command: %s\n", strerror(errno)); + return -1; + } + + return 0; +} + +static int +nonblock_command(struct filescanner_command *cmd) +{ + int ret; + + ret = send_command(cmd); + if (ret < 0) + return -1; + + return 0; +} static int push_dir(struct stacked_dir **s, char *path) @@ -573,6 +634,7 @@ filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct char *filename; time_t stamp; int id; + char virtual_path[PATH_MAX]; int ret; filename = strrchr(path, '/'); @@ -678,6 +740,22 @@ filescanner_process_media(char *path, time_t mtime, off_t size, int type, struct fixup_tags(mfi); + if (type & F_SCAN_TYPE_URL) + { + snprintf(virtual_path, PATH_MAX, "/http:/%s", mfi->title); + mfi->virtual_path = strdup(virtual_path); + } + else if (type & F_SCAN_TYPE_SPOTIFY) + { + snprintf(virtual_path, PATH_MAX, "/spotify:/%s/%s/%s", mfi->album_artist, mfi->album, mfi->title); + mfi->virtual_path = strdup(virtual_path); + } + else + { + snprintf(virtual_path, PATH_MAX, "/file:%s", mfi->path); + mfi->virtual_path = strdup(virtual_path); + } + if (mfi->id == 0) db_file_add(mfi); else @@ -815,11 +893,7 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags) DPRINTF(E_LOG, L_SCAN, "Startup rescan triggered, found init-rescan file: %s\n", file); - inofd_event_unset(); // Clears all inotify watches - db_watch_clear(); - - inofd_event_set(); - bulk_scan(F_SCAN_BULK | F_SCAN_RESCAN); + filescanner_initscan(NULL); break; case FILE_CTRL_FULLSCAN: @@ -828,13 +902,7 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags) DPRINTF(E_LOG, L_SCAN, "Full rescan triggered, found full-rescan file: %s\n", file); - player_playback_stop(); - player_queue_clear(); - inofd_event_unset(); // Clears all inotify watches - db_purge_all(); // Clears files, playlists, playlistitems, inotify and groups - - inofd_event_set(); - bulk_scan(F_SCAN_BULK); + filescanner_fullrescan(NULL); break; default: @@ -1064,6 +1132,9 @@ bulk_scan(int flags) time_t end; int i; + // Set global flag to avoid queued scan requests + scanning = 1; + start = time(NULL); playlists = NULL; @@ -1135,6 +1206,9 @@ bulk_scan(int flags) DPRINTF(E_DBG, L_SCAN, "Running post library scan jobs\n"); db_hook_post_scan(); } + + // Set scan in progress flag to FALSE + scanning = 0; } @@ -1210,7 +1284,6 @@ filescanner(void *arg) DPRINTF(E_FATAL, L_SCAN, "Scan event loop terminated ahead of time!\n"); db_perthread_deinit(); - //artworkcache_perthread_deinit(); pthread_exit(NULL); } @@ -1872,6 +1945,134 @@ exit_cb(int fd, short event, void *arg) scan_exit = 1; } +static void +command_cb(int fd, short what, void *arg) +{ + struct filescanner_command *cmd; + int ret; + + ret = read(cmd_pipe[0], &cmd, sizeof(cmd)); + if (ret != sizeof(cmd)) + { + DPRINTF(E_LOG, L_SCAN, "Could not read command! (read %d): %s\n", ret, (ret < 0) ? strerror(errno) : "-no error-"); + goto readd; + } + + if (cmd->nonblock) + { + cmd->func(cmd); + + free(cmd); + goto readd; + } + + pthread_mutex_lock(&cmd->lck); + + ret = cmd->func(cmd); + cmd->ret = ret; + + pthread_cond_signal(&cmd->cond); + pthread_mutex_unlock(&cmd->lck); + + readd: + event_add(&cmdev, NULL); +} + +static int +filescanner_initscan(struct filescanner_command *cmd) +{ + DPRINTF(E_LOG, L_SCAN, "Startup rescan triggered\n"); + + inofd_event_unset(); // Clears all inotify watches + db_watch_clear(); + + inofd_event_set(); + bulk_scan(F_SCAN_BULK | F_SCAN_RESCAN); + + return 0; +} + +static int +filescanner_fullrescan(struct filescanner_command *cmd) +{ + DPRINTF(E_LOG, L_SCAN, "Full rescan triggered\n"); + + player_playback_stop(); + player_queue_clear(); + inofd_event_unset(); // Clears all inotify watches + db_purge_all(); // Clears files, playlists, playlistitems, inotify and groups + + inofd_event_set(); + bulk_scan(F_SCAN_BULK); + + return 0; +} + +void +filescanner_trigger_initscan(void) +{ + struct filescanner_command *cmd; + + if (scanning) + { + DPRINTF(E_INFO, L_SCAN, "Scan already running, ignoring request to trigger a new init scan\n"); + return; + } + + + cmd = (struct filescanner_command *)malloc(sizeof(struct filescanner_command)); + if (!cmd) + { + DPRINTF(E_LOG, L_SCAN, "Could not allocate cache_command\n"); + return; + } + + memset(cmd, 0, sizeof(struct filescanner_command)); + + cmd->nonblock = 1; + + cmd->func = filescanner_initscan; + + nonblock_command(cmd); +} + +void +filescanner_trigger_fullrescan(void) +{ + struct filescanner_command *cmd; + + if (scanning) + { + DPRINTF(E_INFO, L_SCAN, "Scan already running, ignoring request to trigger a new init scan\n"); + return; + } + + cmd = (struct filescanner_command *)malloc(sizeof(struct filescanner_command)); + if (!cmd) + { + DPRINTF(E_LOG, L_SCAN, "Could not allocate cache_command\n"); + return; + } + + memset(cmd, 0, sizeof(struct filescanner_command)); + + cmd->nonblock = 1; + + cmd->func = filescanner_fullrescan; + + nonblock_command(cmd); +} + +/* + * Query the status of the filescanner + * @return 1 if scan is running, otherwise 0 + */ +int +filescanner_scanning(void) +{ + return scanning; +} + /* Thread: main */ int filescanner_init(void) @@ -1879,6 +2080,7 @@ filescanner_init(void) int ret; scan_exit = 0; + scanning = 0; evbase_scan = event_base_new(); if (!evbase_scan) @@ -1924,6 +2126,21 @@ filescanner_init(void) event_base_set(evbase_scan, &exitev); event_add(&exitev, NULL); +# if defined(__linux__) + ret = pipe2(cmd_pipe, O_CLOEXEC); +# else + ret = pipe(cmd_pipe); +# endif + if (ret < 0) + { + DPRINTF(E_LOG, L_SCAN, "Could not create command pipe: %s\n", strerror(errno)); + goto cmd_fail; + } + + event_set(&cmdev, cmd_pipe[0], EV_READ, command_cb, NULL); + event_base_set(evbase_scan, &exitev); + event_add(&cmdev, NULL); + ret = pthread_create(&tid_scan, NULL, filescanner, NULL); if (ret != 0) { @@ -1935,6 +2152,9 @@ filescanner_init(void) return 0; thread_fail: + cmd_fail: + close(cmd_pipe[0]); + close(cmd_pipe[1]); close(inofd); ino_fail: #ifdef USE_EVENTFD @@ -1993,5 +2213,7 @@ filescanner_deinit(void) close(exit_pipe[0]); close(exit_pipe[1]); #endif + close(cmd_pipe[0]); + close(cmd_pipe[1]); event_base_free(evbase_scan); } diff --git a/src/filescanner.h b/src/filescanner.h index 97c628e8..8814f16c 100644 --- a/src/filescanner.h +++ b/src/filescanner.h @@ -36,4 +36,13 @@ void scan_itunes_itml(char *file); #endif +void +filescanner_trigger_initscan(void); + +void +filescanner_trigger_fullrescan(void); + +int +filescanner_scanning(void); + #endif /* !__FILESCANNER_H__ */ diff --git a/src/filescanner_itunes.c b/src/filescanner_itunes.c index 20595052..c73a42f0 100644 --- a/src/filescanner_itunes.c +++ b/src/filescanner_itunes.c @@ -680,6 +680,7 @@ process_pls(plist_t playlists, char *file) int pl_id; uint32_t alen; uint32_t i; + char virtual_path[PATH_MAX]; int ret; alen = plist_array_get_size(playlists); @@ -735,7 +736,8 @@ process_pls(plist_t playlists, char *file) if (pl_id == 0) { - ret = db_pl_add(name, file, &pl_id); + snprintf(virtual_path, PATH_MAX, "/file:%s", file); + ret = db_pl_add(name, file, virtual_path, &pl_id); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Error adding iTunes playlist '%s' (%s)\n", name, file); diff --git a/src/filescanner_playlist.c b/src/filescanner_playlist.c index b3b5e7e7..a21099d0 100644 --- a/src/filescanner_playlist.c +++ b/src/filescanner_playlist.c @@ -91,6 +91,7 @@ scan_playlist(char *file, time_t mtime) int pl_format; int mfi_id; int ret; + char virtual_path[PATH_MAX]; int i; DPRINTF(E_LOG, L_SCAN, "Processing static playlist: %s\n", file); @@ -158,7 +159,9 @@ scan_playlist(char *file, time_t mtime) if (ptr) *ptr = '.'; - ret = db_pl_add(buf, file, &pl_id); + snprintf(virtual_path, PATH_MAX, "/file:%s", file); + + ret = db_pl_add(buf, file, virtual_path, &pl_id); if (ret < 0) { DPRINTF(E_LOG, L_SCAN, "Error adding playlist '%s'\n", file); diff --git a/src/httpd_dacp.c b/src/httpd_dacp.c index 49d49d9d..043bbba3 100644 --- a/src/httpd_dacp.c +++ b/src/httpd_dacp.c @@ -725,6 +725,7 @@ dacp_reply_cue_play(struct evhttp_request *req, struct evbuffer *evbuf, char **u const char *cuequery; const char *param; uint32_t id; + uint32_t pos; int clear; int ret; @@ -773,19 +774,21 @@ dacp_reply_cue_play(struct evhttp_request *req, struct evbuffer *evbuf, char **u dacp_propset_shufflestate(param, NULL); id = 0; + pos = 0; param = evhttp_find_header(query, "index"); if (param) { - ret = safe_atou32(param, &id); + ret = safe_atou32(param, &pos); if (ret < 0) DPRINTF(E_LOG, L_DACP, "Invalid index (%s) in cue request\n", param); } /* If selection was from Up Next queue (command will be playnow), then index is relative */ + //TODO playnow for mode = 1 (index is relativ to the history queue if ((param = evhttp_find_header(query, "command")) && (strcmp(param, "playnow") == 0)) - id += status.pos_pl; + pos += status.pos_pl; - ret = player_playback_start(&id); + ret = player_playback_startpos(pos, &id); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not start playback\n"); @@ -859,6 +862,7 @@ dacp_reply_playspec(struct evhttp_request *req, struct evbuffer *evbuf, char **u const char *shuffle; uint32_t plid; uint32_t id; + uint32_t pos; int ret; /* /ctrl-int/1/playspec?database-spec='dmap.persistentid:0x1'&container-spec='dmap.persistentid:0x5'&container-item-spec='dmap.containeritemid:0x9' @@ -934,7 +938,7 @@ dacp_reply_playspec(struct evhttp_request *req, struct evbuffer *evbuf, char **u DPRINTF(E_DBG, L_DACP, "Playspec request for playlist %d, start song id %d%s\n", plid, id, (shuffle) ? ", shuffle" : ""); - ps = player_queue_make_pl(plid, &id); + ps = player_queue_make_pl(plid, &pos); if (!ps) { DPRINTF(E_LOG, L_DACP, "Could not build song queue from playlist %d\n", plid); @@ -942,7 +946,7 @@ dacp_reply_playspec(struct evhttp_request *req, struct evbuffer *evbuf, char **u goto out_fail; } - DPRINTF(E_DBG, L_DACP, "Playspec start song index is %d\n", id); + DPRINTF(E_DBG, L_DACP, "Playspec start song index is %d\n", pos); player_get_status(&status); @@ -956,7 +960,7 @@ dacp_reply_playspec(struct evhttp_request *req, struct evbuffer *evbuf, char **u if (shuffle) dacp_propset_shufflestate(shuffle, NULL); - ret = player_playback_start(&id); + ret = player_playback_startpos(pos, &id); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not start playback\n"); @@ -1129,15 +1133,6 @@ dacp_reply_playresume(struct evhttp_request *req, struct evbuffer *evbuf, char * evhttp_send_reply(req, HTTP_NOCONTENT, "No Content", evbuf); } -static struct player_source * -next_ps(struct player_source *ps, char shuffle) -{ - if (shuffle) - return ps->shuffle_next; - else - return ps->pl_next; -} - static int playqueuecontents_add_source(struct evbuffer *songlist, uint32_t source_id, int pos_in_queue, uint32_t plid) { @@ -1197,10 +1192,9 @@ dacp_reply_playqueuecontents(struct evhttp_request *req, struct evbuffer *evbuf, struct daap_session *s; struct evbuffer *songlist; struct evbuffer *playlists; - struct player_source *ps; - struct player_source *head; struct player_status status; struct player_history *history; + struct player_queue *queue; const char *param; int span; int i; @@ -1271,19 +1265,13 @@ dacp_reply_playqueuecontents(struct evhttp_request *req, struct evbuffer *evbuf, /* Get queue and make songlist only if playing or paused */ if (status.status != PLAY_STOPPED) { - /* Fast forward to song currently being played */ - head = player_queue_get(); - if (head) + queue = player_queue_get(-1, abs(span), status.shuffle); + if (queue) { - ps = head; - while ((ps->id != status.id) && (ps = next_ps(ps, status.shuffle)) && (ps != head)) - i++; - - while ((n < abs(span)) && (ps = next_ps(ps, status.shuffle)) && (ps != head)) + i = queue->start_pos; + for (n = 0; (n < queue->count) && (n < abs(span)); n++) { - n++; - - ret = playqueuecontents_add_source(songlist, ps->id, (n + i + 1), status.plid); + ret = playqueuecontents_add_source(songlist, queue->queue[n], (n + i + 1), status.plid); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not add song to songlist for playqueue-contents\n"); @@ -1293,6 +1281,7 @@ dacp_reply_playqueuecontents(struct evhttp_request *req, struct evbuffer *evbuf, } } } + queue_free(queue); } } @@ -1513,7 +1502,7 @@ dacp_reply_playqueueedit_add(struct evhttp_request *req, struct evbuffer *evbuf, } DPRINTF(E_DBG, L_DACP, "Song queue built, playback starting at index %" PRIu32 "\n", idx); - ret = player_playback_start(&idx); + ret = player_playback_startpos(idx, NULL); if (ret < 0) { DPRINTF(E_LOG, L_DACP, "Could not start playback\n"); diff --git a/src/logger.c b/src/logger.c index a499c959..80accc9c 100644 --- a/src/logger.c +++ b/src/logger.c @@ -43,7 +43,7 @@ static int threshold; static int console; static char *logfilename; static FILE *logfile; -static char *labels[] = { "config", "daap", "db", "httpd", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache" }; +static char *labels[] = { "config", "daap", "db", "httpd", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm", "cache", "mpd" }; static char *severities[] = { "FATAL", "LOG", "WARN", "INFO", "DEBUG", "SPAM" }; diff --git a/src/logger.h b/src/logger.h index a203dc8d..e643f070 100644 --- a/src/logger.h +++ b/src/logger.h @@ -29,8 +29,9 @@ #define L_SPOTIFY 20 #define L_LASTFM 21 #define L_CACHE 22 +#define L_MPD 23 -#define N_LOGDOMAINS 23 +#define N_LOGDOMAINS 24 /* Severities */ #define E_FATAL 0 diff --git a/src/main.c b/src/main.c index 2240c619..606dfcc9 100644 --- a/src/main.c +++ b/src/main.c @@ -61,6 +61,7 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL; #include "cache.h" #include "filescanner.h" #include "httpd.h" +#include "mpd.h" #include "mdns.h" #include "remote_pairing.h" #include "player.h" @@ -724,6 +725,18 @@ main(int argc, char **argv) goto httpd_fail; } +#ifdef MPD + /* Spawn MPD thread */ + ret = mpd_init(); + if (ret != 0) + { + DPRINTF(E_FATAL, L_MAIN, "MPD thread failed to start\n"); + + ret = EXIT_FAILURE; + goto mpd_fail; + } +#endif + /* Start Remote pairing service */ ret = remote_pairing_init(); if (ret != 0) @@ -808,6 +821,13 @@ main(int argc, char **argv) httpd_deinit(); httpd_fail: + DPRINTF(E_LOG, L_MAIN, "TCPd deinit\n"); +#ifdef MPD + DPRINTF(E_LOG, L_MAIN, "MPD deinit\n"); + mpd_deinit(); +#endif + + mpd_fail: DPRINTF(E_LOG, L_MAIN, "Player deinit\n"); player_deinit(); diff --git a/src/mpd.c b/src/mpd.c new file mode 100644 index 00000000..15b04082 --- /dev/null +++ b/src/mpd.c @@ -0,0 +1,3140 @@ +/* + * Copyright (C) 2009-2010 Julien BLACHE + * + * 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 + +#if defined(HAVE_SYS_EVENTFD_H) && defined(HAVE_EVENTFD) +# define USE_EVENTFD +# include +#endif + +#include "logger.h" +#include "db.h" +#include "conffile.h" +#include "misc.h" +#include "mpd.h" + +#include "player.h" +#include "filescanner.h" + + +static pthread_t tid_mpd; + +struct event_base *evbase_mpd; +static int g_exit_pipe[2]; +static struct event *g_exitev; + +#define COMMAND_ARGV_MAX 37 + +/* MPD error codes (taken from ack.h) */ +enum ack +{ + ACK_ERROR_NOT_LIST = 1, + ACK_ERROR_ARG = 2, + ACK_ERROR_PASSWORD = 3, + ACK_ERROR_PERMISSION = 4, + ACK_ERROR_UNKNOWN = 5, + + ACK_ERROR_NO_EXIST = 50, + ACK_ERROR_PLAYLIST_MAX = 51, + ACK_ERROR_SYSTEM = 52, + ACK_ERROR_PLAYLIST_LOAD = 53, + ACK_ERROR_UPDATE_ALREADY = 54, + ACK_ERROR_PLAYER_SYNC = 55, + ACK_ERROR_EXIST = 56, +}; + +enum command_list_type +{ + COMMAND_LIST = 1, + COMMAND_LIST_OK = 2, + COMMAND_LIST_NONE = 3 +}; + +static void +thread_exit(void) +{ + int dummy = 42; + + DPRINTF(E_DBG, L_MPD, "Killing mpd thread\n"); + + if (write(g_exit_pipe[1], &dummy, sizeof(dummy)) != sizeof(dummy)) + DPRINTF(E_LOG, L_MPD, "Could not write to exit fd: %s\n", strerror(errno)); +} + + +/* Thread: mpd */ +static void * +mpd(void *arg) +{ + int ret; + + ret = db_perthread_init(); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error: DB init failed\n"); + + pthread_exit(NULL); + } + + event_base_dispatch(evbase_mpd); + + db_perthread_deinit(); + + pthread_exit(NULL); +} + +static void +exit_cb(int fd, short what, void *arg) +{ + int dummy; + int ret; + + ret = read(g_exit_pipe[0], &dummy, sizeof(dummy)); + if (ret != sizeof(dummy)) + DPRINTF(E_LOG, L_MPD, "Error reading from exit pipe\n"); + + event_base_loopbreak(evbase_mpd); + + event_add(g_exitev, NULL); +} + +/* + * Parses a rage argument of the form START:END (the END item is not included in the range) + * into its start and end position. + * + * @param range the range argument + * @param start_pos set by this method to the start position + * @param end_pos set by this method to the end postion + * @return 0 on success, -1 on failure + */ +static int +mpd_pars_range_arg(char *range, int *start_pos, int *end_pos) +{ + int ret; + + if (strchr(range, ':')) + { + ret = sscanf(range, "%d:%d", start_pos, end_pos); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error parsing range argument '%s' (return code = %d)\n", range, ret); + return -1; + } + } + else + { + ret = safe_atoi32(range, start_pos); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error parsing integer argument '%s' (return code = %d)\n", range, ret); + return -1; + } + + *end_pos = (*start_pos) + 1; + } + + return 0; +} + +/* + * Returns the next unquoted string argument from the input string + */ +static char* +mpd_pars_unquoted(char **input) +{ + char *arg; + + arg = *input; + + while (**input != 0) + { + if (**input == ' ') + { + **input = '\0'; + (*input)++; + return arg; + } + + (*input)++; + } + + return arg; +} + +/* + * Returns the next quoted string argument from the input string + * with the quotes removed + */ +static char* +mpd_pars_quoted(char **input) +{ + char *arg; + + // skip double quote character + (*input)++; + + arg = *input; + + while (**input != '"') + { + // A backslash character escapes the following character + if (**input == '\\') + { + (*input)++; + } + + if (**input == 0) + { + // Error handling for missing double quote at end of parameter + DPRINTF(E_LOG, L_MPD, "Error missing closing double quote in argument\n"); + return NULL; + } + + (*input)++; + } + + **input = '\0'; + (*input)++; + + return arg; +} + +/* + * Parses the argument string into an array of strings. + * Arguments are seperated by a whitespace character and may be wrapped in double quotes. + * + * @param args the arguments + * @param argc the number of arguments in the argument string + * @param argv the array containing the found arguments + */ +static int +mpd_parse_args(char *args, int *argc, char **argv) +{ + char *input; + + input = args; + *argc = 0; + + while (*input != 0) + { + // Ignore whitespace characters + if (*input == ' ') + { + input++; + continue; + } + + // Check if the parameter is wrapped in double quotes + if (*input == '"') + { + argv[*argc] = mpd_pars_quoted(&input); + if (argv[*argc] == NULL) + { + return -1; + } + *argc = *argc + 1; + } + else + { + argv[*argc] = mpd_pars_unquoted(&input); + *argc = *argc + 1; + } + } + + return 0; +} + +/* + * Adds the informations (path, id, tags, etc.) for the given song to the given buffer + * with additional information for the position of this song in the playqueue. + * + * Example output: + * file: foo/bar/song.mp3 + * Last-Modified: 2013-07-14T06:57:59Z + * Time: 172 + * Artist: foo + * AlbumArtist: foo + * ArtistSort: foo + * AlbumArtistSort: foo + * Title: song + * Album: bar + * Track: 1/11 + * Date: 2012-09-11 + * Genre: Alternative + * Disc: 1/1 + * MUSICBRAINZ_ALBUMARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 + * MUSICBRAINZ_ARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 + * MUSICBRAINZ_ALBUMID: 812f4b87-8ad9-41bd-be79-38151f17a2b4 + * MUSICBRAINZ_TRACKID: fde95c39-ee51-48f6-a7f9-b5631c2ed156 + * Pos: 0 + * Id: 1 + * + * @param evbuf the response event buffer + * @param mfi media information + * @param pos_pl position in the playqueue, if -1 the position is ignored + * @return the number of bytes added if successful, or -1 if an error occurred. + */ +static int +mpd_add_mediainfo(struct evbuffer *evbuf, struct media_file_info *mfi, int pos_pl) +{ + int ret; + + if (pos_pl < 0) + { + ret = evbuffer_add_printf(evbuf, + "file: %s\n" + "Time: %d\n" + "Artist: %s\n" + "Album: %s\n" + "Title: %s\n" + "Id: %d\n", + (mfi->virtual_path + 1), + (mfi->song_length / 1000), + mfi->artist, + mfi->album, + mfi->title, + mfi->id); + } + else + { + ret = evbuffer_add_printf(evbuf, + "file: %s\n" + "Time: %d\n" + "Artist: %s\n" + "Album: %s\n" + "Title: %s\n" + "Pos: %d\n" + "Id: %d\n", + (mfi->virtual_path + 1), + (mfi->song_length / 1000), + mfi->artist, + mfi->album, + mfi->title, + pos_pl, + mfi->id); + } + + return ret; +} + +static int +mpd_add_mediainfo_byid(struct evbuffer *evbuf, int id, int pos_pl) +{ + struct media_file_info *mfi; + int ret; + + mfi = db_file_fetch_byid(id); + if (!mfi) + { + DPRINTF(E_LOG, L_MPD, "Error fetching file by id: %d\n", id); + return -1; + } + + ret = mpd_add_mediainfo(evbuf, mfi, pos_pl); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error adding media info for file with id: %d\n", id); + + free_mfi(mfi, 0); + + return -1; + } + + free_mfi(mfi, 0); + return 0; +} + +/* + * Adds the informations (path, id, tags, etc.) for the given song to the given buffer. + * + * Example output: + * file: foo/bar/song.mp3 + * Last-Modified: 2013-07-14T06:57:59Z + * Time: 172 + * Artist: foo + * AlbumArtist: foo + * ArtistSort: foo + * AlbumArtistSort: foo + * Title: song + * Album: bar + * Track: 1/11 + * Date: 2012-09-11 + * Genre: Alternative + * Disc: 1/1 + * MUSICBRAINZ_ALBUMARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 + * MUSICBRAINZ_ARTISTID: c5c2ea1c-4bde-4f4d-bd0b-47b200bf99d6 + * MUSICBRAINZ_ALBUMID: 812f4b87-8ad9-41bd-be79-38151f17a2b4 + * MUSICBRAINZ_TRACKID: fde95c39-ee51-48f6-a7f9-b5631c2ed156 + * Id: 1 + * + * @param evbuf the response event buffer + * @param mfi media information + * @return the number of bytes added if successful, or -1 if an error occurred. + */ +static int +mpd_add_db_media_file_info(struct evbuffer *evbuf, struct db_media_file_info *dbmfi) +{ + uint32_t songlength; + int ret; + + if (safe_atou32(dbmfi->song_length, &songlength) != 0) + { + DPRINTF(E_LOG, L_MPD, "Error converting song length to uint32_t: %s\n", dbmfi->song_length); + return -1; + } + + ret = evbuffer_add_printf(evbuf, + "file: %s\n" + "Time: %d\n" + "Artist: %s\n" + "Album: %s\n" + "Title: %s\n" + "Id: %s\n", + (dbmfi->virtual_path + 1), + (songlength / 1000), + dbmfi->artist, + dbmfi->album, + dbmfi->title, + dbmfi->id); + + return ret; +} + +/* + * Command handler function for 'currentsong' + */ +static int +mpd_command_currentsong(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + + struct player_status status; + int ret; + + player_get_status(&status); + + if (status.status == PLAY_STOPPED) + { + // Return empty evbuffer if there is no current playing song + return 0; + } + + ret = mpd_add_mediainfo_byid(evbuf, status.id, status.pos_pl); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error adding media info for file with id: %d\n", status.id); + ret = asprintf(errmsg, "Error adding media info for file with id: %d", status.id); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * + * Example input: + * idle "database" "mixer" "options" "output" "player" "playlist" "sticker" "update" + */ +static int +mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + DPRINTF(E_WARN, L_MPD, "Idle command is not supported by forked-daapd, there will be no notifications about changes\n"); + return 0; +} + +static int +mpd_command_noidle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + return 0; +} + +/* + * Command handler function for 'status' + * + * Example output: + * volume: -1 + * repeat: 0 + * random: 0 + * single: 0 + * consume: 0 + * playlist: 2 + * playlistlength: 34 + * mixrampdb: 0.000000 + * state: stop + * song: 0 + * songid: 1 + * time: 28:306 + * elapsed: 28.178 + * bitrate: 278 + * audio: 44100:f:2 + * nextsong: 1 + * nextsongid: 2 + */ +static int +mpd_command_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct player_status status; + char *state; + + player_get_status(&status); + + switch (status.status) + { + case PLAY_PAUSED: + state = "pause"; + break; + + case PLAY_PLAYING: + state = "play"; + break; + + default: + state = "stop"; + break; + } + + evbuffer_add_printf(evbuf, + "volume: %d\n" + "repeat: %d\n" + "random: %d\n" + "single: %d\n" + "consume: %d\n" + "playlist: %d\n" + "playlistlength: %d\n" + "mixrampdb: 0.000000\n" + "state: %s\n", + status.volume, + (status.repeat == REPEAT_OFF ? 0 : 1), + status.shuffle, + (status.repeat == REPEAT_SONG ? 1 : 0), + 0 /* consume: not supported by forked-daapd, always return 'off' */, + status.plid, + status.playlistlength, + state); + + if (status.status != PLAY_STOPPED) + { + evbuffer_add_printf(evbuf, + "song: %d\n" + "songid: %d\n" + "time: %d:%d\n" + "elapsed: %#.3f\n" + "bitrate: 128\n" + "audio: 44100:16:2\n" + "nextsong: %d\n" + "nextsongid: %d\n", + status.pos_pl, + status.id, + (status.pos_ms / 1000), (status.len_ms / 1000), + (status.pos_ms / 1000.0), + status.next_pos_pl, + status.next_id); + } + + if (filescanner_scanning()) + { + evbuffer_add(evbuf, "updating_db: 1\n", 15); + } + + if (status.status != PLAY_STOPPED) + { + evbuffer_add_printf(evbuf, + "nextsong: %d\n" + "nextsongid: %d\n", + status.next_pos_pl, + status.next_id); + } + + return 0; +} + +/* + * Command handler function for 'stats' + */ +static int +mpd_command_stats(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + //TODO implement command stats + evbuffer_add_printf(evbuf, + "artists: %d\n" + "albums: %d\n" + "songs: %d\n" + "uptime: %d\n" //in seceonds + "db_playtime: %d\n" + "db_update: %d\n" + "playtime: %d\n", + 1, + 2, + 3, + 4, + 5, + 6, + 7); + + return 0; +} + +/* + * Command handler function for 'random' + * Sets the shuffle mode, expects argument argv[1] to be an integer with + * 0 = disable shuffle + * 1 = enable shuffle + */ +static int +mpd_command_random(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int enable; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'random'\n"); + ret = asprintf(errmsg, "Missing argument for command 'random'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = safe_atoi32(argv[1], &enable); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + player_shuffle_set(enable); + return 0; +} + +/* + * Command handler function for 'repeat' + * Sets the repeat mode, expects argument argv[1] to be an integer with + * 0 = repeat off + * 1 = repeat all + */ +static int +mpd_command_repeat(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int enable; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'repeat'\n"); + ret = asprintf(errmsg, "Missing argument for command 'repeat'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = safe_atoi32(argv[1], &enable); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + if (enable == 0) + player_repeat_set(REPEAT_OFF); + else + player_repeat_set(REPEAT_ALL); + + return 0; +} + +/* + * Command handler function for 'setvol' + * Sets the volume, expects argument argv[1] to be an integer 0-100 + */ +static int +mpd_command_setvol(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int volume; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'setvol'\n"); + ret = asprintf(errmsg, "Missing argument for command 'setvol'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = safe_atoi32(argv[1], &volume); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + player_volume_set(volume); + + return 0; +} + +/* + * Command handler function for 'single' + * Sets the repeat mode, expects argument argv[1] to be an integer. + * forked-daapd only allows single-mode in combination with repeat, therefor the command + * single translates (depending on the current repeat mode) into: + * a) if repeat off: + * 0 = repeat off + * 1 = repeat song + * b) if repeat all: + * 0 = repeat all + * 1 = repeat song + * c) if repeat song: + * 0 = repeat all + * 1 = repeat song + */ +static int +mpd_command_single(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int enable; + struct player_status status; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'single'\n"); + ret = asprintf(errmsg, "Missing argument for command 'single'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = safe_atoi32(argv[1], &enable); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + player_get_status(&status); + + if (enable == 0 && status.repeat != REPEAT_OFF) + player_repeat_set(REPEAT_ALL); + else if (enable == 0) + player_repeat_set(REPEAT_OFF); + else + player_repeat_set(REPEAT_SONG); + + return 0; +} + +/* + * Command handler function for 'replay_gain_status' + * forked-daapd does not support replay gain, therefor this function returns always + * "replay_gain_mode: off". + */ +static int +mpd_command_replay_gain_status(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + evbuffer_add(evbuf, "replay_gain_mode: off\n", 22); + return 0; +} + +/* + * Command handler function for 'volume' + * Changes the volume by the given amount, expects argument argv[1] to be an integer + * + * According to the mpd protocoll specification this function is deprecated. + */ +static int +mpd_command_volume(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct player_status status; + int volume; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'volume'\n"); + ret = asprintf(errmsg, "Missing argument for command 'volume'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = safe_atoi32(argv[1], &volume); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + player_get_status(&status); + + volume += status.volume; + + player_volume_set(volume); + + return 0; +} + +/* + * Command handler function for 'next' + * Skips to the next song in the playqueue + */ +static int +mpd_command_next(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int ret; + + ret = player_playback_next(); + + if (ret < 0) + { + DPRINTF(E_DBG, L_MPD, "Failed to skip to next song\n"); + ret = asprintf(errmsg, "Failed to skip to next song"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + ret = player_playback_start(NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Player returned an error for start after nextitem\n"); + ret = asprintf(errmsg, "Player returned an error for start after nextitem\n"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'pause' + * Toggles pause/play, if the optional argument argv[1] is present, it must be an integer with + * 0 = play + * 1 = pause + */ +static int +mpd_command_pause(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int pause; + struct player_status status; + int ret; + + pause = 1; + if (argc > 1) + { + ret = safe_atoi32(argv[1], &pause); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + } + else + { + player_get_status(&status); + + if (status.status != PLAY_PLAYING) + pause = 0; + } + + if (pause == 1) + ret = player_playback_pause(); + else + ret = player_playback_start(NULL); + + if (ret != 0) + { + DPRINTF(E_LOG, L_MPD, "Failed to pause playback\n"); + ret = asprintf(errmsg, "Failed to pause playback"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'play' + * Starts playback, the optional argument argv[1] represents the position in the playqueue + * where to start playback. + */ +static int +mpd_command_play(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int songpos; + struct player_status status; + int ret; + + player_get_status(&status); + + //TODO verfiy handling of play with parameter if already playing + if (status.status == PLAY_PLAYING) + { + ret = player_playback_pause(); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error pausing playback\n"); + } + } + + songpos = 0; + if (argc > 1) + { + ret = safe_atoi32(argv[1], &songpos); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + } + + if (songpos > 0) + ret = player_playback_startpos(songpos, NULL); + else + ret = player_playback_start(NULL); + + if (ret != 0) + { + DPRINTF(E_LOG, L_MPD, "Failed to start playback\n"); + ret = asprintf(errmsg, "Failed to start playback"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'playid' + * Starts playback, the optional argument argv[1] represents the songid of the song + * where to start playback. + */ +static int +mpd_command_playid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + uint32_t id; + struct player_status status; + int ret; + + player_get_status(&status); + + //TODO verfiy handling of play with parameter if already playing + if (status.status == PLAY_PLAYING) + { + ret = player_playback_pause(); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error pausing playback\n"); + } + } + + id = 0; + if (argc > 1) + { + ret = safe_atou32(argv[1], &id); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + } + + if (id > 0) + ret = player_playback_startid(id, NULL); + else + ret = player_playback_start(NULL); + + if (ret != 0) + { + DPRINTF(E_LOG, L_MPD, "Failed to start playback\n"); + ret = asprintf(errmsg, "Failed to start playback"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'previous' + * Skips to the previous song in the playqueue + */ +static int +mpd_command_previous(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int ret; + + ret = player_playback_prev(); + + if (ret < 0) + { + DPRINTF(E_DBG, L_MPD, "Failed to skip to previous song\n"); + ret = asprintf(errmsg, "Failed to skip to previous song"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + ret = player_playback_start(NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Player returned an error for start after previtem\n"); + ret = asprintf(errmsg, "Player returned an error for start after previtem\n"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'seekid' + * Seeks to song at the given position in argv[1] to the position in seconds given in argument argv[2] + * (fractions allowed). + */ +static int +mpd_command_seek(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + uint32_t songpos; + float seek_target_sec; + int seek_target_msec; + int ret; + + if (argc < 3) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'seekcur'\n"); + ret = asprintf(errmsg, "Missing argument for command 'seekcur'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = safe_atou32(argv[1], &songpos); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + //TODO Allow seeking in songs not currently playing + if (songpos != 0) + { + DPRINTF(E_LOG, L_MPD, "Given song is not the current playing one, seeking is not supported\n"); + ret = asprintf(errmsg, "Given song is not the current playing one, seeking is not supported\n"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + seek_target_sec = strtof(argv[2], NULL); + seek_target_msec = seek_target_sec * 1000; + + ret = player_playback_seek(seek_target_msec); + + if (ret < 0) + { + DPRINTF(E_DBG, L_MPD, "Failed to seek current song to time %d msec\n", seek_target_msec); + ret = asprintf(errmsg, "Failed to seek current song to time %d msec\n", seek_target_msec); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + ret = player_playback_start(NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Player returned an error for start after seekcur\n"); + ret = asprintf(errmsg, "Player returned an error for start after seekcur\n"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'seekid' + * Seeks to song with id given in argv[1] to the position in seconds given in argument argv[2] + * (fractions allowed). + */ +static int +mpd_command_seekid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct player_status status; + uint32_t id; + float seek_target_sec; + int seek_target_msec; + int ret; + + if (argc < 3) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'seekcur'\n"); + ret = asprintf(errmsg, "Missing argument for command 'seekcur'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = safe_atou32(argv[1], &id); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + //TODO Allow seeking in songs not currently playing + player_get_status(&status); + if (status.id != id) + { + DPRINTF(E_LOG, L_MPD, "Given song is not the current playing one, seeking is not supported\n"); + ret = asprintf(errmsg, "Given song is not the current playing one, seeking is not supported\n"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + seek_target_sec = strtof(argv[2], NULL); + seek_target_msec = seek_target_sec * 1000; + + ret = player_playback_seek(seek_target_msec); + + if (ret < 0) + { + DPRINTF(E_DBG, L_MPD, "Failed to seek current song to time %d msec\n", seek_target_msec); + ret = asprintf(errmsg, "Failed to seek current song to time %d msec\n", seek_target_msec); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + ret = player_playback_start(NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Player returned an error for start after seekcur\n"); + ret = asprintf(errmsg, "Player returned an error for start after seekcur\n"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'seekcur' + * Seeks the current song to the position in seconds given in argument argv[1] (fractions allowed). + */ +static int +mpd_command_seekcur(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + float seek_target_sec; + int seek_target_msec; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'seekcur'\n"); + ret = asprintf(errmsg, "Missing argument for command 'seekcur'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + seek_target_sec = strtof(argv[1], NULL); + seek_target_msec = seek_target_sec * 1000; + + // TODO If prefixed by '+' or '-', then the time is relative to the current playing position. + ret = player_playback_seek(seek_target_msec); + + if (ret < 0) + { + DPRINTF(E_DBG, L_MPD, "Failed to seek current song to time %d msec\n", seek_target_msec); + ret = asprintf(errmsg, "Failed to seek current song to time %d msec\n", seek_target_msec); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + ret = player_playback_start(NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Player returned an error for start after seekcur\n"); + ret = asprintf(errmsg, "Player returned an error for start after seekcur\n"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'stop' + * Stop playback. + */ +static int +mpd_command_stop(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int ret; + + ret = player_playback_stop(); + + if (ret != 0) + { + DPRINTF(E_DBG, L_MPD, "Failed to stop playback\n"); + ret = asprintf(errmsg, "Failed to stop playback"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'add' + * Adds the all songs under the given path to the end of the playqueue (directories add recursively). + * Expects argument argv[1] to be a path to a single file or directory. + */ +static int +mpd_command_add(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct player_source *ps; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'add'\n"); + ret = asprintf(errmsg, "Missing argument for command 'add'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ps = player_queue_make_mpd(argv[1], 1); + + if (!ps) + { + DPRINTF(E_DBG, L_MPD, "Failed to add song '%s' to playlist\n", argv[1]); + ret = asprintf(errmsg, "Failed to add song '%s' to playlist\n", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + player_queue_add(ps); + + ret = player_playback_start(NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Could not start playback\n"); + } + + return 0; +} + +/* + * Command handler function for 'addid' + * Adds the song under the given path to the end or to the given position of the playqueue. + * Expects argument argv[1] to be a path to a single file. argv[2] is optional, if present + * it must be an integer representing the position in the playqueue. + */ +static int +mpd_command_addid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct player_source *ps; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'addid'\n"); + ret = asprintf(errmsg, "Missing argument for command 'addid'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + //TODO if argc > 2 add song at position argv[2] + if (argc > 2) + { + DPRINTF(E_LOG, L_MPD, "Adding at a specified position not supported for 'addid', adding songs at end of queue.\n"); + } + + ps = player_queue_make_mpd(argv[1], 0); + + if (!ps) + { + DPRINTF(E_DBG, L_MPD, "Failed to add song '%s' to playlist\n", argv[1]); + ret = asprintf(errmsg, "Failed to add song '%s' to playlist\n", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + + player_queue_add(ps); + + evbuffer_add_printf(evbuf, + "addid: %s\n" + "Id: %d\n", + argv[1], + ps->id); + + ret = player_playback_start(NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Could not start playback\n"); + } + + return 0; +} + +/* + * Command handler function for 'clear' + * Stops playback and removes all songs from the playqueue + */ +static int +mpd_command_clear(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int ret; + + ret = player_playback_stop(); + if (ret != 0) + { + DPRINTF(E_DBG, L_MPD, "Failed to stop playback\n"); + } + + player_queue_clear(); + + return 0; +} + +/* + * Command handler function for 'delete' + * Removes songs from the playqueue. Expects argument argv[1] (optional) to be an integer or + * an integer range {START:END} representing the position of the songs in the playlist, that + * should be removed. + */ +static int +mpd_command_delete(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct player_status status; + uint32_t start_pos; + int pos; + int ret; + + // If argv[1] is ommited clear the whole queue except the current playing one + if (argc < 2) + { + player_queue_empty(0); + return 0; + } + + // If argument argv[1] is present remove only the specified songs + //TODO support ranges for argv[1] + ret = safe_atou32(argv[1], &start_pos); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + player_get_status(&status); + + pos = start_pos - status.pos_pl; + + if (pos < 1) + { + DPRINTF(E_LOG, L_MPD, "Removing playing or previously played song not supported (song position %d)\n", pos); + ret = asprintf(errmsg, "Removing playing or previously played song not supported (song position %d)", pos); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = player_queue_remove(pos); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Failed to remove song at position '%d'\n", pos); + ret = asprintf(errmsg, "Failed to remove song at position '%d'", pos); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'deleteid' + * Removes the song with given id from the playqueue. Expects argument argv[1] to be an integer (song id). + */ +static int +mpd_command_deleteid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + uint32_t songid; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'deleteid'\n"); + ret = asprintf(errmsg, "Missing argument for command 'deleteid'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = safe_atou32(argv[1], &songid); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + ret = player_queue_removeid(songid); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Failed to remove song with id '%s'\n", argv[1]); + ret = asprintf(errmsg, "Failed to remove song with id '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + return 0; +} + +/* + * Command handler function for 'playlistid' + * Displays a list of all songs in the queue, or if the optional argument is given, displays information + * only for the song with ID. + * + * The order of the songs is always the not shuffled order. + */ +static int +mpd_command_playlistid(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct player_queue *queue; + uint32_t songid; + int pos_pl; + int i; + int ret; + + songid = 0; + + if (argc > 1) + { + ret = safe_atou32(argv[1], &songid); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + } + + // Get the whole queue (start_pos = 0, end_pos = -1) + queue = player_queue_get(0, -1, 0); + + if (!queue) + { + // Queue is emtpy + return 0; + } + + pos_pl = queue->start_pos; + for (i = 0; i < queue->count; i++) + { + if (songid == 0 || songid == queue->queue[i]) + { + ret = mpd_add_mediainfo_byid(evbuf, queue->queue[i], pos_pl); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error adding media info for file with id: %d\n", queue->queue[i]); + ret = asprintf(errmsg, "Error adding media info for file with id: %d\n", queue->queue[i]); + + queue_free(queue); + + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + } + + pos_pl++; + } + + queue_free(queue); + + return 0; +} + + +/* + * Command handler function for 'playlistinfo' + * Displays a list of all songs in the queue, or if the optional argument is given, displays information + * only for the song SONGPOS or the range of songs START:END given in argv[1]. + * + * The order of the songs is always the not shuffled order. + */ +static int +mpd_command_playlistinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct player_queue *queue; + int start_pos; + int end_pos; + int pos_pl; + int i; + int ret; + + start_pos = 0; + end_pos = -1; + + if (argc > 1) + { + ret = mpd_pars_range_arg(argv[1], &start_pos, &end_pos); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Argument doesn't convert to integer or range: '%s'\n", argv[1]); + ret = asprintf(errmsg, "Argument doesn't convert to integer or range: '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + } + + queue = player_queue_get(start_pos, end_pos, 0); + + if (!queue) + { + // Queue is emtpy + return 0; + } + + pos_pl = queue->start_pos; + for (i = 0; i < queue->count; i++) + { + ret = mpd_add_mediainfo_byid(evbuf, queue->queue[i], pos_pl); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error adding media info for file with id: %d\n", queue->queue[i]); + ret = asprintf(errmsg, "Error adding media info for file with id: %d\n", queue->queue[i]); + + queue_free(queue); + + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + pos_pl++; + } + + queue_free(queue); + + return 0; +} + +/* + * Command handler function for 'load' + * Adds the playlist given by virtual-path in argv[1] to the queue. + */ +static int +mpd_command_load(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + char path[PATH_MAX]; + struct playlist_info *pli; + struct player_source *ps; + uint32_t pos; + int ret; + + if (argc < 2) + { + DPRINTF(E_LOG, L_MPD, "Missing argument for command 'load'\n"); + ret = asprintf(errmsg, "Missing argument for command 'load'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + if (strncmp(argv[1], "/", 1) == 0) + { + ret = snprintf(path, sizeof(path), "%s", argv[1]); + } + else + { + ret = snprintf(path, sizeof(path), "/%s", argv[1]); + } + + pli = db_pl_fetch_byvirtualpath(path); + if (!pli) + { + DPRINTF(E_LOG, L_MPD, "Playlist not found for path '%s'\n", argv[1]); + ret = asprintf(errmsg, "Playlist not found for path '%s'", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + //TODO If a second parameter is given only add the specified range of songs to the playqueue + + ps = player_queue_make_pl(pli->id, &pos); + + if (!ps) + { + free_pli(pli, 0); + + DPRINTF(E_DBG, L_MPD, "Failed to add song '%s' to playlist\n", argv[1]); + ret = asprintf(errmsg, "Failed to add song '%s' to playlist\n", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + player_queue_add(ps); + + ret = player_playback_start(NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Could not start playback\n"); + } + + return 0; +} + +static int +mpd_get_query_params_find(int argc, char **argv, struct query_params *qp) +{ + char *c1; + char *c2; + int start_pos; + int end_pos; + int i; + int ret; + + c1 = NULL; + c2 = NULL; + + for (i = 0; i < argc; i += 2) + { + if (0 == strcasecmp(argv[i], "any")) + { + c1 = sqlite3_mprintf("(f.artist LIKE '%%%q%%' OR f.album LIKE '%%%q%%' OR f.title LIKE '%%%q%%')", argv[i + 1], argv[i + 1], argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "file")) + { + c1 = sqlite3_mprintf("(f.virtual_path = '/%q')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "base")) + { + c1 = sqlite3_mprintf("(f.virtual_path LIKE '/%q%%')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "modified-since")) + { + DPRINTF(E_WARN, L_MPD, "Special parameter 'modified-since' is not supported by forked-daapd and will be ignored\n"); + } + else if (0 == strcasecmp(argv[i], "window")) + { + ret = mpd_pars_range_arg(argv[i + 1], &start_pos, &end_pos); + if (ret == 0) + { + qp->idx_type = I_SUB; + qp->limit = end_pos - start_pos; + qp->offset = start_pos; + } + else + { + DPRINTF(E_LOG, L_MPD, "Window argument doesn't convert to integer or range: '%s'\n", argv[i + 1]); + } + } + else if (0 == strcasecmp(argv[i], "artist")) + { + c1 = sqlite3_mprintf("(f.artist = '%q')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "albumartist")) + { + c1 = sqlite3_mprintf("(f.album_artist = '%q')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "album")) + { + c1 = sqlite3_mprintf("(f.album = '%q')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "title")) + { + c1 = sqlite3_mprintf("(f.title = '%q')", argv[i + 1]); + } + else + { + DPRINTF(E_WARN, L_MPD, "Parameter '%s' is not supported by forked-daapd and will be ignored\n", argv[i]); + } + + if (c1) + { + if (qp->filter) + c2 = sqlite3_mprintf("%s AND %s", qp->filter, c1); + else + c2 = sqlite3_mprintf("%s", c1); + + if (qp->filter) + sqlite3_free(qp->filter); + + qp->filter = c2; + c2 = NULL; + sqlite3_free(c1); + c1 = NULL; + } + } + + return 0; +} + +static int +mpd_command_find(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct query_params qp; + struct db_media_file_info dbmfi; + int ret; + + if (argc < 3 || ((argc - 1) % 2) != 0) + { + DPRINTF(E_LOG, L_MPD, "Missing argument(s) for command 'find'\n"); + ret = asprintf(errmsg, "Missing argument(s) for command 'find'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + memset(&qp, 0, sizeof(struct query_params)); + + qp.type = Q_ITEMS; + qp.sort = S_NAME; + qp.idx_type = I_NONE; + + mpd_get_query_params_find(argc - 1, argv + 1, &qp); + + ret = db_query_start(&qp); + if (ret < 0) + { + db_query_end(&qp); + + DPRINTF(E_LOG, L_MPD, "Could not start query\n"); + ret = asprintf(errmsg, "Could not start query"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) + { + ret = mpd_add_db_media_file_info(evbuf, &dbmfi); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); + } + } + + db_query_end(&qp); + + return 0; +} + +static int +mpd_command_findadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + return 0; +} + +static int +mpd_command_list(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct query_params qp; + struct db_group_info dbgri; + char *type; + int ret; + + if (argc < 2 || (argc % 2) != 0) + { + DPRINTF(E_LOG, L_MPD, "Missing argument(s) for command 'list'\n"); + ret = asprintf(errmsg, "Missing argument(s) for command 'list'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + memset(&qp, 0, sizeof(struct query_params)); + + if (0 == strcasecmp(argv[1], "artist") + || 0 == strcasecmp(argv[1], "albumartist")) + { + qp.type = Q_GROUP_ARTISTS; + qp.sort = S_ARTIST; + type = "Artist: "; + } + else if (0 == strcasecmp(argv[1], "album")) + { + qp.type = Q_GROUP_ALBUMS; + qp.sort = S_ALBUM; + type = "Album: "; + } + else + { + DPRINTF(E_WARN, L_MPD, "Unsupported type argument for command 'list': %s\n", argv[1]); + return 0; + } + + qp.idx_type = I_NONE; + + if (argc > 2) + { + mpd_get_query_params_find(argc - 2, argv + 2, &qp); + } + + ret = db_query_start(&qp); + if (ret < 0) + { + db_query_end(&qp); + + DPRINTF(E_LOG, L_MPD, "Could not start query\n"); + ret = asprintf(errmsg, "Could not start query"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + while ((ret = db_query_fetch_group(&qp, &dbgri)) == 0) + { + evbuffer_add_printf(evbuf, + "%s%s\n", + type, + dbgri.itemname); + } + + db_query_end(&qp); + + return 0; +} + +/* + * Command handler function for 'lsinfo' + * Lists the contents of the directory given in argv[1]. + */ +static int +mpd_command_lsinfo(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct query_params qp; + char parent[PATH_MAX]; + struct filelist_info *fi; + struct media_file_info *mfi; + int ret; + + if (argc < 2 || strlen(argv[1]) == 0 + || (strncmp(argv[1], "/", 1) == 0 && strlen(argv[1]) == 1)) + { + ret = snprintf(parent, sizeof(parent), "/"); + } + else if (strncmp(argv[1], "/", 1) == 0) + { + ret = snprintf(parent, sizeof(parent), "%s/", argv[1]); + } + else + { + ret = snprintf(parent, sizeof(parent), "/%s/", argv[1]); + } + + if ((ret < 0) || (ret >= sizeof(parent))) + { + DPRINTF(E_INFO, L_MPD, "Parent path exceeds PATH_MAX\n"); + return -1; + } + + fi = (struct filelist_info*)malloc(sizeof(struct filelist_info)); + if (!fi) + { + DPRINTF(E_LOG, L_MPD, "Out of memory for fi\n"); + return ACK_ERROR_UNKNOWN; + } + + memset(&qp, 0, sizeof(struct query_params)); + + ret = db_mpd_start_query_filelist(&qp, parent); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Could not start query for path '%s'\n", argv[1]); + ret = asprintf(errmsg, "Could not start query for path '%s'\n", argv[1]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + + free_fi(fi, 0); + return ACK_ERROR_UNKNOWN; + } + + while (((ret = db_mpd_query_fetch_filelist(&qp, fi)) == 0) && (fi->virtual_path)) + { + if (fi->type == F_DIR) + { + evbuffer_add_printf(evbuf, + "directory: %s\n" + "Last-Modified: 2014-07-11T14:13:56Z\n", //TODO Send correct last modified timestamp + (fi->virtual_path + 1)); + } + else if (fi->type == F_PLAYLIST) + { + evbuffer_add_printf(evbuf, + "playlist: %s\n" + "Last-Modified: 2014-07-11T14:13:56Z\n", //TODO Send correct last modified timestamp + (fi->virtual_path + 1)); + } + else if (fi->type == F_FILE) + { + mfi = db_file_fetch_byvirtualpath(fi->virtual_path); + if (mfi) + { + ret = mpd_add_mediainfo(evbuf, mfi, -1); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Could not add mediainfo for path '%s'\n", fi->virtual_path); + } + + free_mfi(mfi, 0); + } + } + } + + db_query_end(&qp); + + if (fi) + free_fi(fi, 0); + + return 0; +} + +static int +mpd_get_query_params_search(int argc, char **argv, struct query_params *qp) +{ + char *c1; + char *c2; + int start_pos; + int end_pos; + int i; + int ret; + + c1 = NULL; + c2 = NULL; + + for (i = 0; i < argc; i += 2) + { + if (0 == strcasecmp(argv[i], "any")) + { + c1 = sqlite3_mprintf("(f.artist LIKE '%%%q%%' OR f.album LIKE '%%%q%%' OR f.title LIKE '%%%q%%')", argv[i + 1], argv[i + 1], argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "file")) + { + c1 = sqlite3_mprintf("(f.virtual_path LIKE '%%%q%%')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "base")) + { + c1 = sqlite3_mprintf("(f.virtual_path LIKE '/%q%%')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "modified-since")) + { + DPRINTF(E_WARN, L_MPD, "Special parameter 'modified-since' is not supported by forked-daapd and will be ignored\n"); + } + else if (0 == strcasecmp(argv[i], "window")) + { + ret = mpd_pars_range_arg(argv[i + 1], &start_pos, &end_pos); + if (ret == 0) + { + qp->idx_type = I_SUB; + qp->limit = end_pos - start_pos; + qp->offset = start_pos; + } + else + { + DPRINTF(E_LOG, L_MPD, "Window argument doesn't convert to integer or range: '%s'\n", argv[i + 1]); + } + } + else if (0 == strcasecmp(argv[i], "artist")) + { + c1 = sqlite3_mprintf("(f.artist LIKE '%%%q%%')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "albumartist")) + { + c1 = sqlite3_mprintf("(f.album_artist LIKE '%%%q%%')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "album")) + { + c1 = sqlite3_mprintf("(f.album LIKE '%%%q%%')", argv[i + 1]); + } + else if (0 == strcasecmp(argv[i], "title")) + { + c1 = sqlite3_mprintf("(f.title LIKE '%%%q%%')", argv[i + 1]); + } + else + { + DPRINTF(E_WARN, L_MPD, "Parameter '%s' is not supported by forked-daapd and will be ignored\n", argv[i]); + } + + if (c1) + { + if (qp->filter) + c2 = sqlite3_mprintf("%s AND %s", qp->filter, c1); + else + c2 = sqlite3_mprintf("%s", c1); + + if (qp->filter) + sqlite3_free(qp->filter); + + qp->filter = c2; + c2 = NULL; + sqlite3_free(c1); + c1 = NULL; + } + } + + return 0; +} + +/* + * Command handler function for 'search' + * Lists any song that matches the given list of arguments. Arguments are pairs of TYPE and WHAT, where + * TYPE is the tag that contains WHAT (case insensitiv). + * + * TYPE can also be one of the special parameter: + * - any: checks all tags + * - file: checks the virtual_path + * - base: restricts result to the given directory + * - modified-since (not supported) + * - window: limits result to the given range of "START:END" + * + * Example request: "search artist foo album bar" + */ +static int +mpd_command_search(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + struct query_params qp; + struct db_media_file_info dbmfi; + int ret; + + if (argc < 3 || ((argc - 1) % 2) != 0) + { + DPRINTF(E_LOG, L_MPD, "Missing argument(s) for command 'search'\n"); + ret = asprintf(errmsg, "Missing argument(s) for command 'search'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + memset(&qp, 0, sizeof(struct query_params)); + + qp.type = Q_ITEMS; + qp.sort = S_NAME; + qp.idx_type = I_NONE; + + mpd_get_query_params_search(argc - 1, argv + 1, &qp); + + ret = db_query_start(&qp); + if (ret < 0) + { + db_query_end(&qp); + + DPRINTF(E_LOG, L_MPD, "Could not start query\n"); + ret = asprintf(errmsg, "Could not start query"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_UNKNOWN; + } + + while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) + { + ret = mpd_add_db_media_file_info(evbuf, &dbmfi); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error adding song to the evbuffer, song id: %s\n", dbmfi.id); + } + } + + db_query_end(&qp); + + return 0; +} + +/* + * Command handler function for 'update' + * Initiates an init-rescan (scans for new files) + */ +static int +mpd_command_update(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int ret; + + if (argc > 1 && strlen(argv[1]) > 0) + { + DPRINTF(E_LOG, L_MPD, "Update for specific uri not supported for command 'update'\n"); + ret = asprintf(errmsg, "Update for specific uri not supported for command 'update'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + filescanner_trigger_initscan(); + + evbuffer_add(evbuf, "updating_db: 1\n", 15); + + return 0; +} + +/* +static int +mpd_command_rescan(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int ret; + + if (argc > 1) + { + DPRINTF(E_LOG, L_MPD, "Rescan for specific uri not supported for command 'rescan'\n"); + ret = asprintf(errmsg, "Rescan for specific uri not supported for command 'rescan'"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + return ACK_ERROR_ARG; + } + + filescanner_trigger_fullrescan(); + + evbuffer_add(evbuf, "updating_db: 1\n", 15); + + return 0; +} +*/ + +/* +static void +speaker_enum_cb(uint64_t id, const char *name, int relvol, struct spk_flags flags, void *arg) +{ + / * + * outputid: 0 + * outputname: My ALSA Device + * outputenabled: 0 + * OK + * / + struct evbuffer *evbuf; + + evbuf = (struct evbuffer *)arg; + + evbuffer_add_printf(evbuf, + "outputid: %" PRIi64 "\n" + "outputname: %s\n" + "outputenabled: %d\n", + id, + name, + flags.selected); +} + +static int +mpd_command_outputs(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int ret; + + player_speaker_enumerate(speaker_enum_cb, evbuf); + + return 0; +} +*/ +/* + * Dummy function to handle commands that are not supported by forked-daapd and should + * not raise an error. + */ +static int +mpd_command_ignore(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + //do nothing + DPRINTF(E_DBG, L_MPD, "Ignore command %s\n", argv[0]); + return 0; +} + +static int +mpd_command_commands(struct evbuffer *evbuf, int argc, char **argv, char **errmsg); + + +struct command +{ + /* The command name */ + const char *mpdcommand; + + /* + * The function to execute the command + * + * @param evbuf the response event buffer + * @param argc number of arguments in argv + * @param argv argument array, first entry is the commandname + * @param errmsg error message set by this function if an error occured + * @return 0 if successful, one of ack values if an error occured + */ + int (*handler)(struct evbuffer *evbuf, int argc, char **argv, char **errmsg); +}; + +static struct command mpd_handlers[] = + { + /* + * Commands for querying status + */ + { + .mpdcommand = "clearerror", + .handler = mpd_command_ignore + }, + { + .mpdcommand = "currentsong", + .handler = mpd_command_currentsong + }, + { + .mpdcommand = "idle", + .handler = mpd_command_idle + }, + { + .mpdcommand = "idle", + .handler = mpd_command_noidle + }, + { + .mpdcommand = "status", + .handler = mpd_command_status + }, + { + .mpdcommand = "stats", + .handler = mpd_command_stats + }, + + /* + * Playback options + */ + { + .mpdcommand = "consume", + .handler = mpd_command_ignore + }, + { + .mpdcommand = "crossfade", + .handler = mpd_command_ignore + }, + { + .mpdcommand = "mixrampdb", + .handler = mpd_command_ignore + }, + { + .mpdcommand = "mixrampdelay", + .handler = mpd_command_ignore + }, + { + .mpdcommand = "random", + .handler = mpd_command_random + }, + { + .mpdcommand = "repeat", + .handler = mpd_command_repeat + }, + { + .mpdcommand = "setvol", + .handler = mpd_command_setvol + }, + { + .mpdcommand = "single", + .handler = mpd_command_single + }, + { + .mpdcommand = "replay_gain_mode", + .handler = mpd_command_ignore + }, + { + .mpdcommand = "replay_gain_status", + .handler = mpd_command_replay_gain_status + }, + { + .mpdcommand = "volume", + .handler = mpd_command_volume + }, + + /* + * Controlling playback + */ + { + .mpdcommand = "next", + .handler = mpd_command_next + }, + { + .mpdcommand = "pause", + .handler = mpd_command_pause + }, + { + .mpdcommand = "play", + .handler = mpd_command_play + }, + { + .mpdcommand = "playid", + .handler = mpd_command_playid + }, + { + .mpdcommand = "previous", + .handler = mpd_command_previous + }, + { + .mpdcommand = "seek", + .handler = mpd_command_seek + }, + { + .mpdcommand = "seekid", + .handler = mpd_command_seekid + }, + { + .mpdcommand = "seekcur", + .handler = mpd_command_seekcur + }, + { + .mpdcommand = "stop", + .handler = mpd_command_stop + }, + + /* + * The current playlist + */ + { + .mpdcommand = "add", + .handler = mpd_command_add + }, + { + .mpdcommand = "addid", + .handler = mpd_command_addid + }, + { + .mpdcommand = "clear", + .handler = mpd_command_clear + }, + { + .mpdcommand = "delete", + .handler = mpd_command_delete + }, + { + .mpdcommand = "deleteid", + .handler = mpd_command_deleteid + }, + /* + { + .mpdcommand = "move", + .handler = mpd_command_move + }, + { + .mpdcommand = "moveid", + .handler = mpd_command_moveid + }, + */ + // According to the mpd protocol the use of "playlist" is deprecated + { + .mpdcommand = "playlist", + .handler = mpd_command_playlistinfo + }, + /* + { + .mpdcommand = "playlistfind", + .handler = mpd_command_playlistfind + }, + */ + { + .mpdcommand = "playlistid", + .handler = mpd_command_playlistid + }, + { + .mpdcommand = "playlistinfo", + .handler = mpd_command_playlistinfo + }, + /* + { + .mpdcommand = "playlistsearch", + .handler = mpd_command_playlistsearch + }, + { + .mpdcommand = "plchanges", + .handler = mpd_command_plchanges + }, + { + .mpdcommand = "plchangesposid", + .handler = mpd_command_plchangesposid + }, + { + .mpdcommand = "prio", + .handler = mpd_command_prio + }, + { + .mpdcommand = "prioid", + .handler = mpd_command_prioid + }, + { + .mpdcommand = "rangeid", + .handler = mpd_command_rangeid + }, + { + .mpdcommand = "shuffle", + .handler = mpd_command_shuffle + }, + { + .mpdcommand = "swap", + .handler = mpd_command_swap + }, + { + .mpdcommand = "swapid", + .handler = mpd_command_swapid + }, + { + .mpdcommand = "addtagid", + .handler = mpd_command_addtagid + }, + { + .mpdcommand = "cleartagid", + .handler = mpd_command_cleartagid + }, + */ + + /* + * Stored playlists + */ + /* + { + .mpdcommand = "listplaylist", + .handler = mpd_command_listplaylist + }, + { + .mpdcommand = "listplaylistinfo", + .handler = mpd_command_listplaylistinfo + }, + { + .mpdcommand = "listplaylists", + .handler = mpd_command_listplaylists + },load + */ + { + .mpdcommand = "load", + .handler = mpd_command_load + }, + /* + { + .mpdcommand = "playlistadd", + .handler = mpd_command_playlistadd + }, + { + .mpdcommand = "playlistclear", + .handler = mpd_command_playlistclear + }, + { + .mpdcommand = "playlistdelete", + .handler = mpd_command_playlistdelete + }, + { + .mpdcommand = "playlistmove", + .handler = mpd_command_playlistmove + }, + { + .mpdcommand = "rename", + .handler = mpd_command_rename + }, + { + .mpdcommand = "rm", + .handler = mpd_command_rm + }, + { + .mpdcommand = "save", + .handler = mpd_command_save + }, + */ + + /* + * The music database + */ + /* + { + .mpdcommand = "count", + .handler = mpd_command_count + }, + */ + { + .mpdcommand = "find", + .handler = mpd_command_find + }, + { + .mpdcommand = "findadd", + .handler = mpd_command_findadd + }, + { + .mpdcommand = "list", + .handler = mpd_command_list + }, + /* + { + .mpdcommand = "listall", + .handler = mpd_command_listall + }, + { + .mpdcommand = "listallinfo", + .handler = mpd_command_listallinfo + }, + { + .mpdcommand = "listfiles", + .handler = mpd_command_listfiles + }, + */ + { + .mpdcommand = "lsinfo", + .handler = mpd_command_lsinfo + }, + /* + { + .mpdcommand = "readcomments", + .handler = mpd_command_readcomments + }, + */ + { + .mpdcommand = "search", + .handler = mpd_command_search + }, + /* + { + .mpdcommand = "searchadd", + .handler = mpd_command_searchadd + }, + { + .mpdcommand = "searchaddpl", + .handler = mpd_command_searchaddpl + }, + */ + { + .mpdcommand = "update", + .handler = mpd_command_update + }, + /* + { + .mpdcommand = "rescan", + .handler = mpd_command_rescan + }, + */ + + /* + * Mounts and neighbors + */ + /* + { + .mpdcommand = "mount", + .handler = mpd_command_mount + }, + { + .mpdcommand = "unmount", + .handler = mpd_command_unmount + }, + { + .mpdcommand = "listmounts", + .handler = mpd_command_listmounts + }, + { + .mpdcommand = "listneighbors", + .handler = mpd_command_listneighbors + }, + */ + + /* + * Stickers + */ + /* + { + .mpdcommand = "sticker", + .handler = mpd_command_sticker + }, + */ + + /* + * Connection settings + */ + /* + { + .mpdcommand = "close", + .handler = mpd_command_close + }, + { + .mpdcommand = "kill", + .handler = mpd_command_kill + }, + { + .mpdcommand = "password", + .handler = mpd_command_password + }, + { + .mpdcommand = "ping", + .handler = mpd_command_ping + }, + */ + + /* + * Audio output devices + */ + /* + { + .mpdcommand = "disableoutput", + .handler = mpd_command_disableoutput + }, + { + .mpdcommand = "enableoutput", + .handler = mpd_command_enableoutput + }, + { + .mpdcommand = "toggleoutput", + .handler = mpd_command_toggleoutput + }, + { + .mpdcommand = "outputs", + .handler = mpd_command_outputs + }, + */ + + /* + * Reflection + */ + /* + { + .mpdcommand = "config", + .handler = mpd_command_config + }, + */ + { + .mpdcommand = "commands", + .handler = mpd_command_commands + }, + /* + { + .mpdcommand = "notcommands", + .handler = mpd_command_notcommands + }, + { + .mpdcommand = "tagtypes", + .handler = mpd_command_tagtypes + }, + { + .mpdcommand = "urlhandlers", + .handler = mpd_command_urlhandlers + }, + { + .mpdcommand = "decoders", + .handler = mpd_command_decoders + }, + */ + + /* + * Client to client + */ + /* + { + .mpdcommand = "subscribe", + .handler = mpd_command_subscribe + }, + { + .mpdcommand = "unsubscribe", + .handler = mpd_command_unsubscribe + }, + { + .mpdcommand = "channels", + .handler = mpd_command_channels + }, + { + .mpdcommand = "readmessages", + .handler = mpd_command_readmessages + }, + { + .mpdcommand = "sendmessage", + .handler = mpd_command_sendmessage + }, + */ + + /* + * NULL command to terminate loop + */ + { + .mpdcommand = NULL, + .handler = NULL + } + }; + +/* + * Finds the command handler for the given command name + * + * @param name the name of the command + * @return the command or NULL if it is an unknown/unsupported command + */ +static struct command* +mpd_find_command(const char *name) +{ + int i; + + for (i = 0; mpd_handlers[i].handler; i++) + { + if (0 == strcmp(name, mpd_handlers[i].mpdcommand)) + { + return &mpd_handlers[i]; + } + } + + return NULL; +} + +static int +mpd_command_commands(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + int i; + + for (i = 0; mpd_handlers[i].handler; i++) + { + evbuffer_add_printf(evbuf, + "command: %s\n", + mpd_handlers[i].mpdcommand); + } + + return 0; +} + + +/* + * The read callback function is invoked if a complete command sequence was received from the client + * (see mpd_input_filter function). + * + * @param bev the buffer event + * @param ctx (not used) + */ +static void +mpd_read_cb(struct bufferevent *bev, void *ctx) +{ + struct evbuffer *input; + struct evbuffer *output; + int ret; + int ncmd; + char *line; + char *errmsg; + struct command *command; + enum command_list_type listtype; + int idle_cmd; + char *argv[COMMAND_ARGV_MAX]; + int argc; + + /* Get the input evbuffer, contains the command sequence received from the client */ + input = bufferevent_get_input(bev); + /* Get the output evbuffer, used to send the server response to the client */ + output = bufferevent_get_output(bev); + + DPRINTF(E_SPAM, L_MPD, "Received MPD command sequence\n"); + + idle_cmd = 0; + + listtype = COMMAND_LIST_NONE; + ncmd = 0; + + while ((line = evbuffer_readln(input, NULL, EVBUFFER_EOL_ANY))) + { + DPRINTF(E_DBG, L_MPD, "MPD message: %s\n", line); + + // Split the read line into command name and arguments + ret = mpd_parse_args(line, &argc, argv); + if (ret != 0) + { + // Error handling for argument parsing error + DPRINTF(E_LOG, L_MPD, "Error parsing arguments for MPD message: %s\n", line); + ret = asprintf(&errmsg, "Error parsing arguments"); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + ret = ACK_ERROR_ARG; + evbuffer_add_printf(output, "ACK [%d@%d] {%s} %s\n", ret, ncmd, "unkown", errmsg); + free(errmsg); + free(line); + break; + } + + /* + * Check if it is a list command + */ + if (0 == strcmp(argv[0], "command_list_ok_begin")) + { + listtype = COMMAND_LIST_OK; + free(line); + continue; + } + else if (0 == strcmp(argv[0], "command_list_begin")) + { + listtype = COMMAND_LIST; + free(line); + continue; + } + else if (0 == strcmp(argv[0], "command_list_end")) + { + free(line); + break; + } + else if (0 == strcmp(argv[0], "idle")) + idle_cmd = 1; + else if (0 == strcmp(argv[0], "noidle")) + idle_cmd = 0; + + /* + * Find the command handler and execute the command function + */ + command = mpd_find_command(argv[0]); + + if (command == NULL) + { + ret = asprintf(&errmsg, "Unsupported command '%s'", argv[0]); + if (ret < 0) + DPRINTF(E_LOG, L_MPD, "Out of memory\n"); + ret = ACK_ERROR_UNKNOWN; + } + else + ret = command->handler(output, argc, argv, &errmsg); + + /* + * If an error occurred, add the ACK line to the response buffer and exit the loop + */ + if (ret != 0) + { + DPRINTF(E_LOG, L_MPD, "Error executing command '%s': %s\n", argv[0], errmsg); + evbuffer_add_printf(output, "ACK [%d@%d] {%s} %s\n", ret, ncmd, argv[0], errmsg); + free(errmsg); + free(line); + break; + } + + /* + * If the command sequence started with command_list_ok_begin, add a list_ok line to the + * response buffer after each command output. + */ + if (listtype == COMMAND_LIST_OK) + { + evbuffer_add(output, "list_OK\n", 8); + } + + free(line); + ncmd++; + } + + DPRINTF(E_SPAM, L_MPD, "Finished MPD command sequence: %d\n", ret); + + /* + * If everything was successful add OK line to signal clients end of message. + * If an error occured the necessary ACK line should already be added to the response buffer. + */ + if (ret == 0 && idle_cmd == 0) + { + evbuffer_add(output, "OK\n", 3); + } +} + +/* + * Callback when an event occurs on the bufferevent + */ +static void +mpd_event_cb(struct bufferevent *bev, short events, void *ctx) +{ + if (events & BEV_EVENT_ERROR) + DPRINTF(E_LOG, L_MPD, "Error from buffer event\n"); + + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) + bufferevent_free(bev); +} + +/* + * The input filter buffer callback checks if the data received from the client is a complete command sequence. + * A command sequence has end with '\n' and if it starts with "command_list_begin\n" or "command_list_ok_begin\n" + * the last line has to be "command_list_end\n". + * + * @param src evbuffer to read data from (contains the data received from the client) + * @param dst evbuffer to write data to (this is the evbuffer for the read callback) + * @param lim the upper bound of bytes to add to destination + * @param state write mode + * @param ctx (not used) + * @return BEV_OK if a complete command sequence was received otherwise BEV_NEED_MORE + */ +static enum bufferevent_filter_result +mpd_input_filter(struct evbuffer *src, struct evbuffer *dst, ev_ssize_t lim, enum bufferevent_flush_mode state, void *ctx) +{ + struct evbuffer_ptr p; + char *line; + int ret; + + while ((line = evbuffer_readln(src, NULL, EVBUFFER_EOL_ANY))) + { + ret = evbuffer_add_printf(dst, "%s\n", line); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Error adding line to buffer: '%s'\n", line); + free(line); + return BEV_ERROR; + } + free(line); + } + + if (evbuffer_get_length(src) > 0) + { + DPRINTF(E_DBG, L_MPD, "Message incomplete, waiting for more data\n"); + return BEV_NEED_MORE; + } + + p = evbuffer_search(dst, "command_list_begin", 18, NULL); + if (p.pos < 0) + { + p = evbuffer_search(dst, "command_list_ok_begin", 21, NULL); + } + + if (p.pos >= 0) + { + p = evbuffer_search(dst, "command_list_end", 16, NULL); + if (p.pos < 0) + { + DPRINTF(E_DBG, L_MPD, "Message incomplete (missing command_list_end), waiting for more data\n"); + return BEV_NEED_MORE; + } + } + + return BEV_OK; +} + +/* + * The connection listener callback function is invoked when a new connection was received. + * + * @param listener the connection listener that received the connection + * @param sock the new socket + * @param address the address from which the connection was received + * @param socklen the length of that address + * @param ctx (not used) + */ +static void +mpd_accept_conn_cb(struct evconnlistener *listener, + evutil_socket_t sock, struct sockaddr *address, int socklen, + void *ctx) +{ + /* + * For each new connection setup a new buffer event and wrap it around a filter event. + * The filter event ensures, that the read callback on the buffer event is only invoked if a complete + * command sequence from the client was received. + */ + struct event_base *base = evconnlistener_get_base(listener); + struct bufferevent *bev = bufferevent_socket_new(base, sock, BEV_OPT_CLOSE_ON_FREE); + + bev = bufferevent_filter_new(bev, mpd_input_filter, NULL, BEV_OPT_CLOSE_ON_FREE, NULL, NULL); + bufferevent_setcb(bev, mpd_read_cb, NULL, mpd_event_cb, bev); + bufferevent_enable(bev, EV_READ | EV_WRITE); + + /* + * According to the mpd protocol send "OK MPD \n" to the client, where version is the version + * of the supported mpd protocol and not the server version. + */ + evbuffer_add(bufferevent_get_output(bev), "OK MPD 0.18.0\n", 14); +} + +/* + * Error callback that gets called whenever an accept() call fails on the listener + * @param listener the connection listener that received the connection + * @param ctx (not used) + */ +static void +mpd_accept_error_cb(struct evconnlistener *listener, void *ctx) +{ + int err; + + err = EVUTIL_SOCKET_ERROR(); + DPRINTF(E_LOG, L_MPD, "Error occured %d (%s) on the listener.\n", err, evutil_socket_error_to_string(err)); +} + + +/* Thread: main */ +int mpd_init(void) +{ + struct evconnlistener *listener; + struct sockaddr_in sin; + unsigned short port; + int ret; + + port = cfg_getint(cfg_getsec(cfg, "mpd"), "port"); + if (port <= 0) + { + DPRINTF(E_INFO, L_MPD, "MPD not enabled\n"); + return 0; + } + +# if defined(__linux__) + ret = pipe2(g_exit_pipe, O_CLOEXEC); +# else + ret = pipe(g_exit_pipe); +# endif + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Could not create pipe: %s\n", strerror(errno)); + goto exit_fail; + } + + evbase_mpd = event_base_new(); + if (!evbase_mpd) + { + DPRINTF(E_LOG, L_MPD, "Could not create an event base\n"); + goto evbase_fail; + } + + g_exitev = event_new(evbase_mpd, g_exit_pipe[0], EV_READ, exit_cb, NULL); + if (!g_exitev) + { + DPRINTF(E_LOG, L_MPD, "Could not create exit event\n"); + goto evnew_fail; + } + + event_add(g_exitev, NULL); + + //TODO ipv6 + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(0); + sin.sin_port = htons(port); + + listener = evconnlistener_new_bind( + evbase_mpd, + mpd_accept_conn_cb, + NULL, + LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, + -1, + (struct sockaddr*) &sin, + sizeof(sin)); + + if (!listener) + { + DPRINTF(E_LOG, L_MPD, "Could not create connection listener for mpd clients on port %d\n", port); + + goto connew_fail; + } + evconnlistener_set_error_cb(listener, mpd_accept_error_cb); + + DPRINTF(E_INFO, L_MPD, "cache thread init\n"); + + ret = pthread_create(&tid_mpd, NULL, mpd, NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MPD, "Could not spawn cache thread: %s\n", strerror(errno)); + + goto thread_fail; + } + + return 0; + + + thread_fail: + connew_fail: + evnew_fail: + event_base_free(evbase_mpd); + evbase_mpd = NULL; + + evbase_fail: + close(g_exit_pipe[0]); + close(g_exit_pipe[1]); + + exit_fail: + return -1; +} + +/* Thread: main */ +void mpd_deinit(void) +{ + unsigned short port; + int ret; + + port = cfg_getint(cfg_getsec(cfg, "mpd"), "port"); + if (port <= 0) + { + DPRINTF(E_INFO, L_MPD, "MPD not enabled\n"); + return; + } + + thread_exit(); + + ret = pthread_join(tid_mpd, NULL); + if (ret != 0) + { + DPRINTF(E_FATAL, L_MPD, "Could not join cache thread: %s\n", strerror(errno)); + return; + } + + // Free event base (should free events too) + event_base_free(evbase_mpd); + + // Close pipes + close(g_exit_pipe[0]); + close(g_exit_pipe[1]); +} diff --git a/src/mpd.h b/src/mpd.h new file mode 100644 index 00000000..c5430a40 --- /dev/null +++ b/src/mpd.h @@ -0,0 +1,20 @@ + +#ifndef __MPD_H__ +#define __MPD_H__ + +#ifdef HAVE_LIBEVENT2 +# include +# include +# include +# include +#else +# include +#endif + +int +mpd_init(void); + +void +mpd_deinit(void); + +#endif /* !__MPD_H__ */ diff --git a/src/player.c b/src/player.c index 4750e649..f4e5ed36 100644 --- a/src/player.c +++ b/src/player.c @@ -97,6 +97,37 @@ struct spk_enum void *arg; }; +enum range_type + { + RANGEARG_NONE, + RANGEARG_ID, + RANGEARG_POS, + RANGEARG_RANGE + }; + +/* + * Identifies an item or a range of items + * + * Depending on item_range.type the item(s) are identified by: + * - item id (type = RANGEARG_ID) given in item_range.id + * - item position (type = RANGEARG_POS) given in item_range.start_pos + * - start and end position (type = RANGEARG_RANGE) given in item_range.start_pos to item_range.end_pos + * + * The pointer id_ptr may be set to an item id by the called function. + */ +struct item_range +{ + enum range_type type; + + uint32_t id; + int start_pos; + int end_pos; + + char shuffle; + + uint32_t *id_ptr; +}; + struct player_command { pthread_mutex_t lck; @@ -121,11 +152,14 @@ struct player_command uint32_t id; int intval; int ps_pos[2]; + struct item_range item_range; } arg; int ret; int raop_pending; + + struct player_queue *queue; }; /* Keep in sync with enum raop_devtype */ @@ -637,6 +671,7 @@ player_queue_make(struct query_params *qp, const char *sort) struct player_source *q_tail; struct player_source *ps; uint32_t id; + uint32_t song_length; int ret; qp->idx_type = I_NONE; @@ -673,6 +708,14 @@ player_queue_make(struct query_params *qp, const char *sort) continue; } + ret = safe_atou32(dbmfi.song_length, &song_length); + if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Invalid song id in query result!\n"); + + continue; + } + ps = (struct player_source *)malloc(sizeof(struct player_source)); if (!ps) { @@ -685,6 +728,7 @@ player_queue_make(struct query_params *qp, const char *sort) memset(ps, 0, sizeof(struct player_source)); ps->id = id; + ps->len_ms = song_length; if (!q_head) q_head = ps; @@ -979,6 +1023,38 @@ player_queue_make_pl(int plid, uint32_t *id) return ps; } +struct player_source * +player_queue_make_mpd(char *path, int recursive) +{ + struct query_params qp; + struct player_source *ps; + int ret; + + memset(&qp, 0, sizeof(struct query_params)); + + qp.type = Q_ITEMS; + qp.idx_type = I_NONE; + qp.sort = S_ALBUM; + + if (recursive) + { + ret = asprintf(&(qp.filter), "f.virtual_path LIKE '/%s%%'", path); + if (ret < 0) + DPRINTF(E_DBG, L_PLAYER, "Out of memory\n"); + } + else + { + ret = asprintf(&(qp.filter), "f.virtual_path LIKE '/%s'", path); + if (ret < 0) + DPRINTF(E_DBG, L_PLAYER, "Out of memory\n"); + } + + ps = player_queue_make(&qp, NULL); + + free(qp.filter); + return ps; +} + static void source_free(struct player_source *ps) { @@ -1414,19 +1490,46 @@ source_prev(void) return 0; } +/* + * Returns the position of the given song (ps) in the playqueue or shufflequeue. + * First song in the queue has position 0. Depending on the 'shuffle' argument, + * the position is either determined in the playqueue or shufflequeue. + * + * @param ps the song to search in the queue + * @param shuffle 0 search in the playqueue, 1 search in the shufflequeue + * @return position 0-based in the queue + */ static int -source_position(struct player_source *ps) +source_position(struct player_source *ps, char shuffle) { struct player_source *p; int ret; ret = 0; - for (p = source_head; p != ps; p = p->pl_next) + for (p = (shuffle ? shuffle_head : source_head); p != ps; p = (shuffle ? p->shuffle_next : p->pl_next)) ret++; return ret; } +static uint32_t +source_count() +{ + struct player_source *ps; + uint32_t ret; + + ret = 0; + + if (source_head) + { + ret++; + for (ps = source_head->pl_next; ps != source_head; ps = ps->pl_next) + ret++; + } + + return ret; +} + static uint64_t source_check(void) { @@ -2235,6 +2338,14 @@ playback_abort(void) metadata_purge(); } +static struct player_source * +next_ps(struct player_source *ps, char shuffle) +{ + if (shuffle) + return ps->shuffle_next; + else + return ps->pl_next; +} /* Actual commands, executed in the player thread */ static int @@ -2248,6 +2359,8 @@ get_status(struct player_command *cmd) status = cmd->arg.status; + memset(status, 0, sizeof(struct player_status)); + status->shuffle = shuffle; status->repeat = repeat; @@ -2271,8 +2384,10 @@ get_status(struct player_command *cmd) pos = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - cur_streaming->stream_start; status->pos_ms = (pos * 1000) / 44100; + status->len_ms = cur_streaming->len_ms; + + status->pos_pl = source_position(cur_streaming, 0); - status->pos_pl = source_position(cur_streaming); break; case PLAY_PLAYING: @@ -2308,9 +2423,16 @@ get_status(struct player_command *cmd) } status->pos_ms = (pos * 1000) / 44100; + status->len_ms = ps->len_ms; status->id = ps->id; - status->pos_pl = source_position(ps); + status->pos_pl = source_position(ps, 0); + + ps = next_ps(ps, shuffle); + status->next_id = ps->id; + status->next_pos_pl = source_position(ps, 0); + + status->playlistlength = source_count(); break; } @@ -2440,11 +2562,45 @@ playback_start_bh(struct player_command *cmd) return -1; } +static struct player_source * +queue_get_source_byid(uint32_t id) +{ + struct player_source *ps; + + if (!source_head) + return NULL; + + ps = source_head->pl_next; + while (ps->id != id && ps != source_head) + { + ps = ps->pl_next; + } + + return ps; +} + +static struct player_source * +queue_get_source_bypos(int pos) +{ + struct player_source *ps; + int i; + + if (!source_head) + return NULL; + + ps = source_head; + for (i = pos; i > 0; i--) + ps = ps->pl_next; + + return ps; +} + static int playback_start(struct player_command *cmd) { struct raop_device *rd; uint32_t *idx_id; + struct player_source *ps; int ret; if (!source_head) @@ -2454,10 +2610,15 @@ playback_start(struct player_command *cmd) return -1; } - idx_id = cmd->arg.id_ptr; + idx_id = cmd->arg.item_range.id_ptr; if (player_state == PLAY_PLAYING) { + /* + * If player is already playing a song, only return current playing song id + * and do not change player state (ignores given arguments for playing a + * specified song by pos or id). + */ if (idx_id) { if (cur_playing) @@ -2471,10 +2632,33 @@ playback_start(struct player_command *cmd) return 0; } + // Update global playback position pb_pos = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - 88200; - if (idx_id) + /* + * If either an item id or an item position is given, get the corresponding + * player_source from the queue. + */ + if (cmd->arg.item_range.type == RANGEARG_ID) + ps = queue_get_source_byid(cmd->arg.item_range.id); + else if (cmd->arg.item_range.type == RANGEARG_POS) + ps = queue_get_source_bypos(cmd->arg.item_range.start_pos); + else + ps = NULL; + + /* + * Update queue and cur_streaming depending on + * - given player_source to start playing + * - player state + */ + if (ps) { + /* + * A song is specified in the arguments (by id or pos) and the corresponding + * player_source (ps) from the queue was found. + * + * Stop playback (if it was paused) and prepare to start playback on ps. + */ if (cur_playing) source_stop(cur_playing); else if (cur_streaming) @@ -2484,38 +2668,33 @@ playback_start(struct player_command *cmd) cur_streaming = NULL; if (shuffle) - { - source_reshuffle(); - cur_streaming = shuffle_head; - } - else - cur_streaming = source_head; + source_reshuffle(); - if (*idx_id > 0) - { - cur_streaming = source_head; - for (; *idx_id > 0; (*idx_id)--) - cur_streaming = cur_streaming->pl_next; + cur_streaming = ps; - if (shuffle) - shuffle_head = cur_streaming; - } + if (shuffle) + shuffle_head = cur_streaming; ret = source_open(cur_streaming, 0); if (ret < 0) { - DPRINTF(E_LOG, L_PLAYER, "Couldn't jump to queue position %d\n", *idx_id); + DPRINTF(E_LOG, L_PLAYER, "Couldn't jump to source %d in queue\n", cur_streaming->id); playback_abort(); return -1; } - *idx_id = cur_streaming->id; + if (idx_id) + *idx_id = cur_streaming->id; + cur_streaming->stream_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES; cur_streaming->output_start = cur_streaming->stream_start; } else if (!cur_streaming) { + /* + * Player was stopped, start playing the queue + */ if (shuffle) source_reshuffle(); @@ -2533,7 +2712,10 @@ playback_start(struct player_command *cmd) } else { - /* After a pause, the source is still open so source_open() doesn't get + /* + * Player was paused, resume playing cur_streaming + * + * After a pause, the source is still open so source_open() doesn't get * called and we have to handle metadata ourselves. */ metadata_send(cur_streaming, 1); @@ -3362,6 +3544,102 @@ shuffle_set(struct player_command *cmd) return 0; } +static unsigned int +queue_count() +{ + struct player_source *ps; + int count; + + if (!source_head) + return 0; + + count = 1; + ps = source_head->pl_next; + while (ps != source_head) + { + count++; + ps = ps->pl_next; + } + + return count; +} + +static int +queue_get(struct player_command *cmd) +{ + int start_pos; + int end_pos; + struct player_queue *queue; + uint32_t *ids; + unsigned int qlength; + unsigned int count; + struct player_source *ps; + int i; + int pos; + char qshuffle; + + queue = malloc(sizeof(struct player_queue)); + + qlength = queue_count(); + qshuffle = cmd->arg.item_range.shuffle; + + start_pos = cmd->arg.item_range.start_pos; + if (start_pos < 0) + { + // Set start_pos to the position of the current item + 1 + ps = cur_playing ? cur_playing : cur_streaming; + start_pos = ps ? source_position(ps, qshuffle) + 1 : 0; + } + + end_pos = cmd->arg.item_range.end_pos; + if (cmd->arg.item_range.start_pos < 0) + end_pos += start_pos; + if (end_pos <= 0 || end_pos > qlength) + end_pos = qlength; + + if (end_pos > start_pos) + count = end_pos - start_pos; + else + count = 0; + + ids = malloc(count * sizeof(uint32_t)); + + pos = 0; + ps = qshuffle ? shuffle_head : source_head; + for (i = 0; i < end_pos; i++) + { + if (i >= start_pos) + { + ids[pos] = ps->id; + pos++; + } + + ps = qshuffle ? ps->shuffle_next : ps->pl_next; + } + + queue->start_pos = start_pos; + queue->count = count; + queue->queue = ids; + + queue->length = qlength; + queue->playingid = 0; + if (cur_playing) + queue->playingid = cur_playing->id; + else if (cur_streaming) + queue->playingid = cur_streaming->id; + + cmd->queue = queue; + + return 0; +} + +void +queue_free(struct player_queue *queue) +{ + free(queue->queue); + free(queue); +} + static int queue_add(struct player_command *cmd) { @@ -3522,30 +3800,55 @@ static int queue_remove(struct player_command *cmd) { struct player_source *ps; - int pos; + uint32_t pos; + uint32_t id; int i; - pos = cmd->arg.ps_pos[0]; - - DPRINTF(E_DBG, L_PLAYER, "Removing song from position %d\n", pos); - - if (pos < 1) - { - DPRINTF(E_LOG, L_PLAYER, "Can't remove song, invalid position %d\n", pos); - return -1; - } - ps = cur_playing ? cur_playing : cur_streaming; if (!ps) { - DPRINTF(E_LOG, L_PLAYER, "Current playing/streaming song not found\n"); + DPRINTF(E_LOG, L_PLAYER, "Current playing/streaming item not found\n"); return -1; } - for (i = 0; i < pos; i++) - { - ps = shuffle ? ps->shuffle_next : ps->pl_next; - } + if (cmd->arg.item_range.type == RANGEARG_ID) + { + id = cmd->arg.item_range.id; + DPRINTF(E_DBG, L_PLAYER, "Removing item with id %d\n", id); + + if (id < 1) + { + DPRINTF(E_LOG, L_PLAYER, "Can't remove item, invalid id %d\n", id); + return -1; + } + else if (id == ps->id) + { + DPRINTF(E_LOG, L_PLAYER, "Can't remove current playing item, id %d\n", id); + return -1; + } + + ps = source_head->pl_next; + while (ps->id != id && ps != source_head) + { + ps = ps->pl_next; + } + } + else + { + pos = cmd->arg.item_range.start_pos; + DPRINTF(E_DBG, L_PLAYER, "Removing item from position %d\n", pos); + + if (pos < 1) + { + DPRINTF(E_LOG, L_PLAYER, "Can't remove item, invalid position %d\n", pos); + return -1; + } + + for (i = 0; i < pos; i++) + { + ps = shuffle ? ps->shuffle_next : ps->pl_next; + } + } ps->shuffle_prev->shuffle_next = ps->shuffle_next; ps->shuffle_next->shuffle_prev = ps->shuffle_prev; @@ -3806,8 +4109,21 @@ player_now_playing(uint32_t *id) return ret; } +/* + * Starts/resumes playback + * + * Depending on the player state, this will either resumes playing the current item (player is paused) + * or begins playing the queue from the beginning. + * + * If shuffle is set, the queue is reshuffled prior to starting playback. + * + * If a pointer is given as argument "itemid", its value will be set to the playing item id. + * + * @param *itemid if not NULL, will be set to the playing item id + * @return 0 if successful, -1 if an error occurred + */ int -player_playback_start(uint32_t *idx_id) +player_playback_start(uint32_t *itemid) { struct player_command cmd; int ret; @@ -3816,7 +4132,8 @@ player_playback_start(uint32_t *idx_id) cmd.func = playback_start; cmd.func_bh = playback_start_bh; - cmd.arg.id_ptr = idx_id; + cmd.arg.item_range.type = RANGEARG_NONE; + cmd.arg.item_range.id_ptr = itemid; ret = sync_command(&cmd); @@ -3825,6 +4142,66 @@ player_playback_start(uint32_t *idx_id) return ret; } +/* + * Starts playback at item number "pos" of the current queue + * + * If shuffle is set, the queue is reshuffled prior to starting playback. + * + * If a pointer is given as argument "itemid", its value will be set to the playing item id. + * + * @param *itemid if not NULL, will be set to the playing item id + * @return 0 if successful, -1 if an error occurred + */ +int +player_playback_startpos(int pos, uint32_t *itemid) +{ + struct player_command cmd; + int ret; + + command_init(&cmd); + + cmd.func = playback_start; + cmd.func_bh = playback_start_bh; + cmd.arg.item_range.type = RANGEARG_POS; + cmd.arg.item_range.start_pos = pos; + cmd.arg.item_range.id_ptr = itemid; + ret = sync_command(&cmd); + + command_deinit(&cmd); + + return ret; +} + +/* + * Starts playback at item with "id" of the current queue + * + * If shuffle is set, the queue is reshuffled prior to starting playback. + * + * If a pointer is given as argument "itemid", its value will be set to the playing item id. + * + * @param *itemid if not NULL, will be set to the playing item id + * @return 0 if successful, -1 if an error occurred + */ +int +player_playback_startid(uint32_t id, uint32_t *itemid) +{ + struct player_command cmd; + int ret; + + command_init(&cmd); + + cmd.func = playback_start; + cmd.func_bh = playback_start_bh; + cmd.arg.item_range.type = RANGEARG_ID; + cmd.arg.item_range.id = id; + cmd.arg.item_range.id_ptr = itemid; + ret = sync_command(&cmd); + + command_deinit(&cmd); + + return ret; +} + int player_playback_stop(void) { @@ -4055,13 +4432,44 @@ player_shuffle_set(int enable) return ret; } -struct player_source * -player_queue_get(void) +/* + * Retrieves a list of item ids in the queue from postion 'start_pos' to 'end_pos' + * + * If start_pos is -1, the list starts with the item next from the current playing item. + * If end_pos is -1, this list contains all songs starting from 'start_pos' + * + * The 'shuffle' argument determines if the items are taken from the playqueue (shuffle = 0) + * or the shufflequeue (shuffle = 1). + * + * @param start_pos Start the listing from 'start_pos' + * @param end_pos End the listing at 'end_pos' + * @param shuffle If set to 1 use the shuffle queue, otherwise the playqueue + * @return List of items (ids) in the queue + */ +struct player_queue * +player_queue_get(int start_pos, int end_pos, char shuffle) { - if (shuffle) - return shuffle_head; - else - return source_head; + struct player_command cmd; + int ret; + + command_init(&cmd); + + cmd.func = queue_get; + cmd.func_bh = NULL; + cmd.arg.item_range.type = RANGEARG_POS; + cmd.arg.item_range.start_pos = start_pos; + cmd.arg.item_range.end_pos = end_pos; + cmd.arg.item_range.shuffle = shuffle; + cmd.queue = NULL; + + ret = sync_command(&cmd); + + command_deinit(&cmd); + + if (ret != 0) + return NULL; + + return cmd.queue; } int @@ -4122,7 +4530,8 @@ player_queue_move(int ps_pos_from, int ps_pos_to) return ret; } -int player_queue_remove(int ps_pos_remove) +int +player_queue_remove(int ps_pos_remove) { struct player_command cmd; int ret; @@ -4131,7 +4540,28 @@ int player_queue_remove(int ps_pos_remove) cmd.func = queue_remove; cmd.func_bh = NULL; - cmd.arg.ps_pos[0] = ps_pos_remove; + cmd.arg.item_range.type = RANGEARG_POS; + cmd.arg.item_range.start_pos = ps_pos_remove; + + ret = sync_command(&cmd); + + command_deinit(&cmd); + + return ret; +} + +int +player_queue_removeid(uint32_t id) +{ + struct player_command cmd; + int ret; + + command_init(&cmd); + + cmd.func = queue_remove; + cmd.func_bh = NULL; + cmd.arg.item_range.type = RANGEARG_ID; + cmd.arg.item_range.id = id; ret = sync_command(&cmd); diff --git a/src/player.h b/src/player.h index dd680540..93ade6d1 100644 --- a/src/player.h +++ b/src/player.h @@ -51,10 +51,22 @@ struct player_status { int volume; + /* Playlist id */ uint32_t plid; + /* Playlist length */ + uint32_t playlistlength; + /* Playing song id*/ uint32_t id; + /* Elapsed time in ms of playing item */ uint32_t pos_ms; + /* Length in ms of playing item */ + uint32_t len_ms; + /* Playlist position of playing item*/ int pos_pl; + /* Item id of next item in playlist */ + uint32_t next_id; + /* Playlist position of next item */ + int next_pos_pl; }; typedef void (*spk_enum_cb)(uint64_t id, const char *name, int relvol, struct spk_flags flags, void *arg); @@ -63,6 +75,7 @@ typedef void (*player_status_handler)(void); struct player_source { uint32_t id; + uint32_t len_ms; enum source_type type; int setup_done; @@ -82,6 +95,21 @@ struct player_source struct player_source *play_next; }; +struct player_queue +{ + // The item id of the current playing item + uint32_t playingid; + // The number of items in the queue + unsigned int length; + + // The position in the queue for the first item in the queue array + unsigned int start_pos; + // The number of items in the queue array + unsigned int count; + // The queue array (array of item ids) + uint32_t *queue; +}; + struct player_history { /* Buffer index of the oldest remembered song */ @@ -113,6 +141,12 @@ player_speaker_set(uint64_t *ids); int player_playback_start(uint32_t *idx_id); +int +player_playback_startpos(int pos, uint32_t *itemid); + +int +player_playback_startid(uint32_t id, uint32_t *itemid); + int player_playback_stop(void); @@ -151,7 +185,13 @@ struct player_source * player_queue_make_pl(int plid, uint32_t *id); struct player_source * -player_queue_get(void); +player_queue_make_mpd(char *path, int recursive); + +struct player_queue * +player_queue_get(int start_pos, int end_pos, char shuffle); + +void +queue_free(struct player_queue *queue); int player_queue_add(struct player_source *ps); @@ -165,6 +205,9 @@ player_queue_move(int ps_pos_from, int ps_pos_to); int player_queue_remove(int ps_pos_remove); +int +player_queue_removeid(uint32_t id); + void player_queue_clear(void); diff --git a/src/spotify.c b/src/spotify.c index dc82c75d..1b9e92f8 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -561,6 +561,7 @@ spotify_playlist_save(sp_playlist *pl) char title[512]; int plid; int num_tracks; + char virtual_path[PATH_MAX]; int ret; int i; @@ -594,6 +595,8 @@ spotify_playlist_save(sp_playlist *pl) pli = db_pl_fetch_bypath(url); snprintf(title, sizeof(title), "[s] %s", name); + snprintf(virtual_path, PATH_MAX, "/spotify:/%s", title); + if (pli) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist found ('%s', link %s), updating\n", name, url); @@ -602,7 +605,7 @@ spotify_playlist_save(sp_playlist *pl) free_pli(pli, 0); - ret = db_pl_update(title, url, plid); + ret = db_pl_update(title, url, virtual_path, plid); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error updating playlist ('%s', link %s)\n", name, url); @@ -616,7 +619,7 @@ spotify_playlist_save(sp_playlist *pl) { DPRINTF(E_DBG, L_SPOTIFY, "Adding playlist ('%s', link %s)\n", name, url); - ret = db_pl_add(title, url, &plid); + ret = db_pl_add(title, url, virtual_path, &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);