mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-28 08:05:56 -05:00
Merge pull request #384 from chme/storedplaylist3
[mpd] Initial support for stored playlists
This commit is contained in:
commit
05b15d9679
@ -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)
|
||||
|
@ -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()
|
||||
};
|
||||
|
||||
|
53
src/db.c
53
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);
|
||||
|
7
src/db.h
7
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);
|
||||
|
134
src/library.c
134
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.
|
||||
*
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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__ */
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
167
src/mpd.c
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user