#include <unistd.h>
#include <pcre.h>
#include <ctype.h>
+#include <langinfo.h>
#include "configuration.h"
#include "syscalls.h"
#include "vector.h"
#include "version.h"
#include "dateparse.h"
+#include "inputline.h"
static disorder_client *client;
}
static void cf_quack(char attribute((unused)) **argv) {
- xprintf("\n"
- " .------------------.\n"
- " | Naath is a babe! |\n"
- " `---------+--------'\n"
- " \\\n"
- " >0\n"
- " (<)'\n"
- "~~~~~~~~~~~~~~~~~~~~~~\n"
- "\n");
+ if(!strcasecmp(nl_langinfo(CODESET), "utf-8")) {
+#define TL "\xE2\x95\xAD"
+#define TR "\xE2\x95\xAE"
+#define BR "\xE2\x95\xAF"
+#define BL "\xE2\x95\xB0"
+#define H "\xE2\x94\x80"
+#define V "\xE2\x94\x82"
+#define T "\xE2\x94\xAC"
+ xprintf("\n"
+ " "TL H H H H H H H H H H H H H H H H H H TR"\n"
+ " "V" Naath is a babe! "V"\n"
+ " "BL H H H H H H H H H T H H H H H H H H BR"\n"
+ " \\\n"
+ " >0\n"
+ " (<)'\n"
+ "~~~~~~~~~~~~~~~~~~~~~~\n"
+ "\n");
+ } else {
+ xprintf("\n"
+ " .------------------.\n"
+ " | Naath is a babe! |\n"
+ " `---------+--------'\n"
+ " \\\n"
+ " >0\n"
+ " (<)'\n"
+ "~~~~~~~~~~~~~~~~~~~~~~\n"
+ "\n");
+ }
}
static void cf_somelist(char **argv,
exit(EXIT_FAILURE);
}
+static void cf_playlists(char attribute((unused)) **argv) {
+ char **vec;
+
+ if(disorder_playlists(getclient(), &vec, 0))
+ exit(EXIT_FAILURE);
+ while(*vec)
+ xprintf("%s\n", nullcheck(utf82mb(*vec++)));
+}
+
+static void cf_playlist_del(char **argv) {
+ if(disorder_playlist_delete(getclient(), argv[0]))
+ exit(EXIT_FAILURE);
+}
+
+static void cf_playlist_get(char **argv) {
+ char **vec;
+
+ if(disorder_playlist_get(getclient(), argv[0], &vec, 0))
+ exit(EXIT_FAILURE);
+ while(*vec)
+ xprintf("%s\n", nullcheck(utf82mb(*vec++)));
+}
+
+static void cf_playlist_set(char **argv) {
+ struct vector v[1];
+ FILE *input;
+ const char *tag;
+ char *l;
+
+ if(argv[1]) {
+ // Read track list from file
+ if(!(input = fopen(argv[1], "r")))
+ fatal(errno, "opening %s", argv[1]);
+ tag = argv[1];
+ } else {
+ // Read track list from standard input
+ input = stdin;
+ tag = "stdin";
+ }
+ vector_init(v);
+ while(!inputline(tag, input, &l, '\n')) {
+ if(!strcmp(l, "."))
+ break;
+ vector_append(v, l);
+ }
+ if(ferror(input))
+ fatal(errno, "reading %s", tag);
+ if(input != stdin)
+ fclose(input);
+ if(disorder_playlist_lock(getclient(), argv[0])
+ || disorder_playlist_set(getclient(), argv[0], v->vec, v->nvec)
+ || disorder_playlist_unlock(getclient()))
+ exit(EXIT_FAILURE);
+}
+
static const struct command {
const char *name;
int min, max;
"Add TRACKS to the end of the queue" },
{ "playing", 0, 0, cf_playing, 0, "",
"Report the playing track" },
+ { "playlist-del", 1, 1, cf_playlist_del, 0, "PLAYLIST",
+ "Delete a playlist" },
+ { "playlist-get", 1, 1, cf_playlist_get, 0, "PLAYLIST",
+ "Get the contents of a playlist" },
+ { "playlist-set", 1, 2, cf_playlist_set, isarg_filename, "PLAYLIST [PATH]",
+ "Set the contents of a playlist" },
+ { "playlists", 0, 0, cf_playlists, 0, "",
+ "List playlists" },
{ "prefs", 1, 1, cf_prefs, 0, "TRACK",
"Display all the preferences for TRACK" },
{ "quack", 0, 0, cf_quack, 0, 0, 0 },
choose.c choose-menu.c choose-search.c popup.c misc.c \
control.c properties.c menu.c log.c progress.c login.c rtp.c \
help.c ../lib/memgc.c settings.c users.c lookup.c choose.h \
- popup.h
+ popup.h playlists.c
disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT) \
$(LIBASOUND) $(COREAUDIO) $(LIBDB)
disobedience_LDFLAGS=$(GTK_LIBS)
/* Update everything to be sure that the connection to the server hasn't
* mysteriously gone stale on us. */
all_update();
+ event_raise("periodic-slow", 0);
/* Recheck RTP status too */
check_rtp_address(0, 0, 0);
return TRUE; /* don't remove me */
recheck_rights = 0;
if(recheck_rights)
check_rights();
+ event_raise("periodic-fast", 0);
return TRUE;
}
disorder_eclient_version(client, version_completed, 0);
event_register("log-connected", check_rtp_address, 0);
suppress_actions = 0;
+ playlists_init();
/* If no password is set yet pop up a login box */
if(!config->password)
login_box();
void set_tool_colors(GtkWidget *w);
void popup_settings(void);
+/* Playlists */
+
+void playlists_init(void);
+void edit_playlists(gpointer callback_data,
+ guint callback_action,
+ GtkWidget *menu_item);
+extern char **playlists;
+extern int nplaylists;
+extern GtkWidget *playlists_widget;
+extern GtkWidget *playlists_menu;
+extern GtkWidget *editplaylists_widget;
+
#endif /* DISOBEDIENCE_H */
/*
static void log_rescanned(void *v);
static void log_rights_changed(void *v, rights_type r);
static void log_adopted(void *v, const char *id, const char *user);
+static void log_playlist_created(void *v,
+ const char *playlist, const char *sharing);
+static void log_playlist_modified(void *v,
+ const char *playlist, const char *sharing);
+static void log_playlist_deleted(void *v,
+ const char *playlist);
/** @brief Callbacks for server state monitoring */
const disorder_eclient_log_callbacks log_callbacks = {
.volume = log_volume,
.rescanned = log_rescanned,
.rights_changed = log_rights_changed,
- .adopted = log_adopted
+ .adopted = log_adopted,
+ .playlist_created = log_playlist_created,
+ .playlist_modified = log_playlist_modified,
+ .playlist_deleted = log_playlist_deleted,
};
/** @brief Update everything */
event_raise("queue-changed", 0);
}
+static void log_playlist_created(void attribute((unused)) *v,
+ const char *playlist,
+ const char attribute((unused)) *sharing) {
+ event_raise("playlist-created", (void *)playlist);
+}
+
+static void log_playlist_modified(void attribute((unused)) *v,
+ const char *playlist,
+ const char attribute((unused)) *sharing) {
+ event_raise("playlist-modified", (void *)playlist);
+}
+
+static void log_playlist_deleted(void attribute((unused)) *v,
+ const char *playlist) {
+ event_raise("playlist-deleted", (void *)playlist);
+}
+
/*
Local Variables:
c-basic-offset:2
static GtkWidget *selectall_widget;
static GtkWidget *selectnone_widget;
static GtkWidget *properties_widget;
+GtkWidget *playlists_widget;
+GtkWidget *playlists_menu;
+GtkWidget *editplaylists_widget;
/** @brief Main menu widgets */
GtkItemFactory *mainmenufactory;
&& t->selectnone_sensitive(t->extra));
}
}
-
+
/** @brief Fetch version in order to display the about... popup */
static void about_popup(gpointer attribute((unused)) callback_data,
guint attribute((unused)) callback_action,
0, /* item_type */
0 /* extra_data */
},
+ {
+ (char *)"/Edit/Edit playlists", /* path */
+ 0, /* accelerator */
+ edit_playlists, /* callback */
+ 0, /* callback_action */
+ 0, /* item_type */
+ 0 /* extra_data */
+ },
+
{
(char *)"/Control", /* path */
(char *)"<CheckItem>", /* item_type */
0 /* extra_data */
},
+ {
+ (char *)"/Control/Activate playlist", /* path */
+ 0, /* accelerator */
+ 0, /* callback */
+ 0, /* callback_action */
+ (char *)"<Branch>", /* item_type */
+ 0 /* extra_data */
+ },
{
(char *)"/Help", /* path */
"<GdisorderMain>/Edit/Deselect all tracks");
properties_widget = gtk_item_factory_get_widget(mainmenufactory,
"<GdisorderMain>/Edit/Track properties");
+ playlists_widget = gtk_item_factory_get_item(mainmenufactory,
+ "<GdisorderMain>/Control/Activate playlist");
+ playlists_menu = gtk_item_factory_get_widget(mainmenufactory,
+ "<GdisorderMain>/Control/Activate playlist");
+ editplaylists_widget = gtk_item_factory_get_widget(mainmenufactory,
+ "<GdisorderMain>/Edit/Edit playlists");
assert(selectall_widget != 0);
assert(selectnone_widget != 0);
assert(properties_widget != 0);
+ assert(playlists_widget != 0);
+ assert(playlists_menu != 0);
+ assert(editplaylists_widget != 0);
-
GtkWidget *edit_widget = gtk_item_factory_get_widget(mainmenufactory,
"<GdisorderMain>/Edit");
g_signal_connect(edit_widget, "show", G_CALLBACK(edit_menu_show), 0);
-
+
event_register("rights-changed", menu_rights_changed, 0);
users_set_sensitive(0);
m = gtk_item_factory_get_widget(mainmenufactory,
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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
+ */
+/** @file disobedience/playlists.c
+ * @brief Playlist for Disobedience
+ *
+ */
+#include "disobedience.h"
+
+static void playlists_updated(void *v,
+ const char *err,
+ int nvec, char **vec);
+
+/** @brief Current list of playlists or NULL */
+char **playlists;
+
+/** @brief Count of playlists */
+int nplaylists;
+
+/** @brief Schedule an update to the list of playlists */
+static void playlists_update(const char attribute((unused)) *event,
+ void attribute((unused)) *eventdata,
+ void attribute((unused)) *callbackdata) {
+ disorder_eclient_playlists(client, playlists_updated, 0);
+}
+
+/** @brief qsort() callback for playlist name comparison */
+static int playlistcmp(const void *ap, const void *bp) {
+ const char *a = *(char **)ap, *b = *(char **)bp;
+ const char *ad = strchr(a, '.'), *bd = strchr(b, '.');
+ int c;
+
+ /* Group owned playlists by owner */
+ if(ad && bd) {
+ const int adn = ad - a, bdn = bd - b;
+ if((c = strncmp(a, b, adn < bdn ? adn : bdn)))
+ return c;
+ /* Lexical order within playlists of a single owner */
+ return strcmp(ad + 1, bd + 1);
+ }
+
+ /* Owned playlists after shared ones */
+ if(ad) {
+ return 1;
+ } else if(bd) {
+ return -1;
+ }
+
+ /* Lexical order of shared playlists */
+ return strcmp(a, b);
+}
+
+/** @brief Called with a new list of playlists */
+static void playlists_updated(void attribute((unused)) *v,
+ const char *err,
+ int nvec, char **vec) {
+ if(err) {
+ playlists = 0;
+ nplaylists = -1;
+ /* Probably means server does not support playlists */
+ } else {
+ playlists = vec;
+ nplaylists = nvec;
+ qsort(playlists, nplaylists, sizeof (char *), playlistcmp);
+ }
+ /* Tell our consumers */
+ event_raise("playlists-updated", 0);
+}
+
+/** @brief Called to activate a playlist */
+static void menu_activate_playlist(GtkMenuItem *menuitem,
+ gpointer attribute((unused)) user_data) {
+ GtkLabel *label = GTK_LABEL(GTK_BIN(menuitem)->child);
+ const char *playlist = gtk_label_get_text(label);
+
+ fprintf(stderr, "activate playlist %s\n", playlist); /* TODO */
+}
+
+/** @brief Called when the playlists change */
+static void menu_playlists_changed(const char attribute((unused)) *event,
+ void attribute((unused)) *eventdata,
+ void attribute((unused)) *callbackdata) {
+ if(!playlists_menu)
+ return; /* OMG too soon */
+ GtkMenuShell *menu = GTK_MENU_SHELL(playlists_menu);
+ /* TODO: we could be more sophisticated and only insert/remove widgets as
+ * needed. For now that's too much effort. */
+ while(menu->children)
+ gtk_container_remove(GTK_CONTAINER(menu), GTK_WIDGET(menu->children->data));
+ /* NB nplaylists can be -1 as well as 0 */
+ for(int n = 0; n < nplaylists; ++n) {
+ GtkWidget *w = gtk_menu_item_new_with_label(playlists[n]);
+ g_signal_connect(w, "activate", G_CALLBACK(menu_activate_playlist), 0);
+ gtk_widget_show(w);
+ gtk_menu_shell_append(menu, w);
+ }
+ gtk_widget_set_sensitive(playlists_widget,
+ nplaylists > 0);
+ gtk_widget_set_sensitive(editplaylists_widget,
+ nplaylists >= 0);
+}
+
+ void edit_playlists(gpointer attribute((unused)) callback_data,
+ guint attribute((unused)) callback_action,
+ GtkWidget attribute((unused)) *menu_item) {
+ fprintf(stderr, "edit playlists\n"); /* TODO */
+}
+
+/** @brief Initialize playlist support */
+void playlists_init(void) {
+ /* We re-get all playlists upon any change... */
+ event_register("playlist-created", playlists_update, 0);
+ event_register("playlist-modified", playlists_update, 0);
+ event_register("playlist-deleted", playlists_update, 0);
+ /* ...and on reconnection */
+ event_register("log-connected", playlists_update, 0);
+ /* ...and from time to time */
+ event_register("periodic-slow", playlists_update, 0);
+ /* ...and at startup */
+ event_register("playlists-updated", menu_playlists_changed, 0);
+ playlists_update(0, 0, 0);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
/** @brief Return the @ref queue_entry corresponding to @p iter
* @param model Model that owns @p iter
* @param iter Tree iterator
- * @return ID string
+ * @return Pointer to queue entry
*/
struct queue_entry *ql_iter_to_q(GtkTreeModel *model,
GtkTreeIter *iter) {
--suppress_actions;
}
+/* Drag and drop has to be figured out experimentally, because it is not well
+ * documented.
+ *
+ * First you get a row-inserted. The path argument points to the destination
+ * row but this will not yet have had its values set. The source row is still
+ * present. AFAICT the iter argument points to the same place.
+ *
+ * Then you get a row-deleted. The path argument identifies the row that was
+ * deleted. By this stage the row inserted above has acquired its values.
+ *
+ * A complication is that the deletion will move the inserted row. For
+ * instance, if you do a drag that moves row 1 down to after the track that was
+ * formerly on row 9, in the row-inserted call it will show up as row 10, but
+ * in the row-deleted call, row 1 will have been deleted thus making the
+ * inserted row be row 9.
+ *
+ * So when we see the row-inserted we have no idea what track to move.
+ * Therefore we stash it until we see a row-deleted.
+ */
+
+/** @brief row-inserted callback */
+static void ql_row_inserted(GtkTreeModel attribute((unused)) *treemodel,
+ GtkTreePath *path,
+ GtkTreeIter attribute((unused)) *iter,
+ gpointer user_data) {
+ struct queuelike *const ql = user_data;
+ if(!suppress_actions) {
+#if 0
+ char *ps = gtk_tree_path_to_string(path);
+ GtkTreeIter piter[1];
+ gboolean pi = gtk_tree_model_get_iter(treemodel, piter, path);
+ struct queue_entry *pq = pi ? ql_iter_to_q(treemodel, piter) : 0;
+ struct queue_entry *iq = ql_iter_to_q(treemodel, iter);
+
+ fprintf(stderr, "row-inserted %s path=%s pi=%d pq=%p path=%s iq=%p iter=%s\n",
+ ql->name,
+ ps,
+ pi,
+ pq,
+ (pi
+ ? (pq ? pq->track : "(pq=0)")
+ : "(pi=FALSE)"),
+ iq,
+ iq ? iq->track : "(iq=0)");
+
+ GtkTreeIter j[1];
+ gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
+ int row = 0;
+ while(jt) {
+ struct queue_entry *q = ql_iter_to_q(treemodel, j);
+ fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
+ jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), j);
+ }
+ g_free(ps);
+#endif
+ /* Remember an iterator pointing at the insertion target */
+ if(ql->drag_target)
+ gtk_tree_path_free(ql->drag_target);
+ ql->drag_target = gtk_tree_path_copy(path);
+ }
+}
+
+/** @brief row-deleted callback */
+static void ql_row_deleted(GtkTreeModel attribute((unused)) *treemodel,
+ GtkTreePath *path,
+ gpointer user_data) {
+ struct queuelike *const ql = user_data;
+
+ if(!suppress_actions) {
+#if 0
+ char *ps = gtk_tree_path_to_string(path);
+ fprintf(stderr, "row-deleted %s path=%s ql->drag_target=%s\n",
+ ql->name, ps, gtk_tree_path_to_string(ql->drag_target));
+ GtkTreeIter j[1];
+ gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
+ int row = 0;
+ while(jt) {
+ struct queue_entry *q = ql_iter_to_q(treemodel, j);
+ fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
+ jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), j);
+ }
+ g_free(ps);
+#endif
+ if(!ql->drag_target) {
+ error(0, "%s: unsuppressed row-deleted with no row-inserted",
+ ql->name);
+ return;
+ }
+
+ /* Get the source and destination row numbers. */
+ int srcrow = gtk_tree_path_get_indices(path)[0];
+ int dstrow = gtk_tree_path_get_indices(ql->drag_target)[0];
+ //fprintf(stderr, "srcrow=%d dstrow=%d\n", srcrow, dstrow);
+
+ /* Note that the source row is computed AFTER the destination has been
+ * inserted, since GTK+ does the insert before the delete. Therefore if
+ * the source row is south (higher row number) of the destination, it will
+ * be one higher than expected.
+ *
+ * For instance if we drag row 1 to before row 0 we will see row-inserted
+ * for row 0 but then a row-deleted for row 2.
+ */
+ if(srcrow > dstrow)
+ --srcrow;
+
+ /* Tell the queue implementation */
+ ql->drop(srcrow, dstrow);
+
+ /* Dispose of stashed data */
+ gtk_tree_path_free(ql->drag_target);
+ ql->drag_target = 0;
+ }
+}
+
/** @brief Initialize a @ref queuelike */
GtkWidget *init_queuelike(struct queuelike *ql) {
D(("init_queuelike"));
g_signal_connect(ql->view, "button-press-event",
G_CALLBACK(ql_button_release), ql);
+ /* Drag+drop*/
+ if(ql->drop) {
+ gtk_tree_view_set_reorderable(GTK_TREE_VIEW(ql->view), TRUE);
+ g_signal_connect(ql->store,
+ "row-inserted",
+ G_CALLBACK(ql_row_inserted), ql);
+ g_signal_connect(ql->store,
+ "row-deleted",
+ G_CALLBACK(ql_row_deleted), ql);
+ }
+
/* TODO style? */
ql->init();
/** @brief Menu callbacks */
struct tabtype tabtype;
+
+ /** @brief Drag-drop callback, or NULL for no drag+drop
+ * @param src Row to move
+ * @param dst Destination position
+ *
+ * If the rearrangement is impossible then the displayed queue must be put
+ * back.
+ */
+ void (*drop)(int src, int dst);
+
+ /** @brief Stashed drag target row */
+ GtkTreePath *drag_target;
};
enum {
g_timeout_add(1000/*ms*/, playing_periodic, 0);
}
+static void queue_move_completed(void attribute((unused)) *v,
+ const char *err) {
+ if(err) {
+ popup_protocol_error(0, err);
+ return;
+ }
+ /* The log should tell us the queue changed so we do no more here */
+}
+
+/** @brief Called when drag+drop completes */
+static void queue_drop(int src, int dst) {
+ struct queue_entry *sq, *dq;
+ int n;
+
+ //fprintf(stderr, "queue_drop %d -> %d\n", src, dst);
+ if(playing_track) {
+ /* If there's a playing track then you can't drag it anywhere */
+ if(src == 0) {
+ //fprintf(stderr, "cannot drag playing track\n");
+ queue_playing_changed();
+ return;
+ }
+ /* If you try to drop before the playing track we assume you missed and
+ * mean after instead */
+ if(!dst)
+ dst = 1;
+ //fprintf(stderr, "...adjusted to %d -> %d\n\n", src, dst);
+ }
+ /* Find the entry to move */
+ for(n = 0, sq = ql_queue.q; n < src; ++n)
+ sq = sq->next;
+ /*fprintf(stderr, "source=%s (%s)\n",
+ sq->id, sq->track);*/
+ const int after = dst - 1;
+ if(after == -1)
+ dq = 0;
+ else
+ /* Find the entry to insert after */
+ for(n = 0, dq = ql_queue.q; n < after; ++n)
+ dq = dq->next;
+ if(dq == playing_track)
+ dq = 0;
+#if 0
+ if(dq)
+ fprintf(stderr, "after=%s (%s)\n",
+ dq->id, dq->track);
+ else
+ fprintf(stderr, "after=NULL\n");
+#endif
+ disorder_eclient_moveafter(client,
+ dq ? dq->id : "",
+ 1, &sq->id,
+ queue_move_completed, NULL);
+}
+
/** @brief Columns for the queue */
static const struct queue_column queue_columns[] = {
{ "When", column_when, 0, COL_RIGHT },
.columns = queue_columns,
.ncolumns = sizeof queue_columns / sizeof *queue_columns,
.menuitems = queue_menuitems,
- .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems
+ .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems,
+ .drop = queue_drop
};
-/* Drag and drop has to be figured out experimentally, because it is not well
- * documented.
- *
- * First you get a row-inserted. The path argument points to the destination
- * row but this will not yet have had its values set. The source row is still
- * present. AFAICT the iter argument points to the same place.
- *
- * Then you get a row-deleted. The path argument identifies the row that was
- * deleted. By this stage the row inserted above has acquired its values.
- *
- * A complication is that the deletion will move the inserted row. For
- * instance, if you do a drag that moves row 1 down to after the track that was
- * formerly on row 9, in the row-inserted call it will show up as row 10, but
- * in the row-deleted call, row 1 will have been deleted thus making the
- * inserted row be row 9.
- *
- * So when we see the row-inserted we have no idea what track to move.
- * Therefore we stash it until we see a row-deleted.
- */
-
-/** @brief Target row for drag */
-static int queue_drag_target = -1;
-
-static void queue_move_completed(void attribute((unused)) *v,
- const char *err) {
- if(err) {
- popup_protocol_error(0, err);
- return;
- }
- /* The log should tell us the queue changed so we do no more here */
-}
-
-static void queue_row_deleted(GtkTreeModel *treemodel,
- GtkTreePath *path,
- gpointer attribute((unused)) user_data) {
- if(!suppress_actions) {
-#if 0
- char *ps = gtk_tree_path_to_string(path);
- fprintf(stderr, "row-deleted path=%s queue_drag_target=%d\n",
- ps, queue_drag_target);
- GtkTreeIter j[1];
- gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
- int row = 0;
- while(jt) {
- struct queue_entry *q = ql_iter_to_q(treemodel, j);
- fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
- jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j);
- }
- g_free(ps);
-#endif
- if(queue_drag_target < 0) {
- error(0, "unsuppressed row-deleted with no row-inserted");
- return;
- }
- int drag_source = gtk_tree_path_get_indices(path)[0];
-
- /* If the drag is downwards (=towards higher row numbers) then the target
- * will have been moved upwards (=towards lower row numbers) by one row. */
- if(drag_source < queue_drag_target)
- --queue_drag_target;
-
- /* Find the track to move */
- GtkTreeIter src[1];
- gboolean srcv = gtk_tree_model_iter_nth_child(treemodel, src, NULL,
- queue_drag_target);
- if(!srcv) {
- error(0, "cannot get iterator to drag target %d", queue_drag_target);
- queue_playing_changed();
- queue_drag_target = -1;
- return;
- }
- struct queue_entry *srcq = ql_iter_to_q(treemodel, src);
- assert(srcq);
- //fprintf(stderr, "move %s %s\n", srcq->id, srcq->track);
-
- /* Don't allow the currently playing track to be moved. As above, we put
- * the queue back into the right order straight away. */
- if(srcq == playing_track) {
- //fprintf(stderr, "cannot move currently playing track\n");
- queue_playing_changed();
- queue_drag_target = -1;
- return;
- }
-
- /* Find the destination */
- struct queue_entry *dstq;
- if(queue_drag_target) {
- GtkTreeIter dst[1];
- gboolean dstv = gtk_tree_model_iter_nth_child(treemodel, dst, NULL,
- queue_drag_target - 1);
- if(!dstv) {
- error(0, "cannot get iterator to drag target predecessor %d",
- queue_drag_target - 1);
- queue_playing_changed();
- queue_drag_target = -1;
- return;
- }
- dstq = ql_iter_to_q(treemodel, dst);
- assert(dstq);
- if(dstq == playing_track)
- dstq = 0;
- } else
- dstq = 0;
- /* NB if the user attempts to move a queued track before the currently
- * playing track we assume they just missed a bit, and put it after. */
- //fprintf(stderr, " target %s %s\n", dstq ? dstq->id : "(none)", dstq ? dstq->track : "(none)");
- /* Now we know what is to be moved. We need to know the preceding queue
- * entry so we can move it. */
- disorder_eclient_moveafter(client,
- dstq ? dstq->id : "",
- 1, &srcq->id,
- queue_move_completed, NULL);
- queue_drag_target = -1;
- }
-}
-
-static void queue_row_inserted(GtkTreeModel attribute((unused)) *treemodel,
- GtkTreePath *path,
- GtkTreeIter attribute((unused)) *iter,
- gpointer attribute((unused)) user_data) {
- if(!suppress_actions) {
-#if 0
- char *ps = gtk_tree_path_to_string(path);
- GtkTreeIter piter[1];
- gboolean pi = gtk_tree_model_get_iter(treemodel, piter, path);
- struct queue_entry *pq = pi ? ql_iter_to_q(treemodel, piter) : 0;
- struct queue_entry *iq = ql_iter_to_q(treemodel, iter);
-
- fprintf(stderr, "row-inserted path=%s pi=%d pq=%p path=%s iq=%p iter=%s\n",
- ps,
- pi,
- pq,
- (pi
- ? (pq ? pq->track : "(pq=0)")
- : "(pi=FALSE)"),
- iq,
- iq ? iq->track : "(iq=0)");
-
- GtkTreeIter j[1];
- gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
- int row = 0;
- while(jt) {
- struct queue_entry *q = ql_iter_to_q(treemodel, j);
- fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
- jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j);
- }
- g_free(ps);
-#endif
- queue_drag_target = gtk_tree_path_get_indices(path)[0];
- }
-}
-
/** @brief Called when a key is pressed in the queue tree view */
static gboolean queue_key_press(GtkWidget attribute((unused)) *widget,
GdkEventKey *event,
GtkWidget *queue_widget(void) {
GtkWidget *const w = init_queuelike(&ql_queue);
- /* Enable drag+drop */
- gtk_tree_view_set_reorderable(GTK_TREE_VIEW(ql_queue.view), TRUE);
- g_signal_connect(ql_queue.store,
- "row-inserted",
- G_CALLBACK(queue_row_inserted), &ql_queue);
- g_signal_connect(ql_queue.store,
- "row-deleted",
- G_CALLBACK(queue_row_deleted), &ql_queue);
/* Catch keypresses */
g_signal_connect(ql_queue.view, "key-press-event",
G_CALLBACK(queue_key_press), &ql_queue);
.B playing
Report the currently playing track.
.TP
+.B playlist-del \fIPLAYLIST\fR
+Deletes playlist \fIPLAYLIST\fR.
+.TP
+.B playlist-get \fIPLAYLIST\fR
+Gets the contents of playlist \fIPLAYLIST\fR.
+.TP
+.B playlist-set \fIPLAYLIST\fR [\fIPATH\fR]
+Set the contents of playlist \fIPLAYLIST\fR.
+If an absolute path name is specified then the track list is read from
+that filename.
+Otherwise the track list is read from standard input.
+In either case, the list is terminated either by end of file or by a line
+containing a single ".".
+.TP
+.B playlists
+Lists known playlists (in no particular order).
+.TP
.B prefs \fITRACK\fR
Display all the preferences for \fITRACK\fR.
See \fBdisorder_preferences\fR (5).
Bodies borrow their syntax from RFC821; they consist of zero or more ordinary
lines, with any initial full stop doubled up, and are terminated by a line
consisting of a full stop and a line feed.
+.PP
+Commands only have a body if explicitly stated below.
+If they do have a body then the body should always be sent immediately;
+unlike (for instance) the SMTP "DATA" command there is no intermediate step
+where the server asks for the body to be sent.
+.PP
+Replies also only have a body if stated below.
+The presence of a reply body can always be inferred from the response code;
+if the last digit is a 3 then a body is present, otherwise it is not.
.SH COMMANDS
Commands always have a command name as the first field of the line; responses
always have a 3-digit response code as the first field.
stated otherwise.
If not stated otherwise, the \fBread\fR right is sufficient to execute
the command.
-.PP
-Neither commands nor responses have a body unless stated otherwise.
.TP
.B adduser \fIUSERNAME PASSWORD \fR[\fIRIGHTS\fR]
Create a new user with the given username and password.
.IP
If the response is \fB259\fR then nothing is playing.
.TP
+.B playlist-delete \fIPLAYLIST\fR
+Delete a playlist.
+Requires permission to modify that playlist and the \fBplay\fR right.
+.TP
+.B playlist-get \fIPLAYLIST\fR
+Get the contents of a playlist, in a response body.
+Requires permission to read that playlist and the \fBread\fR right.
+.TP
+.B playlist-get-share \fIPLAYLIST\fR
+Get the sharing status of a playlist.
+The result will be \fBpublic\fR, \fBprivate\fR or \fBshared\fR.
+Requires permission to read that playlist and the \fBread\fR right.
+.TP
+.B playlist-lock \fIPLAYLIST\fR
+Lock a playlist.
+Requires permission to modify that playlist and the \fBplay\fR right.
+Only one playlist may be locked at a time on a given connection and the lock
+automatically expires when the connection is closed.
+.TP
+.B playlist-set \fIPLAYLIST\fR
+Set the contents of a playlist.
+The new contents should be supplied in a command body.
+Requires permission to modify that playlist and the \fBplay\fR right.
+The playlist must be locked.
+.TP
+.B playlist-set-share \fIPLAYLIST\fR \fISHARE\fR
+Set the sharing status of a playlist to
+\fBpublic\fR, \fBprivate\fR or \fBshared\fR.
+Requires permission to modify that playlist and the \fBplay\fR right.
+.TP
+.B playlist-unlock\fR
+Unlock the locked playlist.
+.TP
+.B playlists
+List all playlists that this connection has permission to read.
+Requires the \fBread\fR right.
+.TP
.B prefs \fBTRACK\fR
Send back the preferences for \fITRACK\fR in a response body.
Each line of the response has the usual line syntax, the first field being the
.B playing \fITRACK\fR [\fIUSERNAME\fR]
Started playing \fITRACK\fR.
.TP
+.B playlist_created \fIPLAYLIST\fR \fISHARING\fR
+Sent when a playlist is created.
+For private playlists this is intended to be sent only to the owner (but
+this is not currently implemented).
+.TP
+.B playlist_deleted \fIPLAYLIST\fR
+Sent when a playlist is deleted.
+For private playlists this is intended to be sent only to the owner (but
+this is not currently implemented).
+.TP
+.B playlist_modified \fIPLAYLIST\fR \fISHARING\fR
+Sent when a playlist is modified (either its contents or its sharing status).
+For private playlists this is intended to be sent only to the owner (but
+this is not currently implemented).
+.TP
.B queue \fIQUEUE-ENTRY\fR...
Added \fITRACK\fR to the queue.
.TP
table.c table.h \
timeval.h \
$(TRACKDB) trackdb.h trackdb-int.h \
+ trackdb-playlists.c \
trackname.c trackorder.c trackname.h \
tracksort.c \
url.h url.c \
* @param c Client
* @param rp Where to store result, or NULL
* @param cmd Command
+ * @param body Body or NULL
+ * @param nbody Length of body or -1
* @param ap Arguments (UTF-8), terminated by (char *)0
* @return 0 on success, non-0 on error
*
*
* NB that the response will NOT be converted to the local encoding
* nor will quotes be stripped. See dequote().
+ *
+ * If @p body is not NULL then the body is sent immediately after the
+ * command. @p nbody should be the number of lines or @c -1 to count
+ * them if @p body is NULL-terminated.
+ *
+ * Usually you would call this via one of the following interfaces:
+ * - disorder_simple()
+ * - disorder_simple_body()
+ * - disorder_simple_list()
*/
static int disorder_simple_v(disorder_client *c,
char **rp,
- const char *cmd, va_list ap) {
+ const char *cmd,
+ char **body, int nbody,
+ va_list ap) {
const char *arg;
struct dynstr d;
dynstr_append(&d, '\n');
dynstr_terminate(&d);
D(("command: %s", d.vec));
- if(fputs(d.vec, c->fpout) < 0 || fflush(c->fpout)) {
- byte_xasprintf((char **)&c->last, "write error: %s", strerror(errno));
- error(errno, "error writing to %s", c->ident);
- return -1;
+ if(fputs(d.vec, c->fpout) < 0)
+ goto write_error;
+ if(body) {
+ if(nbody < 0)
+ for(nbody = 0; body[nbody]; ++nbody)
+ ;
+ for(int n = 0; n < nbody; ++n) {
+ if(body[n][0] == '.')
+ if(fputc('.', c->fpout) < 0)
+ goto write_error;
+ if(fputs(body[n], c->fpout) < 0)
+ goto write_error;
+ if(fputc('\n', c->fpout) < 0)
+ goto write_error;
+ }
+ if(fputs(".\n", c->fpout) < 0)
+ goto write_error;
}
+ if(fflush(c->fpout))
+ goto write_error;
}
return check_response(c, rp);
+write_error:
+ byte_xasprintf((char **)&c->last, "write error: %s", strerror(errno));
+ error(errno, "error writing to %s", c->ident);
+ return -1;
}
/** @brief Issue a command and parse a simple response
int ret;
va_start(ap, cmd);
- ret = disorder_simple_v(c, rp, cmd, ap);
+ ret = disorder_simple_v(c, rp, cmd, 0, 0, ap);
+ va_end(ap);
+ return ret;
+}
+
+/** @brief Issue a command with a body and parse a simple response
+ * @param c Client
+ * @param rp Where to store result, or NULL (UTF-8)
+ * @param body Pointer to body
+ * @param nbody Size of body
+ * @param cmd Command
+ * @return 0 on success, non-0 on error
+ *
+ * See disorder_simple().
+ */
+static int disorder_simple_body(disorder_client *c,
+ char **rp,
+ char **body, int nbody,
+ const char *cmd, ...) {
+ va_list ap;
+ int ret;
+
+ va_start(ap, cmd);
+ ret = disorder_simple_v(c, rp, cmd, body, nbody, ap);
va_end(ap);
return ret;
}
* *)0. They should be in UTF-8.
*
* 5xx responses count as errors.
+ *
+ * See disorder_simple().
*/
static int disorder_simple_list(disorder_client *c,
char ***vecp, int *nvecp,
int ret;
va_start(ap, cmd);
- ret = disorder_simple_v(c, 0, cmd, ap);
+ ret = disorder_simple_v(c, 0, cmd, 0, 0, ap);
va_end(ap);
if(ret) return ret;
return readlist(c, vecp, nvecp);
return disorder_simple(c, 0, "adopt", id, (char *)0);
}
+/** @brief Delete a playlist
+ * @param c Client
+ * @param playlist Playlist to delete
+ * @return 0 on success, non-0 on error
+ */
+int disorder_playlist_delete(disorder_client *c,
+ const char *playlist) {
+ return disorder_simple(c, 0, "playlist-delete", playlist, (char *)0);
+}
+
+/** @brief Get the contents of a playlist
+ * @param c Client
+ * @param playlist Playlist to get
+ * @param tracksp Where to put list of tracks
+ * @param ntracksp Where to put count of tracks
+ * @return 0 on success, non-0 on error
+ */
+int disorder_playlist_get(disorder_client *c, const char *playlist,
+ char ***tracksp, int *ntracksp) {
+ return disorder_simple_list(c, tracksp, ntracksp,
+ "playlist-get", playlist, (char *)0);
+}
+
+/** @brief List all readable playlists
+ * @param c Client
+ * @param playlistsp Where to put list of playlists
+ * @param nplaylistsp Where to put count of playlists
+ * @return 0 on success, non-0 on error
+ */
+int disorder_playlists(disorder_client *c,
+ char ***playlistsp, int *nplaylistsp) {
+ return disorder_simple_list(c, playlistsp, nplaylistsp,
+ "playlists", (char *)0);
+}
+
+/** @brief Get the sharing status of a playlist
+ * @param c Client
+ * @param playlist Playlist to inspect
+ * @param sharep Where to put sharing status
+ * @return 0 on success, non-0 on error
+ *
+ * Possible @p sharep values are @c public, @c private and @c shared.
+ */
+int disorder_playlist_get_share(disorder_client *c, const char *playlist,
+ char **sharep) {
+ return disorder_simple(c, sharep,
+ "playlist-get-share", playlist, (char *)0);
+}
+
+/** @brief Get the sharing status of a playlist
+ * @param c Client
+ * @param playlist Playlist to modify
+ * @param share New sharing status
+ * @return 0 on success, non-0 on error
+ *
+ * Possible @p share values are @c public, @c private and @c shared.
+ */
+int disorder_playlist_set_share(disorder_client *c, const char *playlist,
+ const char *share) {
+ return disorder_simple(c, 0,
+ "playlist-set-share", playlist, share, (char *)0);
+}
+
+/** @brief Lock a playlist for modifications
+ * @param c Client
+ * @param playlist Playlist to lock
+ * @return 0 on success, non-0 on error
+ */
+int disorder_playlist_lock(disorder_client *c, const char *playlist) {
+ return disorder_simple(c, 0,
+ "playlist-lock", playlist, (char *)0);
+}
+
+/** @brief Unlock the locked playlist
+ * @param c Client
+ * @return 0 on success, non-0 on error
+ */
+int disorder_playlist_unlock(disorder_client *c) {
+ return disorder_simple(c, 0,
+ "playlist-unlock", (char *)0);
+}
+
+/** @brief Set the contents of a playlst
+ * @param c Client
+ * @param playlist Playlist to modify
+ * @param tracks List of tracks
+ * @param ntracks Length of @p tracks (or -1 to count up to the first NULL)
+ * @return 0 on success, non-0 on error
+ */
+int disorder_playlist_set(disorder_client *c,
+ const char *playlist,
+ char **tracks,
+ int ntracks) {
+ return disorder_simple_body(c, 0, tracks, ntracks,
+ "playlist-set", playlist, (char *)0);
+}
+
/*
Local Variables:
c-basic-offset:2
const char *action,
...);
int disorder_adopt(disorder_client *c, const char *id);
+int disorder_playlist_delete(disorder_client *c,
+ const char *playlist);
+int disorder_playlist_get(disorder_client *c, const char *playlist,
+ char ***tracksp, int *ntracksp);
+int disorder_playlists(disorder_client *c,
+ char ***playlistsp, int *nplaylists);
+int disorder_playlist_get_share(disorder_client *c, const char *playlist,
+ char **sharep);
+int disorder_playlist_set_share(disorder_client *c, const char *playlist,
+ const char *share);
+int disorder_playlist_lock(disorder_client *c, const char *playlist);
+int disorder_playlist_unlock(disorder_client *c);
+int disorder_playlist_set(disorder_client *c,
+ const char *playlist,
+ char **tracks,
+ int ntracks);
#endif /* CLIENT_H */
{ C(noticed_history), &type_integer, validate_positive },
{ C(password), &type_string, validate_any },
{ C(player), &type_stringlist_accum, validate_player },
+ { C(playlist_lock_timeout), &type_integer, validate_positive },
+ { C(playlist_max) , &type_integer, validate_positive },
{ C(plugins), &type_string_accum, validate_isdir },
{ C(prefsync), &type_integer, validate_positive },
{ C(queue_pad), &type_integer, validate_positive },
c->new_bias_age = 7 * 86400; /* 1 week */
c->new_bias = 4500000; /* 50 times the base weight */
c->sox_generation = DEFAULT_SOX_GENERATION;
+ c->playlist_max = INT_MAX; /* effectively no limit */
+ c->playlist_lock_timeout = 10; /* 10s */
/* Default stopwords */
if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords))
exit(1);
*/
int api;
+ /** @brief Maximum size of a playlist */
+ long playlist_max;
+
+ /** @brief Maximum lifetime of a playlist lock */
+ long playlist_lock_timeout;
+
/* These values had better be non-negative */
#define BACKEND_ALSA 0 /**< Use ALSA (Linux only) */
#define BACKEND_COMMAND 1 /**< Execute a command */
struct operation {
struct operation *next; /**< @brief next operation */
char *cmd; /**< @brief command to send or 0 */
+ char **body; /**< @brief command body */
operation_callback *opcallback; /**< @brief internal completion callback */
void (*completed)(); /**< @brief user completion callback or 0 */
void *v; /**< @brief data for COMPLETED */
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
const char *cmd,
...);
static void log_opcallback(disorder_eclient *c, struct operation *op);
static void logentry_user_edit(disorder_eclient *c, int nvec, char **vec);
static void logentry_rights_changed(disorder_eclient *c, int nvec, char **vec);
static void logentry_adopted(disorder_eclient *c, int nvec, char **vec);
+static void logentry_playlist_created(disorder_eclient *c, int nvec, char **vec);
+static void logentry_playlist_deleted(disorder_eclient *c, int nvec, char **vec);
+static void logentry_playlist_modified(disorder_eclient *c, int nvec, char **vec);
/* Tables ********************************************************************/
LE(failed, 2, 2),
LE(moved, 1, 1),
LE(playing, 1, 2),
+ LE(playlist_created, 2, 2),
+ LE(playlist_deleted, 1, 1),
+ LE(playlist_modified, 2, 2),
LE(queue, 2, INT_MAX),
LE(recent_added, 2, INT_MAX),
LE(recent_removed, 1, 1),
/* State machine *************************************************************/
+/** @brief Send an operation (into the output buffer)
+ * @param op Operation to send
+ */
+static void op_send(struct operation *op) {
+ disorder_eclient *const c = op->client;
+ put(c, op->cmd, strlen(op->cmd));
+ if(op->body) {
+ for(int n = 0; op->body[n]; ++n) {
+ if(op->body[n][0] == '.')
+ put(c, ".", 1);
+ put(c, op->body[n], strlen(op->body[n]));
+ put(c, "\n", 1);
+ }
+ put(c, ".\n", 2);
+ }
+ op->sent = 1;
+}
+
/** @brief Called when there's something to do
* @param c Client
* @param mode bitmap of @ref DISORDER_POLL_READ and/or @ref DISORDER_POLL_WRITE.
D(("state_connected"));
/* We just connected. Initiate the authentication protocol. */
stash_command(c, 1/*queuejump*/, authbanner_opcallback,
- 0/*completed*/, 0/*v*/, 0/*cmd*/);
+ 0/*completed*/, 0/*v*/, -1/*nbody*/, 0/*body*/, 0/*cmd*/);
/* We never stay is state_connected very long. We could in principle jump
* straight to state_cmdresponse since there's actually no command to
* send, but that would arguably be cheating. */
if(c->authenticated) {
/* Transmit all unsent operations */
for(op = c->ops; op; op = op->next) {
- if(!op->sent) {
- put(c, op->cmd, strlen(op->cmd));
- op->sent = 1;
- }
+ if(!op->sent)
+ op_send(op);
}
} else {
/* Just send the head operation */
- if(c->ops->cmd && !c->ops->sent) {
- put(c, c->ops->cmd, strlen(c->ops->cmd));
- c->ops->sent = 1;
- }
+ if(c->ops->cmd && !c->ops->sent)
+ op_send(c->ops);
}
/* Awaiting response for the operation at the head of the list */
c->state = state_cmdresponse;
return;
}
stash_command(c, 1/*queuejump*/, authuser_opcallback, 0/*completed*/, 0/*v*/,
+ -1/*nbody*/, 0/*body*/,
"user", quoteutf8(config->username), quoteutf8(res),
(char *)0);
}
if(c->log_callbacks && !(c->ops && c->ops->opcallback == log_opcallback))
/* We are a log client, switch to logging mode */
stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, c->log_v,
+ -1/*nbody*/, 0/*body*/,
"log", (char *)0);
}
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
int ncmd,
char **cmd) {
struct operation *op = xmalloc(sizeof *op);
op->cmd = d.vec;
} else
op->cmd = 0; /* usually, awaiting challenge */
+ if(nbody >= 0) {
+ op->body = xcalloc(nbody + 1, sizeof (char *));
+ for(n = 0; n < nbody; ++n)
+ op->body[n] = xstrdup(body[n]);
+ op->body[n] = 0;
+ } else
+ op->body = NULL;
op->opcallback = opcallback;
op->completed = completed;
op->v = v;
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
const char *cmd, va_list ap) {
char *arg;
struct vector vec;
while((arg = va_arg(ap, char *)))
vector_append(&vec, arg);
stash_command_vector(c, queuejump, opcallback, completed, v,
- vec.nvec, vec.vec);
+ nbody, body, vec.nvec, vec.vec);
} else
- stash_command_vector(c, queuejump, opcallback, completed, v, 0, 0);
+ stash_command_vector(c, queuejump, opcallback, completed, v,
+ nbody, body,
+ 0, 0);
}
static void stash_command(disorder_eclient *c,
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
const char *cmd,
...) {
va_list ap;
va_start(ap, cmd);
- vstash_command(c, queuejump, opcallback, completed, v, cmd, ap);
+ vstash_command(c, queuejump, opcallback, completed, v, nbody, body, cmd, ap);
va_end(ap);
}
D(("list_response_callback"));
if(c->rc / 100 == 2)
completed(op->v, NULL, c->vec.nvec, c->vec.vec);
+ else if(c->rc == 555)
+ completed(op->v, NULL, -1, NULL);
else
completed(op->v, errorstring(c), 0, 0);
}
va_list ap;
va_start(ap, cmd);
- vstash_command(c, 0/*queuejump*/, opcallback, completed, v, cmd, ap);
+ vstash_command(c, 0/*queuejump*/, opcallback, completed, v, -1, 0, cmd, ap);
+ va_end(ap);
+ /* Give the state machine a kick, since we might be in state_idle */
+ disorder_eclient_polled(c, 0);
+ return 0;
+}
+
+static int simple_body(disorder_eclient *c,
+ operation_callback *opcallback,
+ void (*completed)(),
+ void *v,
+ int nbody,
+ char **body,
+ const char *cmd, ...) {
+ va_list ap;
+
+ va_start(ap, cmd);
+ vstash_command(c, 0/*queuejump*/, opcallback, completed, v, nbody, body, cmd, ap);
va_end(ap);
/* Give the state machine a kick, since we might be in state_idle */
disorder_eclient_polled(c, 0);
for(n = 0; n < nids; ++n)
vector_append(&vec, (char *)ids[n]);
stash_command_vector(c, 0/*queuejump*/, no_response_opcallback, completed, v,
- vec.nvec, vec.vec);
+ -1, 0, vec.nvec, vec.vec);
disorder_eclient_polled(c, 0);
return 0;
}
"adopt", id, (char *)0);
}
+/** @brief Get the list of playlists
+ * @param c Client
+ * @param completed Called with list of playlists
+ * @param v Passed to @p completed
+ *
+ * The playlist list is not sorted in any particular order.
+ */
+int disorder_eclient_playlists(disorder_eclient *c,
+ disorder_eclient_list_response *completed,
+ void *v) {
+ return simple(c, list_response_opcallback, (void (*)())completed, v,
+ "playlists", (char *)0);
+}
+
+/** @brief Delete a playlist
+ * @param c Client
+ * @param completed Called on completion
+ * @param playlist Playlist to delete
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_delete(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *playlist,
+ void *v) {
+ return simple(c, no_response_opcallback, (void (*)())completed, v,
+ "playlist-delete", playlist, (char *)0);
+}
+
+/** @brief Lock a playlist
+ * @param c Client
+ * @param completed Called on completion
+ * @param playlist Playlist to lock
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_lock(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *playlist,
+ void *v) {
+ return simple(c, no_response_opcallback, (void (*)())completed, v,
+ "playlist-lock", playlist, (char *)0);
+}
+
+/** @brief Unlock the locked a playlist
+ * @param c Client
+ * @param completed Called on completion
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_unlock(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ void *v) {
+ return simple(c, no_response_opcallback, (void (*)())completed, v,
+ "playlist-unlock", (char *)0);
+}
+
+/** @brief Set a playlist's sharing
+ * @param c Client
+ * @param completed Called on completion
+ * @param playlist Playlist to modify
+ * @param sharing @c "public" or @c "private"
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_set_share(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *playlist,
+ const char *sharing,
+ void *v) {
+ return simple(c, no_response_opcallback, (void (*)())completed, v,
+ "playlist-set-share", playlist, sharing, (char *)0);
+}
+
+/** @brief Get a playlist's sharing
+ * @param c Client
+ * @param completed Called with sharing status
+ * @param playlist Playlist to inspect
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_get_share(disorder_eclient *c,
+ disorder_eclient_string_response *completed,
+ const char *playlist,
+ void *v) {
+ return simple(c, string_response_opcallback, (void (*)())completed, v,
+ "playlist-get-share", playlist, (char *)0);
+}
+
+/** @brief Set a playlist
+ * @param c Client
+ * @param completed Called on completion
+ * @param playlist Playlist to modify
+ * @param tracks List of tracks
+ * @param ntracks Number of tracks
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_set(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *playlist,
+ char **tracks,
+ int ntracks,
+ void *v) {
+ return simple_body(c, no_response_opcallback, (void (*)())completed, v,
+ ntracks, tracks,
+ "playlist-set", playlist, (char *)0);
+}
+
+/** @brief Get a playlist's contents
+ * @param c Client
+ * @param completed Called with playlist contents
+ * @param playlist Playlist to inspect
+ * @param v Passed to @p completed
+ */
+int disorder_eclient_playlist_get(disorder_eclient *c,
+ disorder_eclient_list_response *completed,
+ const char *playlist,
+ void *v) {
+ return simple(c, list_response_opcallback, (void (*)())completed, v,
+ "playlist-get", playlist, (char *)0);
+}
+
/* Log clients ***************************************************************/
/** @brief Monitor the server log
if(c->log_callbacks->state)
c->log_callbacks->state(c->log_v, c->statebits);
stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, v,
- "log", (char *)0);
+ -1, 0, "log", (char *)0);
disorder_eclient_polled(c, 0);
return 0;
}
}
}
+static void logentry_playlist_created(disorder_eclient *c,
+ int attribute((unused)) nvec,
+ char **vec) {
+ if(c->log_callbacks->playlist_created)
+ c->log_callbacks->playlist_created(c->log_v, vec[0], vec[1]);
+}
+
+static void logentry_playlist_deleted(disorder_eclient *c,
+ int attribute((unused)) nvec,
+ char **vec) {
+ if(c->log_callbacks->playlist_deleted)
+ c->log_callbacks->playlist_deleted(c->log_v, vec[0]);
+}
+
+static void logentry_playlist_modified(disorder_eclient *c,
+ int attribute((unused)) nvec,
+ char **vec) {
+ if(c->log_callbacks->playlist_modified)
+ c->log_callbacks->playlist_modified(c->log_v, vec[0], vec[1]);
+}
+
static const struct {
unsigned long bit;
const char *enable;
/*
* This file is part of DisOrder.
- * Copyright (C) 2006, 2007 Richard Kettlewell
+ * Copyright (C) 2006-2008 Richard Kettlewell
*
* 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
/** @brief Called when a track is adopted */
void (*adopted)(void *v, const char *id, const char *who);
+
+ /** @brief Called when a new playlist is created */
+ void (*playlist_created)(void *v, const char *playlist, const char *sharing);
+
+ /** @brief Called when a playlist is modified */
+ void (*playlist_modified)(void *v, const char *playlist, const char *sharing);
+
+ /** @brief Called when a new playlist is deleted */
+ void (*playlist_deleted)(void *v, const char *playlist);
} disorder_eclient_log_callbacks;
/* State bits */
*
* @p error will be NULL on success. In this case @p value will be the result
* (which might be NULL for disorder_eclient_get(),
- * disorder_eclient_get_global() and disorder_eclient_userinfo()).
+ * disorder_eclient_get_global(), disorder_eclient_userinfo() and
+ * disorder_eclient_playlist_get_share()).
*
* @p error will be non-NULL on failure. In this case @p value is always NULL.
*/
* @param vec Pointer to response list
*
* @p error will be NULL on success. In this case @p nvec and @p vec will give
- * the result.
+ * the result, or be -1 and NULL respectively e.g. from
+ * disorder_eclient_playlist_get() if there is no such playlist.
*
* @p error will be non-NULL on failure. In this case @p nvec and @p vec will
* be 0 and NULL.
disorder_eclient_no_response *completed,
const char *id,
void *v);
+int disorder_eclient_playlists(disorder_eclient *c,
+ disorder_eclient_list_response *completed,
+ void *v);
+int disorder_eclient_playlist_delete(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *playlist,
+ void *v);
+int disorder_eclient_playlist_lock(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *playlist,
+ void *v);
+int disorder_eclient_playlist_unlock(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ void *v);
+int disorder_eclient_playlist_set_share(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *playlist,
+ const char *sharing,
+ void *v);
+int disorder_eclient_playlist_get_share(disorder_eclient *c,
+ disorder_eclient_string_response *completed,
+ const char *playlist,
+ void *v);
+int disorder_eclient_playlist_set(disorder_eclient *c,
+ disorder_eclient_no_response *completed,
+ const char *playlist,
+ char **tracks,
+ int ntracks,
+ void *v);
+int disorder_eclient_playlist_get(disorder_eclient *c,
+ disorder_eclient_list_response *completed,
+ const char *playlist,
+ void *v);
+
#endif
/*
#include <db.h>
+#include "trackdb.h"
#include "kvp.h"
struct vector; /* forward declaration */
extern DB *trackdb_globaldb;
extern DB *trackdb_usersdb;
extern DB *trackdb_scheduledb;
+extern DB *trackdb_playlistsdb;
DBC *trackdb_opencursor(DB *db, DB_TXN *tid);
/* open a transaction */
char **parsetags(const char *s);
int tag_intersection(char **a, char **b);
+int valid_username(const char *user);
#endif /* TRACKDB_INT_H */
--- /dev/null
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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
+ */
+/** @file lib/trackdb-playlists.c
+ * @brief Track database playlist support
+ *
+ * This file implements reading and modification of playlists, including access
+ * control, but not locking or event logging (at least yet).
+ */
+#include "common.h"
+
+#include <errno.h>
+
+#include "trackdb-int.h"
+#include "mem.h"
+#include "log.h"
+#include "configuration.h"
+#include "vector.h"
+#include "eventlog.h"
+
+static int trackdb_playlist_get_tid(const char *name,
+ const char *who,
+ char ***tracksp,
+ int *ntracksp,
+ char **sharep,
+ DB_TXN *tid);
+static int trackdb_playlist_set_tid(const char *name,
+ const char *who,
+ char **tracks,
+ int ntracks,
+ const char *share,
+ DB_TXN *tid);
+static int trackdb_playlist_list_tid(const char *who,
+ char ***playlistsp,
+ int *nplaylistsp,
+ DB_TXN *tid);
+static int trackdb_playlist_delete_tid(const char *name,
+ const char *who,
+ DB_TXN *tid);
+
+/** @brief Parse a playlist name
+ * @param name Playlist name
+ * @param ownerp Where to put owner, or NULL
+ * @param sharep Where to put default sharing, or NULL
+ * @return 0 on success, -1 on error
+ *
+ * Playlists take the form USER.PLAYLIST or just PLAYLIST. The PLAYLIST part
+ * is alphanumeric and nonempty. USER is a username (see valid_username()).
+ */
+int playlist_parse_name(const char *name,
+ char **ownerp,
+ char **sharep) {
+ const char *dot = strchr(name, '.'), *share;
+ char *owner;
+
+ if(dot) {
+ /* Owned playlist */
+ owner = xstrndup(name, dot - name);
+ if(!valid_username(owner))
+ return -1;
+ if(!valid_username(dot + 1))
+ return -1;
+ share = "private";
+ } else {
+ /* Shared playlist */
+ if(!valid_username(name))
+ return -1;
+ owner = 0;
+ share = "shared";
+ }
+ if(ownerp)
+ *ownerp = owner;
+ if(sharep)
+ *sharep = xstrdup(share);
+ return 0;
+}
+
+/** @brief Check read access rights
+ * @param name Playlist name
+ * @param who Who wants to read
+ * @param share Playlist share status
+ */
+static int playlist_may_read(const char *name,
+ const char *who,
+ const char *share) {
+ char *owner;
+
+ if(playlist_parse_name(name, &owner, 0))
+ return 0;
+ /* Anyone can read shared playlists */
+ if(!owner)
+ return 1;
+ /* You can always read playlists you own */
+ if(!strcmp(owner, who))
+ return 1;
+ /* You can read public playlists */
+ if(!strcmp(share, "public"))
+ return 1;
+ /* Anything else is prohibited */
+ return 0;
+}
+
+/** @brief Check modify access rights
+ * @param name Playlist name
+ * @param who Who wants to modify
+ * @param share Playlist share status
+ */
+static int playlist_may_write(const char *name,
+ const char *who,
+ const char attribute((unused)) *share) {
+ char *owner;
+
+ if(playlist_parse_name(name, &owner, 0))
+ return 0;
+ /* Anyone can modify shared playlists */
+ if(!owner)
+ return 1;
+ /* You can always modify playlists you own */
+ if(!strcmp(owner, who))
+ return 1;
+ /* Anything else is prohibited */
+ return 0;
+}
+
+/** @brief Get playlist data
+ * @param name Name of playlist
+ * @param who Who wants to know
+ * @param tracksp Where to put list of tracks, or NULL
+ * @param ntracksp Where to put count of tracks, or NULL
+ * @param sharep Where to put sharing type, or NULL
+ * @return 0 on success, non-0 on error
+ *
+ * Possible return values:
+ * - @c 0 on success
+ * - @c ENOENT if the playlist doesn't exist
+ * - @c EINVAL if the playlist name is invalid
+ * - @c EACCES if the playlist cannot be read by @p who
+ */
+int trackdb_playlist_get(const char *name,
+ const char *who,
+ char ***tracksp,
+ int *ntracksp,
+ char **sharep) {
+ int e;
+
+ if(playlist_parse_name(name, 0, 0)) {
+ error(0, "invalid playlist name '%s'", name);
+ return EINVAL;
+ }
+ WITH_TRANSACTION(trackdb_playlist_get_tid(name, who,
+ tracksp, ntracksp, sharep,
+ tid));
+ /* Don't expose libdb error codes too much */
+ if(e == DB_NOTFOUND)
+ e = ENOENT;
+ return e;
+}
+
+static int trackdb_playlist_get_tid(const char *name,
+ const char *who,
+ char ***tracksp,
+ int *ntracksp,
+ char **sharep,
+ DB_TXN *tid) {
+ struct kvp *k;
+ int e, ntracks;
+ const char *s;
+
+ if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid)))
+ return e;
+ /* Get sharability */
+ if(!(s = kvp_get(k, "sharing"))) {
+ error(0, "playlist '%s' has no 'sharing' key", name);
+ s = "private";
+ }
+ /* Check the read is allowed */
+ if(!playlist_may_read(name, who, s))
+ return EACCES;
+ /* Return sharability */
+ if(sharep)
+ *sharep = xstrdup(s);
+ /* Get track count */
+ if(!(s = kvp_get(k, "count"))) {
+ error(0, "playlist '%s' has no 'count' key", name);
+ s = "0";
+ }
+ ntracks = atoi(s);
+ if(ntracks < 0) {
+ error(0, "playlist '%s' has negative count", name);
+ ntracks = 0;
+ }
+ /* Return track count */
+ if(ntracksp)
+ *ntracksp = ntracks;
+ if(tracksp) {
+ /* Get track list */
+ char **tracks = xcalloc(ntracks + 1, sizeof (char *));
+ char b[16];
+
+ for(int n = 0; n < ntracks; ++n) {
+ snprintf(b, sizeof b, "%d", n);
+ if(!(s = kvp_get(k, b))) {
+ error(0, "playlist '%s' lacks track %d", name, n);
+ s = "unknown";
+ }
+ tracks[n] = xstrdup(s);
+ }
+ tracks[ntracks] = 0;
+ /* Return track list */
+ *tracksp = tracks;
+ }
+ return 0;
+}
+
+/** @brief Modify or create a playlist
+ * @param name Playlist name
+ * @param tracks List of tracks to set, or NULL to leave alone
+ * @param ntracks Length of @p tracks
+ * @param share Sharing status, or NULL to leave alone
+ * @return 0 on success, non-0 on error
+ *
+ * If the playlist exists it is just modified.
+ *
+ * If the playlist does not exist it is created. The default set of tracks is
+ * none, and the default sharing is private (if it is an owned one) or shared
+ * (otherwise).
+ *
+ * If neither @c tracks nor @c share are set then we only do an access check.
+ * The database is never modified (even to create the playlist) in this
+ * situation.
+ *
+ * Possible return values:
+ * - @c 0 on success
+ * - @c EINVAL if the playlist name is invalid
+ * - @c EACCES if the playlist cannot be modified by @p who
+ */
+int trackdb_playlist_set(const char *name,
+ const char *who,
+ char **tracks,
+ int ntracks,
+ const char *share) {
+ int e;
+ char *owner;
+
+ if(playlist_parse_name(name, &owner, 0)) {
+ error(0, "invalid playlist name '%s'", name);
+ return EINVAL;
+ }
+ /* Check valid share types */
+ if(share) {
+ if(owner) {
+ /* Playlists with an owner must be public or private */
+ if(strcmp(share, "public")
+ && strcmp(share, "private")) {
+ error(0, "playlist '%s' must be public or private", name);
+ return EINVAL;
+ }
+ } else {
+ /* Playlists with no owner must be shared */
+ if(strcmp(share, "shared")) {
+ error(0, "playlist '%s' must be shared", name);
+ return EINVAL;
+ }
+ }
+ }
+ /* We've checked as much as we can for now, now go and attempt the change */
+ WITH_TRANSACTION(trackdb_playlist_set_tid(name, who, tracks, ntracks, share,
+ tid));
+ return e;
+}
+
+static int trackdb_playlist_set_tid(const char *name,
+ const char *who,
+ char **tracks,
+ int ntracks,
+ const char *share,
+ DB_TXN *tid) {
+ struct kvp *k;
+ int e;
+ const char *s;
+ const char *event = "playlist_modified";
+
+ if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid))
+ && e != DB_NOTFOUND)
+ return e;
+ /* If the playlist doesn't exist set some defaults */
+ if(e == DB_NOTFOUND) {
+ char *defshare, *owner;
+
+ if(playlist_parse_name(name, &owner, &defshare))
+ return EINVAL;
+ /* Can't create a non-shared playlist belonging to someone else. In fact
+ * this should be picked up by playlist_may_write() below but it's clearer
+ * to do it here. */
+ if(owner && strcmp(owner, who))
+ return EACCES;
+ k = 0;
+ kvp_set(&k, "count", 0);
+ kvp_set(&k, "sharing", defshare);
+ event = "playlist_created";
+ }
+ /* Check that the modification is allowed */
+ if(!(s = kvp_get(k, "sharing"))) {
+ error(0, "playlist '%s' has no 'sharing' key", name);
+ s = "private";
+ }
+ if(!playlist_may_write(name, who, s))
+ return EACCES;
+ /* If no change was requested then don't even create */
+ if(!share && !tracks)
+ return 0;
+ /* Set the new values */
+ if(share)
+ kvp_set(&k, "sharing", share);
+ if(tracks) {
+ char b[16];
+ int oldcount, n;
+
+ /* Sanity check track count */
+ if(ntracks < 0 || ntracks > config->playlist_max) {
+ error(0, "invalid track count %d", ntracks);
+ return EINVAL;
+ }
+ /* Set the tracks */
+ for(n = 0; n < ntracks; ++n) {
+ snprintf(b, sizeof b, "%d", n);
+ kvp_set(&k, b, tracks[n]);
+ }
+ /* Get the old track count */
+ if((s = kvp_get(k, "count")))
+ oldcount = atoi(s);
+ else
+ oldcount = 0;
+ /* Delete old slots */
+ for(; n < oldcount; ++n) {
+ snprintf(b, sizeof b, "%d", n);
+ kvp_set(&k, b, NULL);
+ }
+ /* Set the new count */
+ snprintf(b, sizeof b, "%d", ntracks);
+ kvp_set(&k, "count", b);
+ }
+ /* Store the resulting record */
+ e = trackdb_putdata(trackdb_playlistsdb, name, k, tid, 0);
+ /* Log the event */
+ if(!e)
+ eventlog(event, name, kvp_get(k, "sharing"), (char *)0);
+ return e;
+}
+
+/** @brief Get a list of playlists
+ * @param who Who wants to know
+ * @param playlistsp Where to put list of playlists
+ * @param nplaylistsp Where to put count of playlists, or NULL
+ */
+void trackdb_playlist_list(const char *who,
+ char ***playlistsp,
+ int *nplaylistsp) {
+ int e;
+
+ WITH_TRANSACTION(trackdb_playlist_list_tid(who, playlistsp, nplaylistsp,
+ tid));
+}
+
+static int trackdb_playlist_list_tid(const char *who,
+ char ***playlistsp,
+ int *nplaylistsp,
+ DB_TXN *tid) {
+ struct vector v[1];
+ DBC *c;
+ DBT k[1], d[1];
+ int e;
+
+ vector_init(v);
+ c = trackdb_opencursor(trackdb_playlistsdb, tid);
+ memset(k, 0, sizeof k);
+ while(!(e = c->c_get(c, k, prepare_data(d), DB_NEXT))) {
+ char *name = xstrndup(k->data, k->size), *owner;
+ const char *share = kvp_get(kvp_urldecode(d->data, d->size),
+ "sharing");
+
+ /* Extract owner; malformed names are skipped */
+ if(playlist_parse_name(name, &owner, 0)) {
+ error(0, "invalid playlist name '%s' found in database", name);
+ continue;
+ }
+ if(!share) {
+ error(0, "playlist '%s' has no 'sharing' key", name);
+ continue;
+ }
+ /* Always list public and shared playlists
+ * Only list private ones to their owner
+ * Don't list anything else
+ */
+ if(!strcmp(share, "public")
+ || !strcmp(share, "shared")
+ || (!strcmp(share, "private")
+ && owner && !strcmp(owner, who)))
+ vector_append(v, name);
+ }
+ trackdb_closecursor(c);
+ switch(e) {
+ case DB_NOTFOUND:
+ break;
+ case DB_LOCK_DEADLOCK:
+ return e;
+ default:
+ fatal(0, "c->c_get: %s", db_strerror(e));
+ }
+ vector_terminate(v);
+ if(playlistsp)
+ *playlistsp = v->vec;
+ if(nplaylistsp)
+ *nplaylistsp = v->nvec;
+ return 0;
+}
+
+/** @brief Delete a playlist
+ * @param name Playlist name
+ * @param who Who is deleting it
+ * @return 0 on success, non-0 on error
+ *
+ * Possible return values:
+ * - @c 0 on success
+ * - @c EINVAL if the playlist name is invalid
+ * - @c EACCES if the playlist cannot be modified by @p who
+ * - @c ENOENT if the playlist doesn't exist
+ */
+int trackdb_playlist_delete(const char *name,
+ const char *who) {
+ int e;
+ char *owner;
+
+ if(playlist_parse_name(name, &owner, 0)) {
+ error(0, "invalid playlist name '%s'", name);
+ return EINVAL;
+ }
+ /* We've checked as much as we can for now, now go and attempt the change */
+ WITH_TRANSACTION(trackdb_playlist_delete_tid(name, who, tid));
+ if(e == DB_NOTFOUND)
+ e = ENOENT;
+ return e;
+}
+
+static int trackdb_playlist_delete_tid(const char *name,
+ const char *who,
+ DB_TXN *tid) {
+ struct kvp *k;
+ int e;
+ const char *s;
+
+ if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid)))
+ return e;
+ /* Check that modification is allowed */
+ if(!(s = kvp_get(k, "sharing"))) {
+ error(0, "playlist '%s' has no 'sharing' key", name);
+ s = "private";
+ }
+ if(!playlist_may_write(name, who, s))
+ return EACCES;
+ /* Delete the playlist */
+ e = trackdb_delkey(trackdb_playlistsdb, name, tid);
+ if(!e)
+ eventlog("playlist_deleted", name, 0);
+ return e;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
*/
DB *trackdb_usersdb;
+/** @brief The playlists database
+ * - Keys are playlist names
+ * - Values are encoded key-value pairs
+ * - Data is user data and cannot be reconstructed
+ */
+DB *trackdb_playlistsdb;
+
static pid_t db_deadlock_pid = -1; /* deadlock manager PID */
static pid_t rescan_pid = -1; /* rescanner PID */
static int initialized, opened; /* state */
trackdb_noticeddb = open_db("noticed.db",
DB_DUPSORT, DB_BTREE, dbflags, 0666);
trackdb_scheduledb = open_db("schedule.db", 0, DB_HASH, dbflags, 0666);
+ trackdb_playlistsdb = open_db("playlists.db", 0, DB_HASH, dbflags, 0666);
if(!trackdb_existing_database) {
/* Stash the database version */
char buf[32];
CLOSE("noticed.db", trackdb_noticeddb);
CLOSE("schedule.db", trackdb_scheduledb);
CLOSE("users.db", trackdb_usersdb);
+ CLOSE("playlists.db", trackdb_playlistsdb);
D(("closed databases"));
}
* Currently we only allow the letters and digits in ASCII. We could be more
* liberal than this but it is a nice simple test. It is critical that
* semicolons are never allowed.
+ *
+ * NB also used by playlist_parse_name() to validate playlist names!
*/
-static int valid_username(const char *user) {
+int valid_username(const char *user) {
if(!*user)
return 0;
while(*user) {
void *ru);
int trackdb_rescan_underway(void);
+int playlist_parse_name(const char *name,
+ char **ownerp,
+ char **sharep);
+int trackdb_playlist_get(const char *name,
+ const char *who,
+ char ***tracksp,
+ int *ntracksp,
+ char **sharep);
+int trackdb_playlist_set(const char *name,
+ const char *who,
+ char **tracks,
+ int ntracks,
+ const char *share);
+void trackdb_playlist_list(const char *who,
+ char ***playlistsp,
+ int *nplaylistsp);
+int trackdb_playlist_delete(const char *name,
+ const char *who);
+
#endif /* TRACKDB_H */
/*
self.cmd_ = cmd
self.details_ = details
def __str__(self):
- """Return the complete response string from the server, with the command
- if available.
+ """Return the complete response string from the server, with the
+ command if available.
Excludes the final newline.
"""
Returns the ID of the new queue entry.
- Note that queue IDs are unicode strings (because all track information
- values are unicode strings).
+ Note that queue IDs are unicode strings (because all track
+ information values are unicode strings).
"""
res, details = self._simple("play", track)
return unicode(details) # because it's unicode in queue() output
The return value is a list of dictionaries corresponding to
recently played tracks. The next track to be played comes first.
- See disorder_protocol(5) for the meanings of the keys. All keys are
- plain strings but the values will be unicode strings."""
+ See disorder_protocol(5) for the meanings of the keys.
+ All keys are plain strings but the values will be unicode strings."""
return self._somequeue("queue")
def _somedir(self, command, dir, re):
The callback should return True to continue or False to stop (don't
forget this, or your program will mysteriously misbehave). Once you
- stop reading the log the connection is useless and should be deleted.
+ stop reading the log the connection is useless and should be
+ deleted.
It is suggested that you use the disorder.monitor class instead of
calling this method directly, but this is not mandatory.
self._simple("schedule-del", event)
def schedule_get(self, event):
- """Get the details for an event as a dict (returns None if event not found)"""
+ """Get the details for an event as a dict (returns None if
+ event not found)"""
res, details = self._simple("schedule-get", event)
if res == 555:
return None
"""Adopt a randomly picked track"""
self._simple("adopt", id)
+ def playlist_delete(self, playlist):
+ """Delete a playlist"""
+ res, details = self._simple("playlist-delete", playlist)
+ if res == 555:
+ raise operationError(res, details, "playlist-delete")
+
+ def playlist_get(self, playlist):
+ """Get the contents of a playlist
+
+ The return value is an array of track names, or None if there is no
+ such playlist."""
+ res, details = self._simple("playlist-get", playlist)
+ if res == 555:
+ return None
+ return self._body()
+
+ def playlist_lock(self, playlist):
+ """Lock a playlist. Playlists can only be modified when locked."""
+ self._simple("playlist-lock", playlist)
+
+ def playlist_unlock(self):
+ """Unlock the locked playlist."""
+ self._simple("playlist-unlock")
+
+ def playlist_set(self, playlist, tracks):
+ """Set the contents of a playlist. The playlist must be locked.
+
+ Arguments:
+ playlist -- Playlist to set
+ tracks -- Array of tracks"""
+ self._simple_body(tracks, "playlist-set", playlist)
+
+ def playlist_set_share(self, playlist, share):
+ """Set the sharing status of a playlist"""
+ self._simple("playlist-set-share", playlist, share)
+
+ def playlist_get_share(self, playlist):
+ """Returns the sharing status of a playlist"""
+ res, details = self._simple("playlist-get-share", playlist)
+ if res == 555:
+ return None
+ return _split(details)[0]
+
+ def playlists(self):
+ """Returns the list of visible playlists"""
+ self._simple("playlists")
+ return self._body()
+
########################################################################
# I/O infrastructure
else:
raise protocolError(self.who, "invalid response %s")
- def _send(self, *command):
- # Quote and send a command
+ def _send(self, body, *command):
+ # Quote and send a command and optional body
#
# Returns the encoded command.
quoted = _quote(command)
try:
self.w.write(encoded)
self.w.write("\n")
+ if body != None:
+ for l in body:
+ if l[0] == ".":
+ self.w.write(".")
+ self.w.write(l)
+ self.w.write("\n")
+ self.w.write(".\n")
self.w.flush()
return encoded
except IOError, e:
self._disconnect()
raise
- def _simple(self, *command):
+ def _simple(self, *command):
# Issue a simple command, throw an exception on error
#
# If an I/O error occurs, disconnect from the server.
# On success or 'normal' errors returns response as a (code, details) tuple
#
# On error raise operationError
+ return self._simple_body(None, *command)
+
+ def _simple_body(self, body, *command):
+ # Issue a simple command with optional body, throw an exception on error
+ #
+ # If an I/O error occurs, disconnect from the server.
+ #
+ # On success or 'normal' errors returns response as a (code, details) tuple
+ #
+ # On error raise operationError
if self.state == 'disconnected':
self.connect()
if command:
- cmd = self._send(*command)
+ cmd = self._send(body, *command)
else:
cmd = None
res, details = self._response()
class monitor:
"""DisOrder event log monitor class
- Intended to be subclassed with methods corresponding to event log messages
- the implementor cares about over-ridden."""
+ Intended to be subclassed with methods corresponding to event log
+ messages the implementor cares about over-ridden."""
def __init__(self, c=None):
"""Constructor for the monitor class
def run(self):
"""Start monitoring logs. Continues monitoring until one of the
- message-specific methods returns False. Can be called more than once
- (but not recursively!)"""
+ message-specific methods returns False. Can be called more than
+ once (but not recursively!)"""
self.c.log(self._callback)
def when(self):
setup-guest schedule-del schedule-list
schedule-set-global schedule-unset-global schedule-play
adopt
+ playlist-del playlist-get playlist-set playlists
-h --help -H --help-commands --version -V --config -c
--length --debug -d" \
disorder
{ "debug", no_argument, 0, 'D' },
{ "recover", no_argument, 0, 'r' },
{ "recover-fatal", no_argument, 0, 'R' },
- { "trackdb", no_argument, 0, 't' },
- { "searchdb", no_argument, 0, 's' },
{ "recompute-aliases", no_argument, 0, 'a' },
{ "remove-pathless", no_argument, 0, 'P' },
{ 0, 0, 0, 0 }
exit(0);
}
+/** @brief Dump one record
+ * @param s Output stream
+ * @param tag Tag for error messages
+ * @param letter Prefix leter for dumped record
+ * @param dbname Database name
+ * @param db Database handle
+ * @param tid Transaction handle
+ * @return 0 or @c DB_LOCK_DEADLOCK
+ */
+static int dump_one(struct sink *s,
+ const char *tag,
+ int letter,
+ const char *dbname,
+ DB *db,
+ DB_TXN *tid) {
+ int err;
+ DBC *cursor;
+ DBT k, d;
+
+ /* dump the preferences */
+ cursor = trackdb_opencursor(db, tid);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_FIRST);
+ while(err == 0) {
+ if(sink_writec(s, letter) < 0
+ || urlencode(s, k.data, k.size)
+ || sink_writec(s, '\n') < 0
+ || urlencode(s, d.data, d.size)
+ || sink_writec(s, '\n') < 0)
+ fatal(errno, "error writing to %s", tag);
+ err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+ DB_NEXT);
+ }
+ switch(err) {
+ case DB_LOCK_DEADLOCK:
+ trackdb_closecursor(cursor);
+ return err;
+ case DB_NOTFOUND:
+ return trackdb_closecursor(cursor);
+ case 0:
+ assert(!"cannot happen");
+ default:
+ fatal(0, "error reading %s: %s", dbname, db_strerror(err));
+ }
+}
+
+static struct {
+ int letter;
+ const char *dbname;
+ DB **db;
+} dbtable[] = {
+ { 'P', "prefs.db", &trackdb_prefsdb },
+ { 'G', "global.db", &trackdb_globaldb },
+ { 'U', "users.db", &trackdb_usersdb },
+ { 'W', "schedule.db", &trackdb_scheduledb },
+ { 'L', "playlists.db", &trackdb_playlistsdb },
+ /* avoid 'T' and 'S' for now */
+};
+#define NDBTABLE (sizeof dbtable / sizeof *dbtable)
+
/* dump prefs to FP, return nonzero on error */
-static void do_dump(FILE *fp, const char *tag,
- int tracksdb, int searchdb) {
- DBC *cursor = 0;
+static void do_dump(FILE *fp, const char *tag) {
DB_TXN *tid;
struct sink *s = sink_stdio(tag, fp);
- int err;
- DBT k, d;
for(;;) {
tid = trackdb_begin_transaction();
fatal(errno, "error calling fflush");
if(ftruncate(fileno(fp), 0) < 0)
fatal(errno, "error calling ftruncate");
- if(fprintf(fp, "V%c\n", (tracksdb || searchdb) ? '1' : '0') < 0)
+ if(fprintf(fp, "V0") < 0)
fatal(errno, "error writing to %s", tag);
- /* dump the preferences */
- cursor = trackdb_opencursor(trackdb_prefsdb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('P', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
-
- /* dump the global preferences */
- cursor = trackdb_opencursor(trackdb_globaldb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('G', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
+ for(size_t n = 0; n < NDBTABLE; ++n)
+ if(dump_one(s, tag,
+ dbtable[n].letter, dbtable[n].dbname, *dbtable[n].db,
+ tid))
+ goto fail;
- /* dump the users */
- cursor = trackdb_opencursor(trackdb_usersdb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('U', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
-
- /* dump the schedule */
- cursor = trackdb_opencursor(trackdb_scheduledb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('W', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
-
-
- if(tracksdb) {
- cursor = trackdb_opencursor(trackdb_tracksdb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('T', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
- cursor = 0;
- }
-
- if(searchdb) {
- cursor = trackdb_opencursor(trackdb_searchdb, tid);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_FIRST);
- while(err == 0) {
- if(fputc('S', fp) < 0
- || urlencode(s, k.data, k.size)
- || fputc('\n', fp) < 0
- || urlencode(s, d.data, d.size)
- || fputc('\n', fp) < 0)
- fatal(errno, "error writing to %s", tag);
- err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
- DB_NEXT);
- }
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } cursor = 0;
- }
-
- if(fputs("E\n", fp) < 0) fatal(errno, "error writing to %s", tag);
- if(err == DB_LOCK_DEADLOCK) {
- error(0, "c->c_get: %s", db_strerror(err));
- goto fail;
- }
- if(err && err != DB_NOTFOUND)
- fatal(0, "cursor->c_get: %s", db_strerror(err));
- if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
+ if(fputs("E\n", fp) < 0)
+ fatal(errno, "error writing to %s", tag);
break;
fail:
- trackdb_closecursor(cursor);
- cursor = 0;
info("aborting transaction and retrying dump");
trackdb_abort_transaction(tid);
}
/* undump from FP, return 0 or DB_LOCK_DEADLOCK */
static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
int err, c;
- DBT k, d;
- const char *which_name;
- DB *which_db;
info("undumping");
if(fseek(fp, 0, SEEK_SET) < 0)
if((err = truncdb(tid, trackdb_scheduledb))) return err;
c = getc(fp);
while(!ferror(fp) && !feof(fp)) {
+ for(size_t n = 0; n < NDBTABLE; ++n) {
+ if(dbtable[n].letter == c) {
+ DB *db = *dbtable[n].db;
+ const char *dbname = dbtable[n].dbname;
+ DBT k, d;
+
+ if(undump_dbt(fp, tag, prepare_data(&k))
+ || undump_dbt(fp, tag, prepare_data(&d)))
+ break;
+ switch(err = db->put(db, tid, &k, &d, 0)) {
+ case 0:
+ break;
+ case DB_LOCK_DEADLOCK:
+ error(0, "error updating %s: %s", dbname, db_strerror(err));
+ return err;
+ default:
+ fatal(0, "error updating %s: %s", dbname, db_strerror(err));
+ }
+ goto next;
+ }
+ }
+
switch(c) {
case 'V':
c = getc(fp);
break;
case 'E':
return 0;
- case 'P':
- case 'G':
- case 'U':
- case 'W':
- switch(c) {
- case 'P':
- which_db = trackdb_prefsdb;
- which_name = "prefs.db";
- break;
- case 'G':
- which_db = trackdb_globaldb;
- which_name = "global.db";
- break;
- case 'U':
- which_db = trackdb_usersdb;
- which_name = "users.db";
- break;
- case 'W': /* for 'when' */
- which_db = trackdb_scheduledb;
- which_name = "scheduledb.db";
- break;
- default:
- abort();
- }
- if(undump_dbt(fp, tag, prepare_data(&k))
- || undump_dbt(fp, tag, prepare_data(&d)))
- break;
- switch(err = which_db->put(which_db, tid, &k, &d, 0)) {
- case 0:
- break;
- case DB_LOCK_DEADLOCK:
- error(0, "error updating %s: %s", which_name, db_strerror(err));
- return err;
- default:
- fatal(0, "error updating %s: %s", which_name, db_strerror(err));
- }
- break;
- case 'T':
- case 'S':
- if(undump_dbt(fp, tag, prepare_data(&k))
- || undump_dbt(fp, tag, prepare_data(&d)))
- break;
- /* We don't restore the tracks.db or search.db entries, instead
- * we recompute them */
- break;
case '\n':
break;
+ default:
+ if(c >= 32 && c <= 126)
+ fatal(0, "unexpected character '%c'", c);
+ else
+ fatal(0, "unexpected character 0x%02X", c);
}
+ next:
c = getc(fp);
}
if(ferror(fp))
int main(int argc, char **argv) {
int n, dump = 0, undump = 0, recover = TRACKDB_NO_RECOVER, recompute = 0;
- int tracksdb = 0, searchdb = 0, remove_pathless = 0, fd;
+ int remove_pathless = 0, fd;
const char *path;
char *tmp;
FILE *fp;
mem_init();
- while((n = getopt_long(argc, argv, "hVc:dDutsrRaP", options, 0)) >= 0) {
+ while((n = getopt_long(argc, argv, "hVc:dDurRaP", options, 0)) >= 0) {
switch(n) {
case 'h': help();
case 'V': version("disorder-dump");
case 'd': dump = 1; break;
case 'u': undump = 1; break;
case 'D': debugging = 1; break;
- case 't': tracksdb = 1; break;
- case 's': searchdb = 1; break;
case 'r': recover = TRACKDB_NORMAL_RECOVER;
case 'R': recover = TRACKDB_FATAL_RECOVER;
case 'a': recompute = 1; break;
}
if(dump + undump + recompute != 1)
fatal(0, "choose exactly one of --dump, --undump or --recompute-aliases");
- if((undump || recompute) && (tracksdb || searchdb))
- fatal(0, "--trackdb and --searchdb with --undump or --recompute-aliases");
if(recompute) {
if(optind != argc)
fatal(0, "--recompute-aliases does not take a filename");
fatal(errno, "error opening %s", tmp);
if(!(fp = fdopen(fd, "w")))
fatal(errno, "fdopen on %s", tmp);
- do_dump(fp, tmp, tracksdb, searchdb);
+ do_dump(fp, tmp);
if(fclose(fp) < 0) fatal(errno, "error closing %s", tmp);
if(rename(tmp, path) < 0)
fatal(errno, "error renaming %s to %s", tmp, path);
int pf;
};
+struct conn;
+
+/** @brief Signature for line reader callback
+ * @param c Connection
+ * @param line Line
+ * @return 0 if incomplete, 1 if complete
+ *
+ * @p line is 0-terminated and excludes the newline. It points into the
+ * input buffer so will become invalid shortly.
+ */
+typedef int line_reader_type(struct conn *c,
+ char *line);
+
+/** @brief Signature for with-body command callbacks
+ * @param c Connection
+ * @param body List of body lines
+ * @param nbody Number of body lines
+ * @param u As passed to fetch_body()
+ * @return 0 to suspend input, 1 if complete
+ *
+ * The body strings are allocated (so survive indefinitely) and don't include
+ * newlines.
+ */
+typedef int body_callback_type(struct conn *c,
+ char **body,
+ int nbody,
+ void *u);
+
/** @brief One client connection */
struct conn {
/** @brief Read commands from here */
struct conn *next;
/** @brief True if pending rescan had 'wait' set */
int rescan_wait;
+ /** @brief Playlist that this connection locks */
+ const char *locked_playlist;
+ /** @brief When that playlist was locked */
+ time_t locked_when;
+ /** @brief Line reader function */
+ line_reader_type *line_reader;
+ /** @brief Called when command body has been read */
+ body_callback_type *body_callback;
+ /** @brief Passed to @c body_callback */
+ void *body_u;
+ /** @brief Accumulating body */
+ struct vector body[1];
};
/** @brief Linked list of connections */
size_t bytes,
int eof,
void *u);
+static int c_playlist_set_body(struct conn *c,
+ char **body,
+ int nbody,
+ void *u);
+static int fetch_body(struct conn *c,
+ body_callback_type body_callback,
+ void *u);
+static int body_line(struct conn *c, char *line);
+static int command(struct conn *c, char *line);
static const char *noyes[] = { "no", "yes" };
return 1;
}
-static int c_tags(struct conn *c,
- char attribute((unused)) **vec,
- int attribute((unused)) nvec) {
- char **tags = trackdb_alltags();
-
- sink_printf(ev_writer_sink(c->w), "253 Tag list follows\n");
- while(*tags) {
+static int list_response(struct conn *c,
+ const char *reply,
+ char **list) {
+ sink_printf(ev_writer_sink(c->w), "253 %s\n", reply);
+ while(*list) {
sink_printf(ev_writer_sink(c->w), "%s%s\n",
- **tags == '.' ? "." : "", *tags);
- ++tags;
+ **list == '.' ? "." : "", *list);
+ ++list;
}
sink_writes(ev_writer_sink(c->w), ".\n");
return 1; /* completed */
}
+static int c_tags(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ return list_response(c, "Tag list follows", trackdb_alltags());
+}
+
static int c_set_global(struct conn *c,
char **vec,
int attribute((unused)) nvec) {
static int c_users(struct conn *c,
char attribute((unused)) **vec,
int attribute((unused)) nvec) {
- /* TODO de-dupe with c_tags */
- char **users = trackdb_listusers();
-
- sink_writes(ev_writer_sink(c->w), "253 User list follows\n");
- while(*users) {
- sink_printf(ev_writer_sink(c->w), "%s%s\n",
- **users == '.' ? "." : "", *users);
- ++users;
- }
- sink_writes(ev_writer_sink(c->w), ".\n");
- return 1; /* completed */
+ return list_response(c, "User list follows", trackdb_listusers());
}
/** @brief Base64 mapping table for confirmation strings
return 1;
}
+static int playlist_response(struct conn *c,
+ int err) {
+ switch(err) {
+ case 0:
+ assert(!"cannot cope with success");
+ case EACCES:
+ sink_writes(ev_writer_sink(c->w), "550 Access denied\n");
+ break;
+ case EINVAL:
+ sink_writes(ev_writer_sink(c->w), "550 Invalid playlist name\n");
+ break;
+ case ENOENT:
+ sink_writes(ev_writer_sink(c->w), "555 No such playlist\n");
+ break;
+ default:
+ sink_writes(ev_writer_sink(c->w), "550 Error accessing playlist\n");
+ break;
+ }
+ return 1;
+}
+
+static int c_playlist_get(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ char **tracks;
+ int err;
+
+ if(!(err = trackdb_playlist_get(vec[0], c->who, &tracks, 0, 0)))
+ return list_response(c, "Playlist contents follows", tracks);
+ else
+ return playlist_response(c, err);
+}
+
+static int c_playlist_set(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ return fetch_body(c, c_playlist_set_body, vec[0]);
+}
+
+static int c_playlist_set_body(struct conn *c,
+ char **body,
+ int nbody,
+ void *u) {
+ const char *playlist = u;
+ int err;
+
+ if(!c->locked_playlist
+ || strcmp(playlist, c->locked_playlist)) {
+ sink_writes(ev_writer_sink(c->w), "550 Playlist is not locked\n");
+ return 1;
+ }
+ if(!(err = trackdb_playlist_set(playlist, c->who,
+ body, nbody, 0))) {
+ sink_printf(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+ } else
+ return playlist_response(c, err);
+}
+
+static int c_playlist_get_share(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ char *share;
+ int err;
+
+ if(!(err = trackdb_playlist_get(vec[0], c->who, 0, 0, &share))) {
+ sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(share));
+ return 1;
+ } else
+ return playlist_response(c, err);
+}
+
+static int c_playlist_set_share(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int err;
+
+ if(!(err = trackdb_playlist_set(vec[0], c->who, 0, 0, vec[1]))) {
+ sink_printf(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+ } else
+ return playlist_response(c, err);
+}
+
+static int c_playlists(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ char **p;
+
+ trackdb_playlist_list(c->who, &p, 0);
+ return list_response(c, "List of playlists follows", p);
+}
+
+static int c_playlist_delete(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int err;
+
+ if(!(err = trackdb_playlist_delete(vec[0], c->who))) {
+ sink_writes(ev_writer_sink(c->w), "250 OK\n");
+ return 1;
+ } else
+ return playlist_response(c, err);
+}
+
+static int c_playlist_lock(struct conn *c,
+ char **vec,
+ int attribute((unused)) nvec) {
+ int err;
+ struct conn *cc;
+
+ /* Check we're allowed to modify this playlist */
+ if((err = trackdb_playlist_set(vec[0], c->who, 0, 0, 0)))
+ return playlist_response(c, err);
+ /* If we hold a lock don't allow a new one */
+ if(c->locked_playlist) {
+ sink_writes(ev_writer_sink(c->w), "550 Already holding a lock\n");
+ return 1;
+ }
+ /* See if some other connection locks the same playlist */
+ for(cc = connections; cc; cc = cc->next)
+ if(cc->locked_playlist && !strcmp(cc->locked_playlist, vec[0]))
+ break;
+ if(cc) {
+ /* TODO: implement config->playlist_lock_timeout */
+ sink_writes(ev_writer_sink(c->w), "550 Already locked\n");
+ return 1;
+ }
+ c->locked_playlist = xstrdup(vec[0]);
+ time(&c->locked_when);
+ sink_writes(ev_writer_sink(c->w), "250 Acquired lock\n");
+ return 1;
+}
+
+static int c_playlist_unlock(struct conn *c,
+ char attribute((unused)) **vec,
+ int attribute((unused)) nvec) {
+ if(!c->locked_playlist) {
+ sink_writes(ev_writer_sink(c->w), "550 Not holding a lock\n");
+ return 1;
+ }
+ c->locked_playlist = 0;
+ sink_writes(ev_writer_sink(c->w), "250 Released lock\n");
+ return 1;
+}
+
static const struct command {
/** @brief Command name */
const char *name;
{ "pause", 0, 0, c_pause, RIGHT_PAUSE },
{ "play", 1, 1, c_play, RIGHT_PLAY },
{ "playing", 0, 0, c_playing, RIGHT_READ },
+ { "playlist-delete", 1, 1, c_playlist_delete, RIGHT_PLAY },
+ { "playlist-get", 1, 1, c_playlist_get, RIGHT_READ },
+ { "playlist-get-share", 1, 1, c_playlist_get_share, RIGHT_READ },
+ { "playlist-lock", 1, 1, c_playlist_lock, RIGHT_PLAY },
+ { "playlist-set", 1, 1, c_playlist_set, RIGHT_PLAY },
+ { "playlist-set-share", 2, 2, c_playlist_set_share, RIGHT_PLAY },
+ { "playlist-unlock", 0, 0, c_playlist_unlock, RIGHT_PLAY },
+ { "playlists", 0, 0, c_playlists, RIGHT_READ },
{ "prefs", 1, 1, c_prefs, RIGHT_READ },
{ "queue", 0, 0, c_queue, RIGHT_READ },
{ "random-disable", 0, 0, c_random_disable, RIGHT_GLOBAL_PREFS },
{ "volume", 0, 2, c_volume, RIGHT_READ|RIGHT_VOLUME }
};
+/** @brief Fetch a command body
+ * @param c Connection
+ * @param body_callback Called with body
+ * @param u Passed to body_callback
+ * @return 1
+ */
+static int fetch_body(struct conn *c,
+ body_callback_type body_callback,
+ void *u) {
+ assert(c->line_reader == command);
+ c->line_reader = body_line;
+ c->body_callback = body_callback;
+ c->body_u = u;
+ vector_init(c->body);
+ return 1;
+}
+
+/** @brief @ref line_reader_type callback for command body lines
+ * @param c Connection
+ * @param line Line
+ * @return 1 if complete, 0 if incomplete
+ *
+ * Called from reader_callback().
+ */
+static int body_line(struct conn *c,
+ char *line) {
+ if(*line == '.') {
+ ++line;
+ if(!*line) {
+ /* That's the lot */
+ c->line_reader = command;
+ vector_terminate(c->body);
+ return c->body_callback(c, c->body->vec, c->body->nvec, c->body_u);
+ }
+ }
+ vector_append(c->body, xstrdup(line));
+ return 1; /* completed */
+}
+
static void command_error(const char *msg, void *u) {
struct conn *c = u;
sink_printf(ev_writer_sink(c->w), "500 parse error: %s\n", msg);
}
-/* process a command. Return 1 if complete, 0 if incomplete. */
+/** @brief @ref line_reader_type callback for commands
+ * @param c Connection
+ * @param line Line
+ * @return 1 if complete, 0 if incomplete
+ *
+ * Called from reader_callback().
+ */
static int command(struct conn *c, char *line) {
char **vec;
int nvec, n;
while((eol = memchr(ptr, '\n', bytes))) {
*eol++ = 0;
ev_reader_consume(reader, eol - (char *)ptr);
- complete = command(c, ptr);
+ complete = c->line_reader(c, ptr); /* usually command() */
bytes -= (eol - (char *)ptr);
ptr = eol;
if(!complete) {
c->reader = reader_callback;
c->l = l;
c->rights = 0;
+ c->line_reader = command;
connections = c;
gcry_randomize(c->nonce, sizeof c->nonce, GCRY_STRONG_RANDOM);
sink_printf(ev_writer_sink(c->w), "231 %d %s %s\n",
TESTS=cookie.py dbversion.py dump.py files.py play.py queue.py \
recode.py search.py user-upgrade.py user.py aliases.py \
- schedule.py
+ schedule.py playlists.py
TESTS_ENVIRONMENT=${PYTHON} -u
--- /dev/null
+#! /usr/bin/env python
+#
+# This file is part of DisOrder.
+# Copyright (C) 2008 Richard Kettlewell
+#
+# 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
+#
+import dtest,disorder
+
+def test():
+ """Playlist testing"""
+ dtest.start_daemon()
+ dtest.create_user()
+ c = disorder.client()
+ c.random_disable()
+ #
+ print " checking initial playlist set is empty"
+ l = c.playlists()
+ assert l == [], "checking initial playlist set is empty"
+ #
+ print " creating a shared playlist"
+ c.playlist_lock("wibble")
+ c.playlist_set("wibble", ["one", "two", "three"])
+ c.playlist_unlock()
+ print " checking new playlist appears in list"
+ l = c.playlists()
+ assert l == ["wibble"], "checking new playlists"
+ print " checking new playlist contents is as assigned"
+ l = c.playlist_get("wibble")
+ assert l == ["one", "two", "three"], "checking playlist contents"
+ #
+ print " checking new playlist is shared"
+ s = c.playlist_get_share("wibble")
+ assert s == "shared", "checking playlist is shared"
+ #
+ print " checking cannot unshare un-owned playlist"
+ try:
+ c.playlist_set_share("wibble", "private")
+ print "*** should not be able to adjust shared playlist's sharing ***"
+ assert False
+ except disorder.operationError:
+ pass # good
+ #
+ print " modifying shared playlist"
+ c.playlist_lock("wibble")
+ c.playlist_set("wibble", ["three", "two", "one"])
+ c.playlist_unlock()
+ print " checking updated playlist contents is as assigned"
+ l = c.playlist_get("wibble")
+ assert l == ["three", "two", "one"], "checking modified playlist contents"
+ #
+ print " creating a private playlist"
+ c.playlist_lock("fred.spong")
+ c.playlist_set("fred.spong", ["a", "b", "c"])
+ c.playlist_unlock()
+ s = c.playlist_get_share("fred.spong")
+ assert s == "private", "owned playlists are private by default"
+ #
+ print " creating a public playlist"
+ c.playlist_lock("fred.foo")
+ c.playlist_set("fred.foo", ["p", "q", "r"])
+ c.playlist_set_share("fred.foo", "public")
+ c.playlist_unlock()
+ s = c.playlist_get_share("fred.foo")
+ assert s == "public", "new playlist is now public"
+ #
+ print " checking fred can see all playlists"
+ l = c.playlists()
+ assert dtest.lists_have_same_contents(l,
+ ["fred.spong", "fred.foo", "wibble"]), "playlist list is as expected"
+ #
+ print " adding a second user"
+ c.adduser("bob", "bobpass")
+ d = disorder.client(user="bob", password="bobpass")
+ print " checking bob cannot see fred's private playlist"
+ l = d.playlists()
+ assert dtest.lists_have_same_contents(l,
+ ["fred.foo", "wibble"]), "playlist list is as expected"
+ #
+ print " checking bob can read shared and public playlists"
+ d.playlist_get("wibble")
+ d.playlist_get("fred.foo")
+ print " checking bob cannot read private playlist"
+ try:
+ d.playlist_get("fred.spong")
+ print "*** should not be able to read private playlist ***"
+ assert False
+ except disorder.operationError:
+ pass # good
+ #
+ print " checking bob cannot modify fred's playlists"
+ try:
+ d.playlist_lock("fred.foo")
+ print "*** should not be able to lock fred's public playlist ***"
+ assert False
+ except disorder.operationError:
+ pass # good
+ try:
+ d.playlist_lock("fred.spong")
+ print "*** should not be able to lock fred's private playlist ***"
+ assert False
+ except disorder.operationError:
+ pass # good
+ print " checking unlocked playlists cannot be modified"
+ #
+ try:
+ c.playlist_set("wibble", ["a"])
+ print "*** should not be able to modify unlocked playlists ***"
+ assert False
+ except disorder.operationError:
+ pass # good
+ #
+ print " deleting playlists"
+ c.playlist_delete("fred.spong")
+ l = c.playlists()
+ assert dtest.lists_have_same_contents(l,
+ ["fred.foo", "wibble"])
+ try:
+ d.playlist_delete("fred.foo")
+ print "*** should not be to delete fred's playlist ***"
+ assert False
+ except disorder.operationError:
+ pass # good
+ d.playlist_delete("wibble")
+ l = c.playlists()
+ assert l == ["fred.foo"]
+ c.playlist_delete("fred.foo")
+ l = c.playlists()
+ assert l == []
+ try:
+ c.playlist_delete("nonesuch")
+ print "*** should not be to delete nonexistent playlist ***"
+ assert False
+ except disorder.operationError:
+ pass # good
+
+if __name__ == '__main__':
+ dtest.run()