Merge pull request #384 from chme/storedplaylist3

[mpd] Initial support for stored playlists
This commit is contained in:
ejurgensen 2017-08-12 21:49:08 +02:00 committed by GitHub
commit 05b15d9679
11 changed files with 712 additions and 58 deletions

View File

@ -264,6 +264,16 @@ 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
# 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 (requires "allow_modify_stored_playlists"
# set to true).
# default_playlist_directory = ""
}
# SQLite configuration (allows to modify the operation of the SQLite databases)

View File

@ -156,6 +156,8 @@ 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()
};

View File

@ -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);

View File

@ -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);

View File

@ -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, &param);
}
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.
*

View File

@ -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);

View File

@ -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,342 @@ 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 (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;
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++)
{
lib_dir = cfg_getnstr(lib, "directories", i);
if (strncmp(dir, lib_dir, strlen(lib_dir)) == 0)
{
ret = true;
break;
}
}
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));
}
/*
* 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;
}
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", pl_path);
free(pl_path);
return NULL;
}
pli = db_pl_fetch_byvirtualpath(vp_playlist);
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 m3u playlist.\n", vp_playlist);
free_pli(pli, 0);
free(pl_path);
return NULL;
}
free_pli(pli, 0);
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)
{
char *pl_path;
struct playlist_info *pli;
int pl_id;
int ret;
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;
}
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);
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 +1984,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,
};

View File

@ -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__ */

View File

@ -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;

View File

@ -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);

167
src/mpd.c
View File

@ -70,6 +70,9 @@ static struct evhttp *evhttpd;
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
@ -185,6 +188,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 +627,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 +2199,130 @@ 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 (!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'");
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 (!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'");
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 (!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'");
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 +4095,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 +4116,7 @@ static struct mpd_command mpd_handlers[] =
.mpdcommand = "rename",
.handler = mpd_command_rename
},
*/
{
.mpdcommand = "rm",
.handler = mpd_command_rm
@ -3973,7 +4125,6 @@ static struct mpd_command mpd_handlers[] =
.mpdcommand = "save",
.handler = mpd_command_save
},
*/
/*
* The music database
@ -4512,6 +4663,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 +4861,7 @@ int mpd_init(void)
unsigned short http_port;
const char *http_addr;
int v6enabled;
const char *pl_dir;
int ret;
@ -4806,6 +4962,11 @@ 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);
DPRINTF(E_INFO, L_MPD, "mpd thread init\n");
ret = pthread_create(&tid_mpd, NULL, mpd, NULL);
@ -4884,4 +5045,6 @@ void mpd_deinit(void)
// Free event base (should free events too)
event_base_free(evbase_mpd);
free(default_pl_dir);
}