From ff4b388d26fd0218502de4b7a347b7af7cbe4bd7 Mon Sep 17 00:00:00 2001 From: chme Date: Mon, 1 May 2017 13:28:46 +0200 Subject: [PATCH 1/6] [db] Use db_get_one_int in db_pl_id_bypath --- src/db.c | 53 ++++++++++++----------------------------------------- src/db.h | 7 +++++-- 2 files changed, 17 insertions(+), 43 deletions(-) diff --git a/src/db.c b/src/db.c index 2ea77abc..8b901482 100644 --- a/src/db.c +++ b/src/db.c @@ -827,7 +827,10 @@ db_get_one_int(const char *query) ret = db_blocking_step(stmt); if (ret != SQLITE_ROW) { - DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); + if (ret == SQLITE_DONE) + DPRINTF(E_INFO, L_DB, "No matching row found for query: %s\n", query); + else + DPRINTF(E_LOG, L_DB, "Could not step: %s (%s)\n", sqlite3_errmsg(hdl), query); sqlite3_finalize(stmt); return -1; @@ -2265,7 +2268,7 @@ db_file_fetch_byid(int id) } struct media_file_info * -db_file_fetch_byvirtualpath(char *virtual_path) +db_file_fetch_byvirtualpath(const char *virtual_path) { #define Q_TMPL "SELECT f.* FROM files f WHERE f.virtual_path = %Q;" struct media_file_info *mfi; @@ -2649,12 +2652,11 @@ db_pl_ping_bymatch(char *path, int isdir) #undef Q_TMPL_NODIR } -static int -db_pl_id_bypath(char *path, int *id) +int +db_pl_id_bypath(const char *path) { #define Q_TMPL "SELECT p.id FROM playlists p WHERE p.path = '%q';" char *query; - sqlite3_stmt *stmt; int ret; query = sqlite3_mprintf(Q_TMPL, path); @@ -2665,41 +2667,11 @@ db_pl_id_bypath(char *path, int *id) return -1; } - DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); + ret = db_get_one_int(query); - ret = db_blocking_prepare_v2(query, -1, &stmt, NULL); - if (ret != SQLITE_OK) - { - DPRINTF(E_LOG, L_DB, "Could not prepare statement: %s\n", sqlite3_errmsg(hdl)); - - sqlite3_free(query); - return -1; - } - - ret = db_blocking_step(stmt); - if (ret != SQLITE_ROW) - { - if (ret == SQLITE_DONE) - DPRINTF(E_DBG, L_DB, "No results\n"); - else - DPRINTF(E_LOG, L_DB, "Could not step: %s\n", sqlite3_errmsg(hdl)); - - sqlite3_finalize(stmt); - sqlite3_free(query); - return -1; - } - - *id = sqlite3_column_int(stmt, 0); - -#ifdef DB_PROFILE - while (db_blocking_step(stmt) == SQLITE_ROW) - ; /* EMPTY */ -#endif - - sqlite3_finalize(stmt); sqlite3_free(query); - return 0; + return ret; #undef Q_TMPL } @@ -2855,7 +2827,7 @@ db_pl_fetch_bypath(const char *path) } struct playlist_info * -db_pl_fetch_byvirtualpath(char *virtual_path) +db_pl_fetch_byvirtualpath(const char *virtual_path) { #define Q_TMPL "SELECT p.* FROM playlists p WHERE p.virtual_path = '%q';" struct playlist_info *pli; @@ -3072,10 +3044,9 @@ void db_pl_delete_bypath(char *path) { int id; - int ret; - ret = db_pl_id_bypath(path, &id); - if (ret < 0) + id = db_pl_id_bypath(path); + if (id < 0) return; db_pl_delete(id); diff --git a/src/db.h b/src/db.h index 0d7510d1..a59f6649 100644 --- a/src/db.h +++ b/src/db.h @@ -536,7 +536,7 @@ struct media_file_info * db_file_fetch_byid(int id); struct media_file_info * -db_file_fetch_byvirtualpath(char *path); +db_file_fetch_byvirtualpath(const char *path); int db_file_add(struct media_file_info *mfi); @@ -572,11 +572,14 @@ db_pl_ping(int id); void db_pl_ping_bymatch(char *path, int isdir); +int +db_pl_id_bypath(const char *path); + struct playlist_info * db_pl_fetch_bypath(const char *path); struct playlist_info * -db_pl_fetch_byvirtualpath(char *virtual_path); +db_pl_fetch_byvirtualpath(const char *virtual_path); struct playlist_info * db_pl_fetch_bytitlepath(char *title, char *path); From f84e87913c36e115a1892433b580dcf422c64f8b Mon Sep 17 00:00:00 2001 From: chme Date: Wed, 9 Aug 2017 18:19:20 +0200 Subject: [PATCH 2/6] [library] Allow save/add/delete of playlists if source supports it --- src/library.c | 134 +++++++++++++++++++++++++++++++++++++++++++++++++ src/library.h | 24 +++++++++ src/listener.h | 2 + 3 files changed, 160 insertions(+) diff --git a/src/library.c b/src/library.c index e1dc1fb9..1d2660d0 100644 --- a/src/library.c +++ b/src/library.c @@ -47,6 +47,12 @@ #include "listener.h" #include "player.h" +struct playlist_add_param +{ + const char *vp_playlist; + const char *vp_item; +}; + static struct commands_base *cmdbase; static pthread_t tid_library; @@ -725,6 +731,134 @@ library_update_trigger(void) commands_exec_async(cmdbase, update_trigger, NULL); } +static enum command_state +playlist_add(void *arg, int *retval) +{ + struct playlist_add_param *param = arg; + int i; + int ret = LIBRARY_ERROR; + + DPRINTF(E_DBG, L_LIB, "Adding item '%s' to playlist '%s'\n", param->vp_item, param->vp_playlist); + + for (i = 0; sources[i]; i++) + { + if (sources[i]->disabled || !sources[i]->playlist_add) + { + DPRINTF(E_DBG, L_LIB, "Library source '%s' is disabled or does not support playlist_add\n", sources[i]->name); + continue; + } + + ret = sources[i]->playlist_add(param->vp_playlist, param->vp_item); + + if (ret == LIBRARY_OK) + { + DPRINTF(E_DBG, L_LIB, "Adding item '%s' to playlist '%s' with library source '%s'\n", param->vp_item, param->vp_playlist, sources[i]->name); + listener_notify(LISTENER_STORED_PLAYLIST); + break; + } + } + + *retval = ret; + return COMMAND_END; +} + +int +library_playlist_add(const char *vp_playlist, const char *vp_item) +{ + struct playlist_add_param param; + + if (library_is_scanning()) + return -1; + + param.vp_playlist = vp_playlist; + param.vp_item = vp_item; + return commands_exec_sync(cmdbase, playlist_add, NULL, ¶m); +} + +static enum command_state +playlist_remove(void *arg, int *retval) +{ + const char *virtual_path; + int i; + int ret = LIBRARY_ERROR; + + virtual_path = arg; + + DPRINTF(E_DBG, L_LIB, "Removing playlist at path '%s'\n", virtual_path); + + for (i = 0; sources[i]; i++) + { + if (sources[i]->disabled || !sources[i]->playlist_remove) + { + DPRINTF(E_DBG, L_LIB, "Library source '%s' is disabled or does not support playlist_remove\n", sources[i]->name); + continue; + } + + ret = sources[i]->playlist_remove(virtual_path); + + if (ret == LIBRARY_OK) + { + DPRINTF(E_DBG, L_LIB, "Removing playlist '%s' with library source '%s'\n", virtual_path, sources[i]->name); + listener_notify(LISTENER_STORED_PLAYLIST); + break; + } + } + + *retval = ret; + return COMMAND_END; +} + +int +library_playlist_remove(char *virtual_path) +{ + if (library_is_scanning()) + return -1; + + return commands_exec_sync(cmdbase, playlist_remove, NULL, virtual_path); +} + +static enum command_state +queue_save(void *arg, int *retval) +{ + const char *virtual_path; + int i; + int ret = LIBRARY_ERROR; + + virtual_path = arg; + + DPRINTF(E_DBG, L_LIB, "Saving queue to path '%s'\n", virtual_path); + + for (i = 0; sources[i]; i++) + { + if (sources[i]->disabled || !sources[i]->queue_save) + { + DPRINTF(E_DBG, L_LIB, "Library source '%s' is disabled or does not support queue_save\n", sources[i]->name); + continue; + } + + ret = sources[i]->queue_save(virtual_path); + + if (ret == LIBRARY_OK) + { + DPRINTF(E_DBG, L_LIB, "Saving queue to path '%s' with library source '%s'\n", virtual_path, sources[i]->name); + listener_notify(LISTENER_STORED_PLAYLIST); + break; + } + } + + *retval = ret; + return COMMAND_END; +} + +int +library_queue_save(char *path) +{ + if (library_is_scanning()) + return -1; + + return commands_exec_sync(cmdbase, queue_save, NULL, path); +} + /* * Execute the function 'func' with the given argument 'arg' in the library thread. * diff --git a/src/library.h b/src/library.h index a72c5957..c4586e07 100644 --- a/src/library.h +++ b/src/library.h @@ -70,6 +70,21 @@ struct library_source * Scans metadata for the media file with the given path into the given mfi */ int (*scan_metadata)(const char *path, struct media_file_info *mfi); + + /* + * Save queue as a new playlist under the given virtual path + */ + int (*playlist_add)(const char *vp_playlist, const char *vp_item); + + /* + * Removes the playlist under the given virtual path + */ + int (*playlist_remove)(const char *virtual_path); + + /* + * Save queue as a new playlist under the given virtual path + */ + int (*queue_save)(const char *virtual_path); }; @@ -103,6 +118,15 @@ library_is_exiting(); void library_update_trigger(void); +int +library_playlist_add(const char *vp_playlist, const char *vp_item); + +int +library_playlist_remove(char *virtual_path); + +int +library_queue_save(char *path); + int library_exec_async(command_function func, void *arg); diff --git a/src/listener.h b/src/listener.h index 61421077..8a557fa9 100644 --- a/src/listener.h +++ b/src/listener.h @@ -16,6 +16,8 @@ enum listener_event_type LISTENER_OPTIONS = (1 << 4), /* The library has been modified */ LISTENER_DATABASE = (1 << 5), + /* A stored playlist has been modified (create, delete, add, rename) */ + LISTENER_STORED_PLAYLIST = (1 << 6), }; typedef void (*notify)(enum listener_event_type type); From d10c3672c6523c61f896e18c21b435ab35c2da39 Mon Sep 17 00:00:00 2001 From: chme Date: Wed, 9 Aug 2017 18:21:00 +0200 Subject: [PATCH 3/6] [filescanner] Add support to save/add/delete local stored playlists Allows creating/modifying of playlists in one of the configured library directories. --- src/library/filescanner.c | 336 +++++++++++++++++++++++++++++ src/library/filescanner.h | 3 + src/library/filescanner_playlist.c | 15 +- 3 files changed, 341 insertions(+), 13 deletions(-) diff --git a/src/library/filescanner.c b/src/library/filescanner.c index 858b798d..5d40389f 100644 --- a/src/library/filescanner.c +++ b/src/library/filescanner.c @@ -177,6 +177,20 @@ filename_from_path(const char *path) return filename; } +char * +strip_extension(const char *path) +{ + char *ptr; + char *result; + + result = strdup(path); + ptr = strrchr(result, '.'); + if (ptr) + *ptr = '\0'; + + return result; +} + static int push_dir(struct stacked_dir **s, char *path, int parent_id) { @@ -1601,6 +1615,325 @@ scan_metadata(const char *path, struct media_file_info *mfi) return LIBRARY_PATH_INVALID; } +static const char * +virtual_path_to_path(const char *virtual_path) +{ + if (strstr(virtual_path, "/../")) + return NULL; + + if (strncmp(virtual_path, "/file:", strlen("/file:")) == 0) + return virtual_path + strlen("/file:"); + + if (strncmp(virtual_path, "file:", strlen("file:")) == 0) + return virtual_path + strlen("file:"); + + return NULL; +} + +static bool +check_path_in_directories(const char *path) +{ + cfg_t *lib; + int ndirs; + int i; + const char *dir_path; + + lib = cfg_getsec(cfg, "library"); + ndirs = cfg_size(lib, "directories"); + for (i = 0; i < ndirs; i++) + { + dir_path = cfg_getnstr(lib, "directories", i); + if (strncmp(path, dir_path, strlen(dir_path)) == 0) + return true; + } + + return false; +} + +/* + * Checks if the given virtual path for a playlist is a valid path for an m3u playlist file in one + * of the configured library directories and translates it to real path. + * + * Returns NULL on error and a new allocated path on success. + */ +static char * +get_playlist_path(const char *vp_playlist) +{ + const char *path; + char *pl_path; + struct playlist_info *pli; + + path = virtual_path_to_path(vp_playlist); + if (!path) + { + DPRINTF(E_LOG, L_SCAN, "Unsupported virtual path '%s'\n", vp_playlist); + return NULL; + } + + if (!check_path_in_directories(path)) + { + DPRINTF(E_LOG, L_SCAN, "Path '%s' is not a virtual path for a configured (local) library directory.\n", vp_playlist); + return NULL; + } + + pli = db_pl_fetch_byvirtualpath(vp_playlist); + if (pli && pli->type != PL_PLAIN) + { + DPRINTF(E_LOG, L_SCAN, "Playlist with virtual path '%s' already exists and is not a plain playlist.\n", vp_playlist); + free_pli(pli, 0); + return NULL; + } + free_pli(pli, 0); + + pl_path = safe_asprintf("%s.m3u", path); + + return pl_path; +} + +static int +get_playlist_id(const char *pl_path, const char *vp_playlist) +{ + const char *filename; + char *title; + int dir_id; + int pl_id; + + pl_id = db_pl_id_bypath(pl_path); + if (pl_id < 0) + { + dir_id = get_parent_dir_id(pl_path); + filename = filename_from_path(pl_path); + title = strip_extension(filename); + pl_id = library_add_playlist_info(pl_path, title, vp_playlist, PL_PLAIN, 0, dir_id); + free(title); + } + + return pl_id; +} + +static int +playlist_add_path(FILE *fp, int pl_id, const char *path) +{ + int ret; + + ret = fprintf(fp, "%s\n", path); + if (ret >= 0) + { + ret = db_pl_add_item_bypath(pl_id, path); + } + + if (ret < 0) + { + DPRINTF(E_LOG, L_SCAN, "Failed to add path '%s' to playlist (id = %d)\n", path, pl_id); + return -1; + } + + return 0; +} + +static int +playlist_add_files(FILE *fp, int pl_id, const char *virtual_path) +{ + struct query_params qp; + struct db_media_file_info dbmfi; + uint32_t data_kind; + int ret; + + memset(&qp, 0, sizeof(struct query_params)); + qp.type = Q_ITEMS; + qp.sort = S_ARTIST; + qp.idx_type = I_NONE; + qp.filter = sqlite3_mprintf("(f.virtual_path = %Q OR f.virtual_path LIKE '%q/%%')", virtual_path, virtual_path); + + ret = db_query_start(&qp); + if (ret < 0) + { + db_query_end(&qp); + return -1; + } + + while (((ret = db_query_fetch_file(&qp, &dbmfi)) == 0) && (dbmfi.id)) + { + if ((safe_atou32(dbmfi.data_kind, &data_kind) < 0) + || (data_kind == DATA_KIND_PIPE)) + { + DPRINTF(E_WARN, L_SCAN, "Item '%s' not added to playlist (id = %d), unsupported data kind\n", dbmfi.path, pl_id); + continue; + } + + ret = playlist_add_path(fp, pl_id, dbmfi.path); + if (ret < 0) + break; + + DPRINTF(E_DBG, L_SCAN, "Item '%s' added to playlist (id = %d)\n", dbmfi.path, pl_id); + } + + db_query_end(&qp); + + return ret; +} + +static int +playlist_add(const char *vp_playlist, const char *vp_item) +{ + char *pl_path; + FILE *fp; + int pl_id; + int ret; + + pl_path = get_playlist_path(vp_playlist); + if (!pl_path) + return LIBRARY_PATH_INVALID; + + fp = fopen(pl_path, "a"); + if (!fp) + { + DPRINTF(E_LOG, L_SCAN, "Error opening file '%s' for writing: %d\n", pl_path, errno); + free(pl_path); + return LIBRARY_ERROR; + } + + pl_id = get_playlist_id(pl_path, vp_playlist); + free(pl_path); + if (pl_id < 0) + { + DPRINTF(E_LOG, L_SCAN, "Could not get playlist id for %s\n", vp_playlist); + fclose(fp); + return LIBRARY_ERROR; + } + + ret = playlist_add_files(fp, pl_id, vp_item); + fclose(fp); + + if (ret < 0) + { + DPRINTF(E_LOG, L_SCAN, "Could not add %s to playlist\n", vp_item); + return LIBRARY_ERROR; + } + + db_pl_ping(pl_id); + + return LIBRARY_OK; +} + +static int +playlist_remove(const char *vp_playlist) +{ + const char *path; + char *pl_path; + struct playlist_info *pli; + int pl_id; + int ret; + + path = virtual_path_to_path(vp_playlist); + if (!path) + { + DPRINTF(E_LOG, L_SCAN, "Unsupported virtual path '%s'\n", vp_playlist); + return LIBRARY_PATH_INVALID; + } + + if (!check_path_in_directories(path)) + { + DPRINTF(E_LOG, L_SCAN, "Path '%s' is not a virtual path for a configured (local) library directory.\n", vp_playlist); + return LIBRARY_ERROR; + } + + pli = db_pl_fetch_byvirtualpath(vp_playlist); + if (!pli || pli->type != PL_PLAIN) + { + DPRINTF(E_LOG, L_SCAN, "Playlist with virtual path '%s' does not exist or is not a plain playlist.\n", vp_playlist); + free_pli(pli, 0); + return LIBRARY_ERROR; + } + pl_id = pli->id; + free_pli(pli, 0); + + pl_path = safe_asprintf("%s.m3u", path); + ret = unlink(pl_path); + free(pl_path); + if (ret < 0) + { + DPRINTF(E_LOG, L_SCAN, "Could not remove playlist \"%s\": %d\n", vp_playlist, errno); + return LIBRARY_ERROR; + } + + db_pl_delete(pl_id); + return LIBRARY_OK; +} + +static int +queue_save(const char *virtual_path) +{ + char *pl_path; + FILE *fp; + struct query_params query_params; + struct db_queue_item queue_item; + int pl_id; + int ret; + + pl_path = get_playlist_path(virtual_path); + if (!pl_path) + return LIBRARY_PATH_INVALID; + + fp = fopen(pl_path, "a"); + if (!fp) + { + DPRINTF(E_LOG, L_SCAN, "Error opening file '%s' for writing: %d\n", pl_path, errno); + free(pl_path); + return LIBRARY_ERROR; + } + + pl_id = get_playlist_id(pl_path, virtual_path); + free(pl_path); + if (pl_id < 0) + { + DPRINTF(E_LOG, L_SCAN, "Could not get playlist id for %s\n", virtual_path); + fclose(fp); + return LIBRARY_ERROR; + } + + memset(&query_params, 0, sizeof(struct query_params)); + ret = db_queue_enum_start(&query_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_SCAN, "Failed to start queue enum\n"); + fclose(fp); + return LIBRARY_ERROR; + } + + while ((ret = db_queue_enum_fetch(&query_params, &queue_item)) == 0 && queue_item.id > 0) + { + if (queue_item.data_kind == DATA_KIND_PIPE) + { + DPRINTF(E_LOG, L_SCAN, "Unsupported data kind for playlist file '%s' ignoring item '%s'\n", virtual_path, queue_item.path); + continue; + } + + ret = fprintf(fp, "%s\n", queue_item.path); + if (ret < 0) + { + DPRINTF(E_LOG, L_SCAN, "Failed to write path '%s' to file '%s'\n", queue_item.path, virtual_path); + break; + } + + ret = db_pl_add_item_bypath(pl_id, queue_item.path); + if (ret < 0) + DPRINTF(E_WARN, L_SCAN, "Could not add %s to playlist\n", queue_item.path); + else + DPRINTF(E_DBG, L_SCAN, "Item '%s' added to playlist (id = %d)\n", queue_item.path, pl_id); + } + + db_queue_enum_end(&query_params); + fclose(fp); + + db_pl_ping(pl_id); + + if (ret < 0) + return LIBRARY_ERROR; + + return LIBRARY_OK; +} + /* Thread: main */ static int filescanner_init(void) @@ -1634,4 +1967,7 @@ struct library_source filescanner = .rescan = filescanner_rescan, .fullrescan = filescanner_fullrescan, .scan_metadata = scan_metadata, + .playlist_add = playlist_add, + .playlist_remove = playlist_remove, + .queue_save = queue_save, }; diff --git a/src/library/filescanner.h b/src/library/filescanner.h index 8279f15d..a39d6374 100644 --- a/src/library/filescanner.h +++ b/src/library/filescanner.h @@ -23,4 +23,7 @@ scan_itunes_itml(char *file); const char * filename_from_path(const char *path); +char * +strip_extension(const char *path); + #endif /* !__FILESCANNER_H__ */ diff --git a/src/library/filescanner_playlist.c b/src/library/filescanner_playlist.c index 7e51c04a..76f10ff8 100644 --- a/src/library/filescanner_playlist.c +++ b/src/library/filescanner_playlist.c @@ -256,22 +256,11 @@ scan_playlist(char *file, time_t mtime, int dir_id) pli->type = PL_PLAIN; /* Get only the basename, to be used as the playlist title */ - ptr = strrchr(filename, '.'); - if (ptr) - *ptr = '\0'; - - pli->title = strdup(filename); - - /* Restore the full filename */ - if (ptr) - *ptr = '.'; + pli->title = strip_extension(filename); pli->path = strdup(file); snprintf(virtual_path, PATH_MAX, "/file:%s", file); - ptr = strrchr(virtual_path, '.'); - if (ptr) - *ptr = '\0'; - pli->virtual_path = strdup(virtual_path); + pli->virtual_path = strip_extension(virtual_path); pli->directory_id = dir_id; From dc3f9443e73fc6e8750e6626322efa0a62cda8f0 Mon Sep 17 00:00:00 2001 From: chme Date: Wed, 9 Aug 2017 18:21:46 +0200 Subject: [PATCH 4/6] [mpd/conf] Add support for stored playlist commands (add/save/delete) This also introduces a new config option to define a default playlist folder. The default playlist folder is used by forked-daapd if the playlist is not a full virtual path (simpifies creation of playlists with mpd clients). --- forked-daapd.conf.in | 5 ++ src/conffile.c | 1 + src/mpd.c | 147 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 151 insertions(+), 2 deletions(-) diff --git a/forked-daapd.conf.in b/forked-daapd.conf.in index 6346d623..1a9963c6 100644 --- a/forked-daapd.conf.in +++ b/forked-daapd.conf.in @@ -264,6 +264,11 @@ mpd { # the playlist like MPD does. Note that some dacp clients do not show # the playqueue if playback is stopped. # clear_queue_on_stop_disable = false + + # A directory in one of the library directories that will be used as the default + # playlist directory. forked-dapd creates new playlists in this directory if only + # a playlist name is provided by the mpd client. +# default_playlist_directory = "" } # SQLite configuration (allows to modify the operation of the SQLite databases) diff --git a/src/conffile.c b/src/conffile.c index 7cc88723..b1d8c799 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -156,6 +156,7 @@ static cfg_opt_t sec_mpd[] = CFG_INT("port", 6600, CFGF_NONE), CFG_INT("http_port", 0, CFGF_NONE), CFG_BOOL("clear_queue_on_stop_disable", cfg_false, CFGF_NONE), + CFG_STR("default_playlist_directory", NULL, CFGF_NONE), CFG_END() }; diff --git a/src/mpd.c b/src/mpd.c index e9dee23a..bd641328 100644 --- a/src/mpd.c +++ b/src/mpd.c @@ -70,6 +70,8 @@ static struct evhttp *evhttpd; struct evconnlistener *listener; +// Virtual path to the default playlist directory +static char *default_pl_dir; #define COMMAND_ARGV_MAX 37 @@ -185,6 +187,26 @@ struct idle_client struct idle_client *idle_clients; +/* + * Creates a new string for the given path that starts with a '/'. + * If 'path' already starts with a '/' the returned string is a duplicate + * of 'path'. + * + * The returned string needs to be freed by the caller. + */ +static char * +prepend_slash(const char *path) +{ + char *result; + + if (path[0] == '/') + result = strdup(path); + else + result = safe_asprintf("/%s", path); + + return result; +} + /* Thread: mpd */ static void * @@ -604,6 +626,10 @@ mpd_command_idle(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) { client->events |= LISTENER_OPTIONS; } + else if (0 == strcmp(argv[i], "stored_playlist")) + { + client->events |= LISTENER_STORED_PLAYLIST; + } else { DPRINTF(E_DBG, L_MPD, "Idle command for '%s' not supported\n", argv[i]); @@ -2172,6 +2198,112 @@ mpd_command_load(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) return 0; } +static int +mpd_command_playlistadd(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + char *vp_playlist; + char *vp_item; + int ret; + + if (argc < 3) + { + *errmsg = safe_asprintf("Missing argument for command 'playlistadd'"); + return ACK_ERROR_ARG; + } + + if (!default_pl_dir || strstr(argv[1], ":/")) + { + // Argument is a virtual path, make sure it starts with a '/' + vp_playlist = prepend_slash(argv[1]); + } + else + { + // Argument is a playlist name, prepend default playlist directory + vp_playlist = safe_asprintf("%s/%s", default_pl_dir, argv[1]); + } + + vp_item = prepend_slash(argv[2]); + + ret = library_playlist_add(vp_playlist, vp_item); + free(vp_playlist); + free(vp_item); + if (ret < 0) + { + *errmsg = safe_asprintf("Error saving queue to file '%s'", argv[1]); + return ACK_ERROR_ARG; + } + + return 0; +} + +static int +mpd_command_rm(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + char *virtual_path; + int ret; + + if (argc < 2) + { + *errmsg = safe_asprintf("Missing argument for command 'rm'"); + return ACK_ERROR_ARG; + } + + if (!default_pl_dir || strstr(argv[1], ":/")) + { + // Argument is a virtual path, make sure it starts with a '/' + virtual_path = prepend_slash(argv[1]); + } + else + { + // Argument is a playlist name, prepend default playlist directory + virtual_path = safe_asprintf("%s/%s", default_pl_dir, argv[1]); + } + + ret = library_playlist_remove(virtual_path); + free(virtual_path); + if (ret < 0) + { + *errmsg = safe_asprintf("Error removing playlist '%s'", argv[1]); + return ACK_ERROR_ARG; + } + + return 0; +} + +static int +mpd_command_save(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) +{ + char *virtual_path; + int ret; + + if (argc < 2) + { + *errmsg = safe_asprintf("Missing argument for command 'save'"); + return ACK_ERROR_ARG; + } + + if (!default_pl_dir || strstr(argv[1], ":/")) + { + // Argument is a virtual path, make sure it starts with a '/' + virtual_path = prepend_slash(argv[1]); + } + else + { + // Argument is a playlist name, prepend default playlist directory + virtual_path = safe_asprintf("%s/%s", default_pl_dir, argv[1]); + } + + ret = library_queue_save(virtual_path); + free(virtual_path); + if (ret < 0) + { + *errmsg = safe_asprintf("Error saving queue to file '%s'", argv[1]); + return ACK_ERROR_ARG; + } + + return 0; +} + static int mpd_get_query_params_find(int argc, char **argv, struct query_params *qp) { @@ -3944,11 +4076,11 @@ static struct mpd_command mpd_handlers[] = .mpdcommand = "load", .handler = mpd_command_load }, - /* { .mpdcommand = "playlistadd", .handler = mpd_command_playlistadd }, + /* { .mpdcommand = "playlistclear", .handler = mpd_command_playlistclear @@ -3965,6 +4097,7 @@ static struct mpd_command mpd_handlers[] = .mpdcommand = "rename", .handler = mpd_command_rename }, + */ { .mpdcommand = "rm", .handler = mpd_command_rm @@ -3973,7 +4106,6 @@ static struct mpd_command mpd_handlers[] = .mpdcommand = "save", .handler = mpd_command_save }, - */ /* * The music database @@ -4512,6 +4644,10 @@ mpd_notify_idle_client(struct idle_client *client, enum listener_event_type type evbuffer_add(client->evbuffer, "changed: options\n", 17); break; + case LISTENER_STORED_PLAYLIST: + evbuffer_add(client->evbuffer, "changed: stored_playlist\n", 25); + break; + default: DPRINTF(E_WARN, L_MPD, "Unsupported event type (%d) in notify idle clients.\n", type); return -1; @@ -4706,6 +4842,7 @@ int mpd_init(void) unsigned short http_port; const char *http_addr; int v6enabled; + const char *pl_dir; int ret; @@ -4806,6 +4943,10 @@ int mpd_init(void) } } + pl_dir = cfg_getstr(cfg_getsec(cfg, "mpd"), "default_playlist_directory"); + if (pl_dir) + default_pl_dir = safe_asprintf("/file:%s", pl_dir); + DPRINTF(E_INFO, L_MPD, "mpd thread init\n"); ret = pthread_create(&tid_mpd, NULL, mpd, NULL); @@ -4884,4 +5025,6 @@ void mpd_deinit(void) // Free event base (should free events too) event_base_free(evbase_mpd); + + free(default_pl_dir); } From 8f49a6fb453d9cb27675a36bde3d375f59f1335a Mon Sep 17 00:00:00 2001 From: chme Date: Sat, 12 Aug 2017 08:28:05 +0200 Subject: [PATCH 5/6] [mpd/conf] Add config option to enable modifying stored playlists --- forked-daapd.conf.in | 7 ++++++- src/conffile.c | 1 + src/mpd.c | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/forked-daapd.conf.in b/forked-daapd.conf.in index 1a9963c6..b8a5e0fd 100644 --- a/forked-daapd.conf.in +++ b/forked-daapd.conf.in @@ -265,9 +265,14 @@ mpd { # the playqueue if playback is stopped. # clear_queue_on_stop_disable = false + # Allows creating, deleting and modifying m3u playlists in the library directories. + # Defaults to being disabled. +# allow_modifying_stored_playlists = false + # A directory in one of the library directories that will be used as the default # playlist directory. forked-dapd creates new playlists in this directory if only - # a playlist name is provided by the mpd client. + # a playlist name is provided by the mpd client (requires "allow_modify_stored_playlists" + # set to true). # default_playlist_directory = "" } diff --git a/src/conffile.c b/src/conffile.c index b1d8c799..06649d29 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -156,6 +156,7 @@ static cfg_opt_t sec_mpd[] = CFG_INT("port", 6600, CFGF_NONE), CFG_INT("http_port", 0, CFGF_NONE), CFG_BOOL("clear_queue_on_stop_disable", cfg_false, CFGF_NONE), + CFG_BOOL("allow_modifying_stored_playlists", cfg_false, CFGF_NONE), CFG_STR("default_playlist_directory", NULL, CFGF_NONE), CFG_END() }; diff --git a/src/mpd.c b/src/mpd.c index bd641328..f20b2e41 100644 --- a/src/mpd.c +++ b/src/mpd.c @@ -72,6 +72,7 @@ struct evconnlistener *listener; // Virtual path to the default playlist directory static char *default_pl_dir; +static bool allow_modifying_stored_playlists; #define COMMAND_ARGV_MAX 37 @@ -2205,6 +2206,12 @@ mpd_command_playlistadd(struct evbuffer *evbuf, int argc, char **argv, char **er char *vp_item; int ret; + if (!allow_modifying_stored_playlists) + { + *errmsg = safe_asprintf("Modifying stored playlists is not enabled"); + return ACK_ERROR_PERMISSION; + } + if (argc < 3) { *errmsg = safe_asprintf("Missing argument for command 'playlistadd'"); @@ -2242,6 +2249,12 @@ mpd_command_rm(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) char *virtual_path; int ret; + if (!allow_modifying_stored_playlists) + { + *errmsg = safe_asprintf("Modifying stored playlists is not enabled"); + return ACK_ERROR_PERMISSION; + } + if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'rm'"); @@ -2276,6 +2289,12 @@ mpd_command_save(struct evbuffer *evbuf, int argc, char **argv, char **errmsg) char *virtual_path; int ret; + if (!allow_modifying_stored_playlists) + { + *errmsg = safe_asprintf("Modifying stored playlists is not enabled"); + return ACK_ERROR_PERMISSION; + } + if (argc < 2) { *errmsg = safe_asprintf("Missing argument for command 'save'"); @@ -4943,6 +4962,7 @@ int mpd_init(void) } } + allow_modifying_stored_playlists = cfg_getbool(cfg_getsec(cfg, "mpd"), "allow_modifying_stored_playlists"); pl_dir = cfg_getstr(cfg_getsec(cfg, "mpd"), "default_playlist_directory"); if (pl_dir) default_pl_dir = safe_asprintf("/file:%s", pl_dir); From d47729480994f476ec0f89464fc3ab384be87867 Mon Sep 17 00:00:00 2001 From: chme Date: Sat, 12 Aug 2017 10:22:18 +0200 Subject: [PATCH 6/6] [filescanner] Guard against modifying files outside the library and guard against creating m3u playlists with the same virtual path like pls playlists --- src/library/filescanner.c | 65 ++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/library/filescanner.c b/src/library/filescanner.c index 5d40389f..800d7050 100644 --- a/src/library/filescanner.c +++ b/src/library/filescanner.c @@ -1618,9 +1618,6 @@ scan_metadata(const char *path, struct media_file_info *mfi) static const char * virtual_path_to_path(const char *virtual_path) { - if (strstr(virtual_path, "/../")) - return NULL; - if (strncmp(virtual_path, "/file:", strlen("/file:")) == 0) return virtual_path + strlen("/file:"); @@ -1636,18 +1633,43 @@ check_path_in_directories(const char *path) cfg_t *lib; int ndirs; int i; - const char *dir_path; + char *tmp_path; + char *dir; + const char *lib_dir; + bool ret; + if (strstr(path, "/../")) + return NULL; + + tmp_path = strdup(path); + dir = dirname(tmp_path); + if (!dir) + { + free(tmp_path); + return false; + } + + ret = false; lib = cfg_getsec(cfg, "library"); ndirs = cfg_size(lib, "directories"); for (i = 0; i < ndirs; i++) { - dir_path = cfg_getnstr(lib, "directories", i); - if (strncmp(path, dir_path, strlen(dir_path)) == 0) - return true; + lib_dir = cfg_getnstr(lib, "directories", i); + if (strncmp(dir, lib_dir, strlen(lib_dir)) == 0) + { + ret = true; + break; + } } - return false; + free(tmp_path); + return ret; +} + +static bool +has_suffix(const char *file, const char *suffix) +{ + return (strlen(file) > 4 && !strcmp(file + strlen(file) - 4, suffix)); } /* @@ -1670,23 +1692,25 @@ get_playlist_path(const char *vp_playlist) return NULL; } - if (!check_path_in_directories(path)) + pl_path = safe_asprintf("%s.m3u", path); + + if (!check_path_in_directories(pl_path)) { - DPRINTF(E_LOG, L_SCAN, "Path '%s' is not a virtual path for a configured (local) library directory.\n", vp_playlist); + DPRINTF(E_LOG, L_SCAN, "Path '%s' is not a virtual path for a configured (local) library directory.\n", pl_path); + free(pl_path); return NULL; } pli = db_pl_fetch_byvirtualpath(vp_playlist); - if (pli && pli->type != PL_PLAIN) + if (pli && (pli->type != PL_PLAIN || !has_suffix(pli->path, ".m3u"))) { - DPRINTF(E_LOG, L_SCAN, "Playlist with virtual path '%s' already exists and is not a plain playlist.\n", vp_playlist); + DPRINTF(E_LOG, L_SCAN, "Playlist with virtual path '%s' already exists and is not a m3u playlist.\n", vp_playlist); free_pli(pli, 0); + free(pl_path); return NULL; } free_pli(pli, 0); - pl_path = safe_asprintf("%s.m3u", path); - return pl_path; } @@ -1819,36 +1843,29 @@ playlist_add(const char *vp_playlist, const char *vp_item) static int playlist_remove(const char *vp_playlist) { - const char *path; char *pl_path; struct playlist_info *pli; int pl_id; int ret; - path = virtual_path_to_path(vp_playlist); - if (!path) + pl_path = get_playlist_path(vp_playlist); + if (!pl_path) { DPRINTF(E_LOG, L_SCAN, "Unsupported virtual path '%s'\n", vp_playlist); return LIBRARY_PATH_INVALID; } - if (!check_path_in_directories(path)) - { - DPRINTF(E_LOG, L_SCAN, "Path '%s' is not a virtual path for a configured (local) library directory.\n", vp_playlist); - return LIBRARY_ERROR; - } - pli = db_pl_fetch_byvirtualpath(vp_playlist); if (!pli || pli->type != PL_PLAIN) { DPRINTF(E_LOG, L_SCAN, "Playlist with virtual path '%s' does not exist or is not a plain playlist.\n", vp_playlist); free_pli(pli, 0); + free(pl_path); return LIBRARY_ERROR; } pl_id = pli->id; free_pli(pli, 0); - pl_path = safe_asprintf("%s.m3u", path); ret = unlink(pl_path); free(pl_path); if (ret < 0)